深入了解Zookeeper核心原理


之前的文章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詳細的源碼解析,持續更新中。

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM