一、Zookeeper概述
1、概述
Zookeeper 是一個開源的為分散式框架提供協調服務的 Apache 專案。在分散式系統中,扮演註冊中心的角色。
Zookeeper資料模型的結構與Linux檔案系統很像,整體上可以看做一棵樹,從根節點往下,每個節點稱為ZNode。每一個ZNode預設能夠儲存1MB的資料,每個ZNode都對應一個唯一的路徑,類似於Linux中的檔案路徑。
Zookeeper從設計模式角度來理解:是一個基於觀察者模式設計的分散式服務管理框架,它負責儲存和管理大家都關心的資料,然後接受觀察者的註冊,一旦這些資料的狀態發生變化,Zookeeper就 將負責通知已經在Zookeeper上註冊的那些觀察者,做出相應的反應。
2、特點
(1)Zookeeper:一個領導者(Leader),多個跟隨者(Follower)組成的叢集。
(2)叢集中只要有半數以上節點存活,Zookeeper叢集就能正常提供服務。所以Zookeeper叢集適合安裝奇數臺伺服器。
(3)全域性資料一致:每個Server儲存一份相同的資料副本,Client無論連線到哪個Server,資料都是一樣的。
(4)更新請求順序執行,來自同一個Client的更新請求按其傳送順序依次執行。
(5)資料更新具有原子性,一次更新要麼成功要麼失敗。
(6)實時性,在一定時間範圍內,Client能讀到最新資料。
3、應用場景
Zookeeper能提供的服務包括:統一命名服務、統一配置管理、統一叢集管理、伺服器節點動態上下線、軟負載均衡等。
(1)統一命名服務
在分散式環境下,經常需要對應用或者服務進行統一命名,便於識別。例如IP和域名的對應關係,一般我們都是輸入域名即可訪問對應網站,但其實內部使用DNS把域名解析成了對應的IP地址。
(2)統一配置管理
在分散式系統中,一個叢集裡各個伺服器的配置檔案在修改後經常需要進行同步,手動同步容易出錯並且耗時耗力,利用Zookeeper監聽的功能,可以很好的實現這個功能。
可以將共同的配置檔案寫入Zookeeper的一個節點中,然後各個Client監聽這個節點,一旦節點中的內容發生變化時,Zookeeper就通知各個Client。
(3)統一叢集管理
分散式環境中,實時監控節點的狀態是很有必要的,這樣可以依據節點的狀態動態的做出一些調整。可以將節點資訊寫入Zookeeper的ZNode中,然後監聽這個ZNode就可以獲取它的實時狀態變化了。
(4)伺服器動態上下線通知
這個第一小節就講了。
(5)軟負載均衡
在Zookeeper中記錄每臺伺服器的訪問數,這樣有新的客戶端請求到來時,可以讓訪問數最少的伺服器處理請求。
二、Zookeeper安裝配置啟動
1、安裝
進入下載地址,選擇對應的版本下載
下載後放到Linux某個目錄下解壓即可。
2、配置
解壓完畢後,來到zookeeper資料夾conf目錄下拷貝一份配置檔案,命名為zoo.cfg
將dataDir的目錄修改成自定義的目錄,因為/tmp目錄下的檔案容易被清理掉。
除了dataDir,上面還有幾個配置項如下:
(1)tickTime,預設2000ms。通訊心跳時間,即客戶端和伺服器或者伺服器和伺服器之間2s會傳送一次心跳,檢測機器的工作狀態是否正常。
(2)initLimit,預設10次。LF初始通訊時限,即叢集中的FL(Follow和Leader)伺服器之間初始連線時能容忍的最多心跳數(tickTime的數量)。
(3)syncLimit,預設5次。LF同步通訊時限,即叢集中的FL(Follow和Leader)伺服器之間一次請求和響應能容忍的最大心跳數。LF之間如果通訊時間超過syncLimit * tickTime,Leader則會認為Follow出故障了,將會從伺服器列表中刪除Follow。
(4)dataDir,Zookeeper資料存放路徑。
(5)clientPort,客戶端連線埠,預設2181。
3、啟動
# 可選引數
./zkServer.sh [--config <conf-dir>] {start|start-foreground|stop|restart|status|print-cmd}
# 啟動
./zkServer.sh start
# 檢視狀態
./zkServer.sh status
# 重啟
./zkServer.sh restart
# 停止
./zkServer.sh stop
# 啟動客戶端
./zkCli.sh
# 退出客戶端
[zk: localhost:2181(CONNECTED) 3] quit
三、Zookeeper叢集配置
因為Zookeeper叢集的規則是半數以上的伺服器可用則這個叢集可用,所以叢集中最少需要3臺以上的伺服器並且伺服器的數量最好是奇數。
為什麼是奇數,因為在相同容錯能力的情況下,奇數臺伺服器更節省資源。比如3臺伺服器的叢集,至少要2臺以上伺服器正常工作這個叢集才可用,也就是最多允許1臺伺服器當機;4臺伺服器的叢集,至少需要3臺以上伺服器正常工作這個叢集才可用,也就是最多允許1臺伺服器當機。3臺或者4臺伺服器的叢集,都至多允許一臺伺服器當機,容錯能力相同的情況下,4臺伺服器就浪費了一臺的數量。
另外一個原因是因為follow在選舉leader的時候,要求可用節點數量 > 總節點數量 / 2。如果叢集的節點數量為偶數臺,那麼就可能出現無法選舉出Leader的情況。具體參考這篇文章
簡單點,這裡我們的叢集中有3個節點,即3個Zookeeper伺服器。
1、解壓安裝
跟上面單機一樣,分別下載解壓到3臺伺服器的指定目錄。
2、配置伺服器編號
在伺服器的zookeeper資料夾的根目錄下建立zkDatas資料夾,並在zkDatas資料夾下建立一個myid的檔案,在檔案中新增server對應的編號,叢集中唯一。
cd /usr/local/apache-zookeeper-3.5.7
mkdir zkDatas
cd zkDatas
vi myid
## 伺服器1的myid檔案內容如下,對應的,伺服器2的myid內容為2,伺服器3的myid內容為3
1
3、配置zoo.cfg檔案
配置三個伺服器的zoo.cfg檔案內容如下
# 拷貝zoo_sample.cfg檔案
cp -r zoo_sample.cfg zoo.cfg
vi zoo.cfg
## zoo.cfg檔案內容如下
dataDir=/usr/local/apache-zookeeper-3.5.7/zkDatas
#######################cluster##########################
server.1=192.168.1.128:2888:3888
server.2=192.168.1.129:2888:3888
server.3=192.168.1.130:2888:3888
關於上面叢集的配置server.A=B:C:D
解釋如下
- A是一個數字,表示第幾號伺服器。之前我們配置了myid檔案,A的值就是myid檔案中的值。zookeeper在啟動的時候會讀取此檔案,拿到裡面的檔案內容後再和zoo.cfg檔案裡的配置進行比較,判斷是哪個伺服器。
- B代表伺服器的地址。
- C是Leader和Follow交換資訊的埠。
- D是當叢集中的Leader伺服器歇逼的時候,重新選舉時的通訊埠。
4、啟動各個伺服器
啟動完後,就會告訴你誰是leader誰是follow。
[root@localhost apache-zookeeper-3.5.7]# bin/zkServer.sh status
JMX enabled by default
Using config: /usr/local/apache-zookeeper-3.5.7/bin/../conf/zoo.cfg
Mode: follower
[root@localhost apache-zookeeper-3.5.7]# bin/zkServer.sh status
JMX enabled by default
Using config: /usr/local/apache-zookeeper-3.5.7/bin/../conf/zoo.cfg
Mode: leader
[root@localhost apache-zookeeper-3.5.7]# bin/zkServer.sh status
JMX enabled by default
Using config: /usr/local/apache-zookeeper-3.5.7/bin/../conf/zoo.cfg
Mode: follower
四、Zookeeper的選舉機制
單機模式下不涉及選舉,只有在叢集模式下才會進行選舉。Leader選舉一般發生在下面兩種情況:第一次啟動叢集的時候和叢集執行過程中Leader掛了。這兩種情況下Leader的選舉機制時不同的,需要分開討論。
在學習選舉機制之前,需要先學習幾個概念。
- SID:伺服器ID,即myid檔案中的值,用來唯一標識一臺Zookeeper叢集中的機器,不能重複。
- ZXID:zookeeper transaction id,即zookeeper事務id,用來標識一次伺服器狀態的變更。在某一時刻,叢集中每臺機器的zxid大概率是不同的。
- Epoch:邏輯時鐘,也叫投票的次數,每投完一次票這個值就會增加,同一輪投票過程中邏輯時鐘的值是相同的。
1、第一次啟動時的選舉機制
(1)server1啟動後,發起一次選舉。server1投自己一票,此時server1共有1票,不夠半數以上(3票),無法產生Leader,server1狀態保持LOOKING。
(2)server2啟動,發起一次選舉。server1和server2各投自己一票並交換選票資訊,此時server1發現server2的SID比自己目前投票的(也就是它自身)更大,於是更改投票為server2。此時server1共有0票,server2共有2票,不夠半數以上,無法產生leader,server1和server2狀態保持LOOKING。
(3)server3啟動,發起一次選舉。和(2)一樣,server1和server2最終都會把票投給server3,那麼此時server1和server2的票數就為0,而server3的票數為3,超過了半數,server3變成leader。server1和server2更改狀態為FOLLOWING,server3更改狀態為LEADING。
(4)server4啟動,發起一次選舉。此時server1和server2都不是LOOKING狀態,所以不會更改選票資訊,所以最終server4少數服從多數,把選票投給server3,此時server3總票數為4,其他三臺伺服器總票數都為0。
(5)server4啟動,發起一次選舉。和(4)一樣的流程。
2、非第一次啟動時的選舉機制
當叢集中出現下面的情況時,會觸發leader選舉機制
(1)叢集執行過程中,有新的伺服器節點加入。
(2)follow伺服器無法和leader伺服器通訊時,follow會認為leader掛了。
當觸發選舉機制時,叢集可能存在兩種情況
(1)叢集中此時已經存在leader。那麼在這種情況下,只需要告訴發起選舉的伺服器有關leader的相關資訊,讓該伺服器和leader建立連線並進行狀態同步即可。
(2)叢集中此時不存在leader。
還是用上一小節的圖來說,叢集中5臺server,SID分別是1,2,3,4,5,ZXID分別是8,8,8,7,7,server3是leader。此時server3和server5掛了,follow伺服器都會把自己的狀態變成LOOKING,開始進行leader選舉。
這裡就需要用到上面說過的三個概念:Epoch、SID和ZXID。選舉Leader規則:
- Epoch大的直接勝出
- Epoch相同,ZXID大的勝出
- ZXID相同,SID大的勝出
對於剩下的server1、server2和server4,它們的(Epoch, ZXID, SID)的值分別是(1, 8, 1)、(1, 8, 2)、(1, 7, 4)。所以最後的leader就是server2。
五、客戶端相關的命令
# 啟動客戶端
./zkCli.sh
# help命令,可以顯式所有的操作命令
[zk: localhost:2181(CONNECTED) 0] help
一些比較常見的如下
(1)ls [-s] [-w] [-R] path
,檢視對應path下的子節點,-w
表示監聽子節點變化,-s
附加次級資訊。
(2)create path
,在對應path下建立一個節點,-s
表示節點帶序號,-e
表示建立臨時節點,重啟後或者超時就會被刪除。
(3)get path
,獲得節點的值(可監聽)。-w
表示監聽節點內容變化,-s
表示附加次級資訊。
(4)set path data
,設定節點的具體值。
(5)stat path
,檢視節點狀態。
(6)delete path
,刪除節點。
(7)deleteall path
,遞迴刪除節點。
1、檢視ZNode節點資訊
[zk: localhost:2181(CONNECTED) 4] ls /
[zookeeper]
[zk: localhost:2181(CONNECTED) 5] ls -s /
[zookeeper]cZxid = 0x0
ctime = Thu Jan 01 08:00:00 CST 1970
mZxid = 0x0
mtime = Thu Jan 01 08:00:00 CST 1970
pZxid = 0x0
cversion = -1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1
ls -s path
可以檢視節點的詳細資料。
(1)cZxid:建立節點的事務id
每次修改 ZooKeeper 狀態都會產生一個 ZooKeeper 事務 ID。事務 ID 是 ZooKeeper 中所有修改總的次序。每次修改都有唯一的 zxid,如果 zxid1 小於 zxid2,那麼 zxid1 在 zxid2 之前發生。
(2)ctime:znode 被建立的毫秒數(從 1970 年開始)
(3)mZxid:znode 最後更新的事務 zxid
(4)mtime:znode 最後修改的毫秒數(從 1970 年開始)
(5)pZxid:znode 最後更新的子節點 zxid
(6)cversion:znode 子節點變化號,znode 子節點修改次數
(7)dataVersion:znode 資料變化號
(8)aclVersion:znode 訪問控制列表的變化號
(9)ephemeralOwner:如果是臨時節點,這個是 znode 擁有者的 session id。如果不是臨時節點則是 0
(10)dataLength:znode 的資料長度
(11)numChildren:znode 子節點數量
2、建立ZNode節點
Zookeeper中節點型別可以大致分為兩類持久化(Persistent)節點和臨時(Ephemeral)節點,又可以細分為四類:
(1)持久化目錄節點
客戶端與Zookeeper斷開連線後,該節點依舊存在。
(2)持久化順序編號目錄節點
客戶端與Zookeeper斷開連線後,該節點依舊存在,只是Zookeeper給該節點名稱進行順序編號。
(3)臨時目錄節點
客戶端與Zookeeper斷開連線後,該節點被刪除。
(4)臨時順序編號目錄節點
客戶端與 Zookeeper 斷開連線後,該節點被刪除,只是Zookeeper給該節點名稱進行順序編號。
建立znode時設定順序標識,znode名稱後會附加一個值,順序號是一個單調遞增的計數器,由父節點維護。
注意:在分散式系統中,順序號可以被用於為所有的事件進行全域性排序,這樣客戶端可以通過順序號推斷事件的順序。
2.1 建立不帶序號的永久節點
[zk: localhost:2181(CONNECTED) 14] create /znode1 "value1"
Created /znode1
[zk: localhost:2181(CONNECTED) 15] create /znode2 "value2"
Created /znode2
# 獲得節點的值
[zk: localhost:2181(CONNECTED) 16] get -s /znode1
value1
cZxid = 0x6
ctime = Sat Dec 04 18:43:50 CST 2021
mZxid = 0x6
mtime = Sat Dec 04 18:43:50 CST 2021
pZxid = 0x6
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 6
numChildren = 0
2.2 建立帶序號的永久節點
[zk: localhost:2181(CONNECTED) 17] create -s /znode1/znode3 "value3"
Created /znode1/znode30000000000
[zk: localhost:2181(CONNECTED) 18] create -s /znode1/znode4 "value4"
Created /znode1/znode40000000001
[zk: localhost:2181(CONNECTED) 19] create -s /znode5 "value5"
Created /znode50000000003
[zk: localhost:2181(CONNECTED) 20] create -s /znode1/znode6 "value6"
Created /znode1/znode60000000002
可以發現如果原來沒有序號節點,序號從 0 開始依次遞增。如果原節點下已有 2 個節點,則再排序時從 2 開始,以此類推。
2.3 建立臨時節點
[zk: localhost:2181(CONNECTED) 21] create -e /znode2/znode7 "value7"
Created /znode2/znode7
[zk: localhost:2181(CONNECTED) 22] create -e -s /znode2/znode8 "value8"
Created /znode2/znode80000000001
[zk: localhost:2181(CONNECTED) 23] ls /znode2
[znode7, znode80000000001]
# 重啟客戶端資料消失
2.4 修改節點的值
[zk: localhost:2181(CONNECTED) 24] set /znode2/znode7 "val"
[zk: localhost:2181(CONNECTED) 25] get /znode2/znode7
val
3、監聽節點
3.1 監聽器原理
客戶端註冊監聽它關心的目錄節點,當目錄節點發生變化時(資料改變、節點刪除、子目錄節點增加刪除),Zookeeper會通知客戶端。
(1)首先要有一個main()執行緒。
(2)在main執行緒中建立zookeeper客戶端,這時就會建立兩個執行緒,一個負責網路連線通訊(connect),一個負責監聽(listener)。
(3)通過connect執行緒將註冊的監聽事件傳送給zookeeper。
(4)將監聽事件新增到zookeeper的註冊監聽列表中。
(5)zookeeper監聽到有資料或路徑變化時,就會通知listener執行緒。
(6)listener執行緒內部呼叫process方法。
3.2 常見的監聽
(1)監聽節點資料的變化:get -w path
(2)監聽子節點增減的變化:ls -w path
注意:上面兩種監聽都是註冊一次生效一次。如果想要多次生效,那麼就要註冊多次。
4、刪除和檢視節點
# 檢視/znode2下的子節點
[zk: localhost:2181(CONNECTED) 1] ls /znode2
[znode7, znode80000000001]
# 刪除/znode2節點,因為下面存在子節點,所以不能刪除
[zk: localhost:2181(CONNECTED) 2] delete /znode2
Node not empty: /znode2
# 刪除/znode2/znode80000000001節點
[zk: localhost:2181(CONNECTED) 3] delete /znode2/znode80000000001
[zk: localhost:2181(CONNECTED) 4] ls /znode2
[znode7]
[zk: localhost:2181(CONNECTED) 5] delete /znode2
Node not empty: /znode2
# 刪除/znode2及其下面所有子節點
[zk: localhost:2181(CONNECTED) 6] deleteall /znode2
[zk: localhost:2181(CONNECTED) 7] ls /znode2
Node does not exist: /znode2
# 檢視節點狀態
[zk: localhost:2181(CONNECTED) 9] stat /znode1
cZxid = 0x6
ctime = Sat Dec 04 18:43:50 CST 2021
mZxid = 0x6
mtime = Sat Dec 04 18:43:50 CST 2021
pZxid = 0xb
cversion = 3
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 6
numChildren = 3
六、客戶端相關的API
除了在啟動zkCli.sh在命令列中使用相關的命令,也可以使用程式語言操作zookeeper相關的api。
首先建立一個Java的maven專案,在pom.xml檔案中引入zookeeper相關的依賴
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.5</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
zookeeper相關的api可以參考官方文件:點選連結
為了便於測試,可以引入junit的依賴
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
</dependency>
client寫資料的時候,寫請求可能是傳送給Leader也可能是Follow。假設叢集中有3臺伺服器,1臺Leader,2臺Follow
(1)傳送給Leader節點時
當叢集中半數以上節點完成write請求後,就開始開放給客戶端請求了。
(2)傳送給Follow節點時
當叢集中半數以上節點完成write請求後,就開始開放給客戶端請求了。
七、Zookeeper實現分散式鎖
可以參考之前的文章:Zookeeper + Curator實現分散式鎖
文章首發於我的公眾號【禿頭哥程式設計】,歡迎大家關注。