之前的文章Zookeeper基礎原理&應用場景詳解中將Zookeeper的基本原理及其應用場景做了一個詳細的介紹,雖然介紹了其底層的存儲原理、如何使用Zookeeper來實現分布式鎖。但是我認為這樣也僅僅只是了解了Zookeeper的一點皮毛而已。所以這篇文章就給大家詳細聊聊Zookeeper的核心底層原理。不太熟悉Zookeeper的可以回過頭去看看。
ZNode
這個應該算是Zookeeper中的基礎,數據存儲的最小單元。在Zookeeper中,類似文件系統的存儲結構,被Zookeeper抽象成了樹,樹中的每一個節點(Node)被叫做ZNode。ZNode中維護了一個數據結構,用於記錄ZNode中數據更改的版本號以及ACL(Access Control List)的變更。
有了這些數據的版本號以及其更新的Timestamp,Zookeeper就可以驗證客戶端請求的緩存是否合法,並協調更新。
而且,當Zookeeper的客戶端執行更新或者刪除操作時,都必須要帶上要修改的對應數據的版本號。如果Zookeeper檢測到對應的版本號不存在,則不會執行這次更新。如果合法,在ZNode中數據更新之后,其對應的版本號也會一起更新。
這套版本號的邏輯,其實很多框架都在用,例如RocketMQ中,Broker向NameServer注冊的時候,也會帶上這樣一個版本號,叫
DateVersion
。
接下來我們來詳細看一下這個維護版本號相關數據的數據結構,它叫Stat Structure
,其字段有:
字段 | 釋義 |
---|---|
czxid | 創建該節點的zxid |
mzxid | 最后一次修改該節點的zxid |
pzxid | 最后一次修改該節點的子節點的zxid |
ctime | 從當前epoch開始到該節點被創建,所間隔的毫秒 |
mtime | 從當前epoch開始到該節點最后一次被編輯,所間隔的毫秒 |
version | 當前節點的改動次數(也就是版本號) |
cversion | 當前節點的子節點的改動次數 |
aversion | 當前節點的ACL改動次數 |
ephemeralOwner | 當前臨時節點owner的SessionID(如果不是臨時節點則為空) |
dataLength | 當前節點的數據的長度 |
numChildren | 當前節點的子節點數量 |
舉個例子,通過stat
命令,我們可以查看某個ZNode中Stat Structure具體的值。

關於這里的epoch、zxid是Zookeeper集群相關的東西,后面會詳細的對其進行介紹。
ACL
ACL(Access Control List)用於控制ZNode的相關權限,其權限控制和Linux中的類似。Linux中權限種類分為了三種,分別是讀、寫、執行,分別對應的字母是r、w、x。其權限粒度也分為三種,分別是擁有者權限、群組權限、其他組權限,舉個例子:
drwxr-xr-x 3 USERNAME GROUP 1.0K 3 15 18:19 dir_name
什么叫粒度?粒度是對權限所作用的對象的分類,把上面三種粒度換個說法描述就是**對用戶(Owner)、用戶所屬的組(Group)、其他組(Other)**的權限划分,這應該算是一種權限控制的標准了,典型的三段式。
Zookeeper中雖然也是三段式,但是兩者對粒度的划分存在區別。Zookeeper中的三段式為Scheme、ID、Permissions,含義分別為權限機制、允許訪問的用戶和具體的權限。

Scheme代表了一種權限模式,有以下5種類型:
-
world 在此中Scheme下, ID
只能是anyone
,代表所有人都可以訪問 -
auth 代表已經通過了認證的用戶 -
digest 使用用戶名+密碼來做校驗。 -
ip 只允許某些特定的IP訪問ZNode -
X509 通過客戶端的證書進行認證
同時權限種類也有五種:
-
CREATE 創建節點 -
READ 獲取節點或列出其子節點 -
WRITE 能設置節點的數據 -
DELETE 能夠刪除子節點 -
ADMIN 能夠設置權限
同Linux中一樣,這個權限也有縮寫,舉個例子:

getAcl
方法用戶查看對應的ZNode的權限,如圖,我們可以輸出的結果呈三段式。分別是:
-
scheme 使用了world -
id 值為 anyone
,代表所有用戶都有權限 -
permissions 其具體的權限為cdrwa,分別是 CREATE、 DELETE、 READ、 WRITE和 ADMIN的縮寫
Session機制
了解了Zookeeper的Version機制,我們可以繼續探索Zookeeper的Session機制了。
我們知道,Zookeeper中有4種類型的節點,分別是持久節點、持久順序節點、臨時節點和臨時順序節點。
在之前的文章我們聊到過,客戶端如果創建了臨時節點,並在之后斷開了連接,那么所有的臨時節點就都會被刪除。實際上斷開連接的說話不是很精確,應該是說客戶端建立連接時的Session過期之后,其創建的所有臨時節點就會被全部刪除。
那么Zookeeper是怎么知道哪些臨時節點是由當前客戶端創建的呢?
答案是Stat Structure中的**ephemeralOwner(臨時節點的Owner)**字段
上面說過,如果當前是臨時順序節點,那么ephemeralOwner
則存儲了創建該節點的Owner的SessionID,有了SessionID,自然就能和對應的客戶端匹配上,當Session失效之后,才能將該客戶端創建的所有臨時節點全部刪除。

