看完這篇文章你就可以告訴領導你精通Zookeeper了

一、Zookeeper概述

1、概述

Zookeeper 是一個開源的為分散式框架提供協調服務的 Apache 項目。在分散式系統中,扮演註冊中心的角色。

Zookeeper數據模型的結構與Linux文件系統很像,整體上可以看做一棵樹,從根節點往下,每個節點稱為ZNode。每一個ZNode默認能夠存儲1MB的數據,每個ZNode都對應一個唯一的路徑,類似於Linux中的文件路徑。

image-20211129213010899.png

Zookeeper從設計模式角度來理解:是一個基於觀察者模式設計的分散式服務管理框架,它負責存儲和管理大家都關心的數據,然後接受觀察者的註冊,一旦這些數據的狀態發生變化,Zookeeper就 將負責通知已經在Zookeeper上註冊的那些觀察者,做出相應的反應。

image-20211129214320967.png

2、特點

(1)Zookeeper:一個領導者(Leader),多個跟隨者(Follower)組成的集群。

(2)集群中只要有半數以上節點存活,Zookeeper集群就能正常提供服務。所以Zookeeper集群適合安裝奇數台伺服器。

(3)全局數據一致:每個Server保存一份相同的數據副本,Client無論連接到哪個Server,數據都是一樣的。

(4)更新請求順序執行,來自同一個Client的更新請求按其發送順序依次執行。

(5)數據更新具有原子性,一次更新要麼成功要麼失敗。

(6)實時性,在一定時間範圍內,Client能讀到最新數據。

image-20211204150309944.png

3、應用場景

Zookeeper能提供的服務包括:統一命名服務、統一配置管理、統一集群管理、伺服器節點動態上下線、軟負載均衡等。
image-20211204151153948.png

(1)統一命名服務

在分散式環境下,經常需要對應用或者服務進行統一命名,便於識別。例如IP和域名的對應關係,一般我們都是輸入域名即可訪問對應網站,但其實內部使用DNS把域名解析成了對應的IP地址。
image-20211204152512415.png
(2)統一配置管理

在分散式系統中,一個集群里各個伺服器的配置文件在修改後經常需要進行同步,手動同步容易出錯並且耗時耗力,利用Zookeeper監聽的功能,可以很好的實現這個功能。

可以將共同的配置文件寫入Zookeeper的一個節點中,然後各個Client監聽這個節點,一旦節點中的內容發生變化時,Zookeeper就通知各個Client。
image-20211204153525895.png
(3)統一集群管理

分散式環境中,實時監控節點的狀態是很有必要的,這樣可以依據節點的狀態動態的做出一些調整。可以將節點資訊寫入Zookeeper的ZNode中,然後監聽這個ZNode就可以獲取它的實時狀態變化了。
image-20211204154306198.png
(4)伺服器動態上下線通知

這個第一小節就講了。
image-20211129214320967.png
(5)軟負載均衡

在Zookeeper中記錄每台伺服器的訪問數,這樣有新的客戶端請求到來時,可以讓訪問數最少的伺服器處理請求。
image-20211204154956362.png

二、Zookeeper安裝配置啟動

1、安裝

進入下載地址,選擇對應的版本下載
image-20211204155320214.png
下載後放到Linux某個目錄下解壓即可。

2、配置

解壓完畢後,來到zookeeper文件夾conf目錄下拷貝一份配置文件,命名為zoo.cfg
image-20211204160209840.png
將dataDir的目錄修改成自定義的目錄,因為/tmp目錄下的文件容易被清理掉。
image-20211204160405577.png
除了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、第一次啟動時的選舉機制

image-20211204200013593.png
(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

image-20211204202928346.png
一些比較常見的如下

(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給該節點名稱進行順序編號。
image-20211204205549310.png
創建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會通知客戶端。
image-20211204231617358.png
(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節點時
image-20211205162442914.png
當集群中半數以上節點完成write請求後,就開始開放給客戶端請求了。

(2)發送給Follow節點時
image-20211205162535496.png

當集群中半數以上節點完成write請求後,就開始開放給客戶端請求了。

七、Zookeeper實現分散式鎖

可以參考之前的文章:Zookeeper + Curator實現分散式鎖

文章首發於我的公眾號【禿頭哥編程】,歡迎大家關注。