深入瞭解Zookeeper核心原理

detectiveHLH發表於2021-04-28

之前的文章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叢集中的節點劃分成了三個角色,分別是LeaderFollowerObserver,如下圖:

總的來說,這套架構和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詳細的原始碼解析,持續更新中。

如果你覺得這篇文章對你有幫助,還麻煩點個贊關個注分個享留個言

相關文章