對應的服務在創建連接的時候,必須要提供一個帶有所有服務器、端口的字符串,單個之間逗號相隔,舉個例子。
127.0.0.1:3000:2181,127.0.0.1:2888,127.0.0.1:3888
Zookeeper的客戶端收到這個字符串之后,會從中隨機選一個服務、端口來建立連接。如果連接在之后斷開,客戶端會從字符串中選擇下一個服務器,繼續嘗試連接,直到連接成功。
除了這種最基本的IP+端口,在Zookeeper的3.2.0之后的版本中還支持連接串中帶上路徑,舉個例子。
127.0.0.1:3000:2181,127.0.0.1:2888,127.0.0.1:3888/app/a
這樣一來,/app/a
就會被當成當前服務的根目錄,在其下創建的所有的節點路經都會帶上前綴/app/a
。舉個例子,我創建了一個節點/node_name
,那其完整的路徑就會為/app/a/node_name
。這個特性特別適用於多租戶的環境,對於每個租戶來說,都認為自己是最頂層的根目錄/
。
當Zookeeper的客戶端和服務器都建立了連接之后,客戶端會拿到一個64位的SessionID和密碼。這個密碼是干什么用的呢?我們知道Zookeeper可以部署多個實例,如果客戶端斷開了連接又和另外的Zookeeper服務器建立了連接,那么在建立連接使就會帶上這個密碼。該密碼是Zookeeper的一種安全措施,所有的Zookeeper節點都可以對其進行驗證。這樣一來,即使連接到了其他Zookeeper節點,Session同樣有效。
Session過期有兩種情況,分別是:
-
過了指定的失效時間 -
指定時間內客戶端沒有發送心跳
對於第一種情況,過期時間會在Zookeeper客戶端建立連接的時候傳給服務器,這個過期時間的范圍目前只能在2倍tickTime
和20倍tickTime
之間。
ticktime是Zookeeper服務器的配置項,用於指定客戶端向服務器發送心跳的間隔,其默認值為
tickTime=2000
,單位為毫秒
而這套Session的過期邏輯由Zookeeper的服務器維護,一旦Session過期,服務器會立即刪除由Client創建的所有臨時節點,然后通知所有正在監聽這些節點的客戶端相關變更。
對於第二種情況,Zookeeper中的心跳是通過PING請求來實現的,每隔一段時間,客戶端都會發送PING請求到服務器,這就是心跳的本質。心跳使服務器感知到客戶端還活着,同樣的讓客戶端也感知到和服務器的連接仍然是有效的,這個間隔就是**tickTime
**,默認為2秒。
Watch機制
了解完ZNode和Session,我們終於可以來繼續下一個關鍵功能Watch了,在上面的內容中也不止一次的提到**監聽(Watch)**這個詞。首先用一句話來概括其作用
給某個節點注冊監聽器,該節點一旦發生變更(例如更新或者刪除),監聽者就會收到一個Watch Event
和ZNode中有多種類型一樣,Watch也有多種類型,分別是一次性Watch和永久性Watch。
-
一次性Watch 在被觸發之后,該Watch就會移除 -
永久性Watch 在被觸發之后,仍然保留,可以繼續監聽ZNode上的變更,是Zookeeper 3.6.0版本新增的功能
一次性的Watch可以在調用getData()
、getChildren()
和exists()
等方法時在參數中進行設置,永久性的Watch則需要調用addWatch()
來實現。
並且一次性的Watch會存在問題,因為在Watch觸發的事件到達客戶端、再到客戶端設立新的Watch,是有一個時間間隔的。而如果在這個時間間隔中發生的變更,客戶端則無法感知。

Zookeeper集群架構
ZAB協議
把前面的都鋪墊好之后就可以來從整體架構的角度再深入了解Zookeeper。Zookeeper為了保證其高可用,采用的基於主從的讀寫分離架構。
我們知道在類似的Redis主從架構中,節點之間是采用的Gossip協議來進行通信的,那么在Zookeeper中通信協議是什么?
答案是**ZAB(Zookeeper Atomic Broadcast)**協議。
ZAB協議是一種支持崩潰恢復的的原子廣播協議,用於在Zookeeper之間傳遞消息,使所有的節點都保持同步。ZAB同時具有高性能、高可用的、容易上手、利於維護的特點,同時支持自動的故障恢復。
ZAB協議將Zookeeper集群中的節點划分成了三個角色,分別是Leader、Follower和Observer,如下圖:

總的來說,這套架構和Redis主從或者MySQL主從的架構類似(感興趣的也可以去看之前的寫的文章,都有聊過)
不同點在於,通常的主從架構中存在兩種角色,分別是Leader、Follower(或者是Master、Slave),但Zookeeper中多了一個Observer。
那問題來了,Observer和Follower的區別是啥呢?
本質上來說兩者的功能是一樣的, 都為Zookeeper提供了橫向擴展的能力,使其能夠扛住更多的並發。但區別在於Leader的選舉過程中,Observer不參與投票選舉。
順序一致性
上文提到了Zookeeper集群中是讀寫分離的,只有Leader節點能處理寫請求,如果Follower節點接收到了寫請求,會將該請求轉發給Leader節點處理,Follower節點自身是不會處理寫請求的。
Leader節點接收到消息之后,會按照請求的嚴格順序一一的進行處理。這是Zookeeper的一大特點,它會保證消息的順序一致性。
舉個例子,如果消息A比消息B先到,那么在所有的Zookeeper節點中,消息A都會先於消息B到達,Zookeeper會保證消息的全局順序。
zxid
那Zookeeper是如何保證消息的順序?答案是通過zxid
。
可以簡單的把zxid理解成Zookeeper中消息的唯一ID,節點之間會通過發送**Proposal(事務提議)**來進行通信、數據同步,proposal中就會帶上zxid和具體的數據(Message)。而zxid由兩部分組成:

-
epoch 可以理解成朝代,或者說Leader迭代的版本,每個Leader的epoch都不一樣 -
counter 計數器,來一條消息就會自增
這也是唯一zxid生成算法的底層實現,由於每個Leader所使用的epoch都是唯一的,而不同的消息在相同的epoch中,counter的值是不同的,這樣一來所有的proposal在Zookeeper集群中都有唯一的zxid。
恢復模式
正常運行的Zookeeper集群會處於廣播模式。相反,如果超過半數的節點宕機,就會進入恢復模式。
什么是恢復模式?
在Zookeeper集群中,存在兩種模式,分別是:
-
恢復模式 -
廣播模式
當Zookeeper集群故障時會進入恢復模式,也叫做Leader Activation,顧名思義就是要在此階段選舉出Leader。節點之間會生成zxid和Proposal,然后相互投票。投票是要有原則的,主要有兩條:
-
選舉出來的Leader的zxid一定要是所有的Follower中最大的 -
並且已有超過半數的Follower返回了ACK,表示認可選舉出來的Leader
如果在選舉的過程中發生異常,Zookeeper會直接進行新一輪的選舉。如果一切順利,Leader就會被成功選舉出來,但是此時集群還不能正常對外提供服務,因為新的Leader和Follower之間還沒有進行關鍵的數據同步。
此后,Leader會等待其余的Follower來連接,然后通過Proposal向所有的Follower發送其缺失的數據。
至於怎么知道缺失哪些數據,Proposal本身是要記錄日志,通過Proposal中的zxid的低32位的Counter中的值,就可以做一個Diff
當然這里有個優化,如果缺失的數據太多,那么一條一條的發送Proposal效率太低。所以如果Leader發現缺失的數據過多就會將當前的數據打個快照,直接打包發送給Follower。
新選舉出來的Leader的Epoch,會在原來的值上+1,並且將Counter重置為0。
到這你是不是以為就完了?實際上到這還是無法正常提供服務
數據同步完成之后,Leader會發送一個NEW_LEADER的Proposal給Follower,當且僅當該Proposal被過半的Follower返回Ack之后,Leader才會Commit該NEW_LEADER Proposal,集群才能正常的進行工作。
至此,恢復模式結束,集群進入廣播模式。
廣播模式
在廣播模式下,Leader接收到消息之后,會向其他所有Follower發送Proposal(事務提議),Follower接收到Proposal之后會返回ACK給Leader。當Leader收到了quorums個ACK之后,當前Proposal就會提交,被應用到節點的內存中去。quorum個是多少呢?
Zookeeper官方建議每2個Zookeeper節點中,至少有一個需要返回ACK才行,假設有N個Zookeeper節點,那計算公式應該是n/2 + 1
。
這樣可能不是很直觀,用大白話來說就是,超過半數的Follower返回了ACK,該Proposal就能夠提交,並且應用至內存中的ZNode。

Zookeeper使用2PC來保證節點之間的數據一致性(如上圖),但是由於Leader需要跟所有的Follower交互,這樣一來通信的開銷會變得較大,Zookeeper的性能就會下降。所以為了提升Zookeeper的性能,才從所有的Follower節點返回ACK變成了過半的Follower返回ACK即可。
好了以上就是本篇博客的全部內容了,歡迎微信搜索關注【SH的全棧筆記】,回復【隊列】獲取MQ學習資料,包含基礎概念解析和RocketMQ詳細的源碼解析,持續更新中。
如果你覺得這篇文章對你有幫助,還麻煩點個贊,關個注,分個享,留個言。
