ZooKeeper 是 Apache 的一個頂級項目,為分布式應用提供高效、高可用的分布式協調服務,提供了諸如數據發布/訂閱、負載均衡、命名服務、分布式協調/通知和分布式鎖等分布式基礎服務。由於 ZooKeeper 便捷的使用方式、卓越的性能和良好的穩定性,被廣泛地應用於諸如 Hadoop、HBase、Kafka 和 Dubbo 等大型分布式系統中。
本文的目標讀者是對 ZooKeeper 有一定了解的技術人員,將從 ZooKeeper 運行模式、集群組成、容災和水平擴容四方面逐步深入,最終構建出高可用的 ZooKeeper 集群。
一、運行模式
Zookeeper 有三種運行模式:單機模式、偽集群模式和集群模式。
1.1 單機模式
這種模式一般適用於開發測試環境,一方面我們沒有那么多機器資源,另外就是平時的開發調試並不需要極好的穩定性。
在 Linux 環境下運行單機模式需要執行以下步驟:
1. 准備 Java 運行環境
由於zookeeper集群的運行需要Java運行環境,所以需要首先安裝 JDK,這里安裝 Java 1.6 或更高版本的 JDK,並配置好 Java 相關的環境變量 $JAVA_HOME 。
2. 下載 ZooKeeper 安裝包
下載地址:http://zookeeper.apache.org/releases.html。選擇最新的 stable 版本並解壓到指定目錄,我們用 $ZK_HOME 表示該目錄。
為了能夠在任意目錄啟動zookeeper,我們需要配置環境變量。
ps:你也可以不配,這不是搭建集群的必要操作,只不過如果你不配置環境變量,那么每次啟動zookeeper需要到安裝文件的 bin 目錄下去啟動。
首先進入到 /etc/profile 目錄,添加相應的配置信息:
#set zookeeper environment export ZK_HOME=/usr/local/zookeeper/zookeeper-3.6.3 export PATH=$PATH:$ZK_HOME/bin
然后通過如下命令使得環境變量生效:
source /etc/profle
3. 配置 zoo.cfg
首次使用 ZooKeeper,需要將 $ZK_HOME 下的 zoo_sample.cfg 文件重命名為 zoo.cfg,並進行以下配置
tickTime=2000 ##Zookeeper最小時間單元,單位毫秒(ms),默認值為3000 dataDir=/var/lib/zookeeper ##Zookeeper服務器存儲快照文件的目錄,必須配置 dataLogDir=/var/lib/log ##Zookeeper服務器存儲事務日志的目錄,默認為dataDir clientPort=2181 ##服務器對外服務端口,一般設置為2181 initLimit=5 ##Leader服務器等待Follower啟動並完成數據同步的時間,默認值10,表示tickTime的10倍 syncLimit=2 ##Leader服務器和Follower之間進行心跳檢測的最大延時時間,默認值5,表示tickTime的5倍
4. 啟動服務
使用 $ZK_HOME/bin 目錄下的 zkServer.sh 腳本進行服務的啟動。
1.2 集群模式
zookeeper 集群通常是用來對用戶的分布式應用程序提供協調服務的,為了保證數據的一致性,對 zookeeper 集群進行了這樣三種角色划分:leader、follower、observer分別對應着總統、議員和觀察者。
領導者(leader):負責進行投票的發起和決議,更新系統狀態。
跟隨者(follower):用於接收客戶端請求並向客戶端返回結果以及在選舉過程中參與投票。
觀察者(observer):也可以接收客戶端連接,將寫請求轉發給leader節點,但是不參與投票過程,只同步leader的狀態。通常對查詢操作做負載。
一個 ZooKeeper 集群通常由一組機器組成,一般 3 台以上就可以組成一個可用的 ZooKeeper 集群了。
組成 ZooKeeper 集群的每台機器都會在內存中維護當前的服務器狀態,並且每台機器之間都會互相保持通信。
重要的一點是,只要集群中存在超過一半的機器能夠正常工作,那么整個集群就能夠正常對外服務。
ZooKeeper 的客戶端程序會選擇和集群中的任意一台服務器創建一個 TCP 連接,而且一旦客戶端和服務器斷開連接,客戶端就會自動連接到集群中的其他服務器。
那么如何運行 ZooKeeper 集群模式呢?首先假如我們有三台服務器,IP 分別為 IP1、IP2 和 IP3,則需要執行以下步驟:
1. 准備 Java 運行環境(同上)
2. 下載 ZooKeeper 安裝包(同上)
3. 配置 zoo.cfg
tickTime=2000 dataDir=/var/lib/zookeeper dataLogDir=/var/lib/log clientPort=2181 initLimit=5 syncLimit=2 server.1=IP1:2888:3888 server.2=IP2:2888:3888 server.3=IP3:2888:3888
可以看到,相比於單機模式,集群模式多了 server.id=host:port1:port2 的配置。
其中,id 被稱為 Server ID,用來標識該機器在集群中的機器序號(在每台機器的 dataDir 目錄下創建 myid 文件,文件內容即為該機器對應的 Server ID 數字)。host 為機器 IP,port1 用於指定 Follower 服務器與 Leader 服務器進行通信和數據同步的端口,port2用於進行 Leader 選舉過程中的投票通信。
在多解釋下參數內容:
①、tickTime:基本事件單元,這個時間是作為Zookeeper服務器之間或客戶端與服務器之間維持心跳的時間間隔,每隔tickTime時間就會發送一個心跳;最小 的session過期時間為2倍tickTime
②、dataDir:存儲內存中數據庫快照的位置,除非另有說明,否則指向數據庫更新的事務日志。注意:應該謹慎的選擇日志存放的位置,使用專用的日志存儲設備能夠大大提高系統的性能,如果將日志存儲在比較繁忙的存儲設備上,那么將會很大程度上影像系統性能。
③、client:監聽客戶端連接的端口。
④、initLimit:允許follower連接並同步到Leader的初始化連接時間,以tickTime為單位。當初始化連接時間超過該值,則表示連接失敗。
⑤、syncLimit:表示Leader與Follower之間發送消息時,請求和應答時間長度。如果follower在設置時間內不能與leader通信,那么此follower將會被丟棄。
⑥、server.A=B:C:D
A:其中 A 是一個數字,表示這個是服務器的編號;
B:是這個服務器的 ip 地址;
C:Zookeeper服務器之間的通信端口;
D:Leader選舉的端口。
我們需要修改的第一個是 dataDir ,在指定的位置處創建好目錄。
第二個需要新增的是 server.A=B:C:D 配置,其中 A 對應下面我們即將介紹的myid 文件。B是集群的各個IP地址,C:D 是端口配置
4. 創建 myid 文件
在 dataDir 目錄下創建名為 myid 的文件,在文件第一行寫上對應的 Server ID。
5. 按照相同步驟,為其他機器配置 zoo.cfg 和 myid文件
6. 啟動服務
#啟動命令: zkServer.sh start #停止命令: zkServer.sh stop #重啟命令: zkServer.sh restart #查看集群節點狀態: zkServer.sh status
查看各個節點的狀態,三台機器中其中一台會被選舉成為leader,而剩下的兩台成為了 follower。這時候,如果你將leader節點機器關掉,會發現剩下兩台又會有一台變成了 leader節點。
1.3 偽集群模式
這是一種特殊的集群模式,即集群的所有服務器都部署在一台機器上。當你手頭上有一台比較好的機器,如果作為單機模式進行部署,就會浪費資源,這種情況下,ZooKeeper允許你在一台機器上通過啟動不同的端口來啟動多個 ZooKeeper 服務實例,以此來以集群的特性來對外服務。
這種模式下,只需要把 zoo.cfg 做如下修改:
tickTime=2000 dataDir=/var/lib/zookeeper dataLogDir=/var/lib/log clientPort=2181 initLimit=5 syncLimit=2 server.1=IP1:2888:3888 server.2=IP1:2889:3889 server.3=IP1:2890:3890
二、集群組成
要搭建一個高可用的 ZooKeeper 集群,我們首先需要確定好集群的規模。
關於 ZooKeeper 集群的服務器組成,相信很多對 ZooKeeper 了解但是理解不夠深入的讀者,都存在或曾經存在過這樣一個錯誤的認識:為了使得 ZooKeeper 集群能夠順利地選舉出 Leader,必須將 ZooKeeper 集群的服務器數部署成奇數。這里我們需要澄清的一點是:任意台 ZooKeeper 服務器都能部署且能正常運行。
那么存在於這么多讀者中的這個錯誤認識是怎么回事呢?其實關於 ZooKeeper 集群服務器數,ZooKeeper 官方確實給出了關於奇數的建議,但絕大部分 ZooKeeper 用戶對於這個建議認識有偏差。在本書前面提到的“過半存活即可用”特性中,我們已經了解了,一個 ZooKeeper 集群如果要對外提供可用的服務,那么集群中必須要有過半的機器正常工作並且彼此之間能夠正常通信。基於這個特性,如果想搭建一個能夠允許 N 台機器 down 掉的集群,那么就要部署一個由 2*N+1 台服務器構成的 ZooKeeper 集群。因此,一個由 3 台機器構成的 ZooKeeper 集群,能夠在掛掉 1 台機器后依然正常工作,而對於一個由 5 台服務器構成的 ZooKeeper 集群,能夠對 2 台機器掛掉的情況進行容災。注意,如果是一個由6台服務器構成的 ZooKeeper 集群,同樣只能夠掛掉 2 台機器,因為如果掛掉 3 台,剩下的機器就無法實現過半了。
因此,從上面的講解中,我們其實可以看出,對於一個由 6 台機器構成的 ZooKeeper 集群來說,和一個由 5 台機器構成的 ZooKeeper 集群,其在容災能力上並沒有任何顯著的優勢,反而多占用了一個服務器資源。基於這個原因,ZooKeeper 集群通常設計部署成奇數台服務器即可。
在詳細看為什么 zookeeper 選擇奇數節點數?
①、容錯率
首先從容錯率來說明:(需要保證集群能夠有半數進行投票)
2台服務器,至少2台正常運行才行(2的半數為1,半數以上最少為2),正常運行1台服務器都不允許掛掉,但是相對於 單節點服務器,2台服務器還有兩個單點故障,所以直接排除了。
3台服務器,至少2台正常運行才行(3的半數為1.5,半數以上最少為2),正常運行可以允許1台服務器掛掉
4台服務器,至少3台正常運行才行(4的半數為2,半數以上最少為3),正常運行可以允許1台服務器掛掉
5台服務器,至少3台正常運行才行(5的半數為2.5,半數以上最少為3),正常運行可以允許2台服務器掛掉
②、防腦裂
腦裂集群的腦裂通常是發生在節點之間通信不可達的情況下,集群會分裂成不同的小集群,小集群各自選出自己的leader節點,導致原有的集群出現多個leader節點的情況,這就是腦裂。
3台服務器,投票選舉半數為1.5,一台服務裂開,和另外兩台服務器無法通行,這時候2台服務器的集群(2票大於半數1.5票),所以可以選舉出leader,而 1 台服務器的集群無法選舉。
4台服務器,投票選舉半數為2,可以分成 1,3兩個集群或者2,2兩個集群,對於 1,3集群,3集群可以選舉;對於2,2集群,則不能選擇,造成沒有leader節點。
5台服務器,投票選舉半數為2.5,可以分成1,4兩個集群,或者2,3兩集群,這兩個集群分別都只能選舉一個集群,滿足zookeeper集群搭建數目。
以上分析,我們從容錯率以及防止腦裂兩方面說明了3台服務器是搭建集群的最少數目,4台發生腦裂時會造成沒有leader節點的錯誤。
三、容災
所謂容災,在 IT 行業通常是指我們的計算機信息系統具有的一種在遭受諸如火災、地震、斷電和其他基礎網絡設備故障等毀滅性災難的時候,依然能夠對外提供可用服務的能力。
對於一些普通的應用,為了達到容災標准,通常我們會選擇在多台機器上進行部署來組成一個集群,這樣即使在集群的一台或是若干台機器出現故障的情況下,整個集群依然能夠對外提供可用的服務。
而對於一些核心應用,不僅要通過使用多台機器構建集群的方式來提供服務,而且還要將集群中的機器部署在兩個機房,這樣的話,即使其中一個機房遭遇災難,依然能夠對外提供可用的服務。
上面講到的都是應用層面的容災模式,那么對於 ZooKeeper 這種底層組件來說,如何進行容災呢?講到這里,可能多少讀者會有疑問,ZooKeeper 既然已經解決了單點問題,那為什么還要進行容災呢?
3.1 單點問題
單點問題是分布式環境中最常見也是最經典的問題之一,在很多分布式系統中都會存在這樣的單點問題。
具體地說,單點問題是指在一個分布式系統中,如果某一個組件出現故障就會引起整個系統的可用性大大下降甚至是處於癱瘓狀態,那么我們就認為該組件存在單點問題。
ZooKeeper 確實已經很好地解決了單點問題。我們已經了解到,基於“過半”設計原則,ZooKeeper 在運行期間,集群中至少有過半的機器保存了最新的數據。因此,只要集群中超過半數的機器還能夠正常工作,整個集群就能夠對外提供服務。
3.2 容災
解決了單點問題,是不是該考慮容災了呢?答案是否定的,在搭建一個高可用的集群的時候依然需要考慮容災問題。正如上面講到的,如果集群中超過半數的機器還在正常工作,集群就能夠對外提供正常的服務。
那么,如果整個機房出現災難性的事故,這時顯然已經不是單點問題的范疇了。
在進行 ZooKeeper 的容災方案設計過程中,我們要充分考慮到“過半原則”。也就是說,無論發生什么情況,我們必須保證 ZooKeeper 集群中有超過半數的機器能夠正常工作。因此,通常有以下兩種部署方案。
3.3.1 雙機房部署
在進行容災方案的設計時,我們通常是以機房為單位來考慮問題。在現實中,很多公司的機房規模並不大,因此雙機房部署是個比較常見的方案。但是遺憾的是,在目前版本的 ZooKeeper 中,還沒有辦法能夠在雙機房條件下實現比較好的容災效果——因為無論哪個機房發生異常情況,都有可能使得 ZooKeeper 集群中可用的機器無法超過半數。當然,在擁有兩個機房的場景下,通常有一個機房是主要機房(一般而言,公司會花費更多的錢去租用一個穩定性更好、設備更可靠的機房,這個機房就是主要機房,而另外一個機房則更加廉價一些)。我們唯一能做的,就是盡量在主要機房部署更多的機器。例如,對於一個由 7 台機器組成的 ZooKeeper 集群,通常在主要機房中部署 4 台機器,剩下的 3 台機器部署到另外一個機房中。
3.3.2 三機房部署
既然在雙機房部署模式下並不能實現好的容災效果,那么對於有條件的公司,選擇三機房部署無疑是個更好的選擇,無論哪個機房發生了故障,剩下兩個機房的機器數量都超過半數。假如我們有三個機房可以部署服務,並且這三個機房間的網絡狀況良好,那么就可以在三個機房中都部署若干個機器來組成一個 ZooKeeper 集群。
我們假定構成 ZooKeeper 集群的機器總數為 N,在三個機房中部署的 ZooKeeper 服務器數分別為 N1、N2 和 N3,如果要使該 ZooKeeper 集群具有較好的容災能力,我們可以根據如下算法來計算 ZooKeeper 集群的機器部署方案。
1. 計算 N1
如果 ZooKeeper 集群的服務器總數是 N,那么:
N1 = (N-1)/2
在 Java 中,“/” 運算符會自動對計算結果向下取整操作。舉個例子,如果 N=8,那么 N1=3;如果 N=7,那么 N1 也等於 3。
2. 計算 N2 的可選值
N2 的計算規則和 N1 非常類似,只是 N2 的取值是在一個取值范圍內:
N2 的取值范圍是 1~(N-N1)/2
即如果 N=8,那么 N1=3,則 N2 的取值范圍就是 1~2,分別是 1 和 2。注意,1 和 2 僅僅是 N2 的可選值,並非最終值——如果 N2為某個可選值的時候,無法計算出 N3 的值,那么該可選值也無效。
3. 計算 N3,同時確定 N2 的值
很顯然,現在只剩下 N3 了,可以簡單的認為 N3 的取值就是剩下的機器數,即:
N3 = N - N1 - N2
只是 N3 的取值必須滿足 N3 < N1+N2。在滿足這個條件的基礎下,我們遍歷步驟 2 中計算得到的 N2 的可選值,即可得到三機房部署時每個機房的服務器數量了。
現在我們以 7 台機器為例,來看看如何分配三機房的機器分布。根據算法的步驟 1,我們首先確定 N1 的取值為 3。根據算法的步驟 2,我們確定了 N2 的可選值為 1 和 2。最后根據步驟 3,我們遍歷 N2 的可選值,即可得到兩種部署方案,分別是 (3,1,3) 和 (3,2,2)。以下是 Java 程序代碼對以上算法的一種簡單實現:
public class Allocation { static final int n = 7; public static void main(String[] args){ int n1,n2,n3; n1 = (n-1) / 2; int n2_max = (n-n1) / 2; for(int i=1; i<=n2_max; i++){ n2 = i; n3 = n - n1 -n2; if(n3 >= (n1+n2)){ continue; } System.out.println("("+n1+","+n2+","+n3+")"); } } }
四、水平擴容
水平可擴容可以說是對一個分布式系統在高可用性方面提出的基本的,也是非常重要的一個要求,通過水平擴容能夠幫助系統在不進行或進行極少改進工作的前提下,快速提高系統對外的服務支撐能力。簡單地講,水平擴容就是向集群中添加更多的機器,以提高系統的服務質量。
很遺憾的是,ZooKeeper 在水平擴容擴容方面做得並不十分完美,需要進行整個集群的重啟。通常有兩種重啟方式,一種是集群整體重啟,另外一種是逐台進行服務器的重啟。
4.1 整體重啟
所謂集群整體重啟,就是先將整個集群停止,然后更新 ZooKeeper 的配置,然后再次啟動。如果在你的系統中,ZooKeeper 並不是個非常核心的組件,並且能夠允許短暫的服務停止(通常是幾秒鍾的時間間隔),那么不妨選擇這種方式。在整體重啟的過程中,所有該集群的客戶端都無法連接上集群。等到集群再次啟動,這些客戶端就能夠自動連接上——注意,整體啟動前建立起的客戶端會話,並不會因為此次整體重啟而失效。也就是說,在整體重啟期間花費的時間將不計入會話超時時間的計算中。
4.2 逐台重啟
這種方式更適合絕大多數的實際場景。在這種方式中,每次僅僅重啟集群中的一台機器,然后逐台對整個集群中的機器進行重啟操作。這種方式可以在重啟期間依然保證集群對外的正常服務。