深入瞭解 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,其字段有:

9zQ9N6

舉個例子,通過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 種類型:

同時權限種類也有五種:

同 Linux 中一樣,這個權限也有縮寫,舉個例子:

getAcl方法用戶查看對應的 ZNode 的權限,如圖,我們可以輸出的結果呈三段式。分別是:

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 可以在調用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 由兩部分組成:

這也是唯一 zxid 生成算法的底層實現,由於每個 Leader 所使用的 epoch 都是唯一的,而不同的消息在相同的 epoch 中,counter 的值是不同的,這樣一來所有的 proposal 在 Zookeeper 集羣中都有唯一的 zxid。

恢復模式

正常運行的 Zookeeper 集羣會處於廣播模式。相反,如果超過半數的節點宕機,就會進入恢復模式

什麼是恢復模式?

在 Zookeeper 集羣中,存在兩種模式,分別是:

當 Zookeeper 集羣故障時會進入恢復模式,也叫做 Leader Activation,顧名思義就是要在此階段選舉出 Leader。節點之間會生成 zxid 和 Proposal,然後相互投票。投票是要有原則的,主要有兩條:

如果在選舉的過程中發生異常,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 即可。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/mP5i0ko9Me0ek485OnqNUw