一文掌握 ZooKeeper 的核心知識

ZooKeeper 是一個分佈式協調服務 ,由 Apache 進行維護。

ZooKeeper 可以視爲一個高可用的文件系統。

ZooKeeper 可以用於發佈 / 訂閱、負載均衡、命令服務、分佈式協調 / 通知、集羣管理、Master 選舉、分佈式鎖和分佈式隊列等功能 。

一、ZooKeeper 簡介

1.1 ZooKeeper 是什麼

ZooKeeper 是 Apache 的頂級項目。ZooKeeper 爲分佈式應用提供了高效且可靠的分佈式協調服務,提供了諸如統一命名服務、配置管理和分佈式鎖等分佈式的基礎服務。在解決分佈式數據一致性方面,ZooKeeper 並沒有直接採用 Paxos 算法,而是採用了名爲 ZAB 的一致性協議。

ZooKeeper 主要用來解決分佈式集羣中應用系統的一致性問題,它能提供基於類似於文件系統的目錄節點樹方式的數據存儲。但是 ZooKeeper 並不是用來專門存儲數據的,它的作用主要是用來維護和監控存儲數據的狀態變化。通過監控這些數據狀態的變化,從而可以達到基於數據的集羣管理。

很多大名鼎鼎的框架都基於 ZooKeeper 來實現分佈式高可用,如:Dubbo、Kafka 等。

1.2 ZooKeeper 的特性

ZooKeeper 具有以下特性:

1.3 ZooKeeper 的設計目標

二、ZooKeeper 核心概念

2.1  數據模型

ZooKeeper 的數據模型是一個樹形結構的文件系統。

樹中的節點被稱爲 znode,其中根節點爲 /,每個節點上都會保存自己的數據和節點信息。znode 可以用於存儲數據,並且有一個與之相關聯的 ACL(詳情可見 ACL)。ZooKeeper 的設計目標是實現協調服務,而不是真的作爲一個文件存儲,因此 znode 存儲數據的大小被限制在 1MB 以內。

圖片

**ZooKeeper 的數據訪問具有原子性。**其讀寫操作都是要麼全部成功,要麼全部失敗。

znode 通過路徑被引用。znode 節點路徑必須是絕對路徑。

znode 有兩種類型:

2.2  節點信息

znode 上有一個順序標誌( SEQUENTIAL )。如果在創建 znode 時,設置了順序標誌( SEQUENTIAL ),那麼 ZooKeeper 會使用計數器爲 znode 添加一個單調遞增的數值,即 zxid。ZooKeeper 正是利用 zxid 實現了嚴格的順序訪問控制能力。

每個 znode 節點在存儲數據的同時,都會維護一個叫做 Stat 的數據結構,裏面存儲了關於該節點的全部狀態信息。如下:

圖片

2.3 集羣角色

Zookeeper 集羣是一個基於主從複製的高可用集羣,每個服務器承擔如下三種角色中的一種。

2.4 ACL

ZooKeeper 採用 ACL(Access Control Lists)策略來進行權限控制。

每個 znode 創建時都會帶有一個 ACL 列表,用於決定誰可以對它執行何種操作。

ACL 依賴於 ZooKeeper 的客戶端認證機制。ZooKeeper 提供了以下幾種認證方式:

ZooKeeper 定義瞭如下五種權限:

三、ZooKeeper 工作原理

3.1 讀操作

Leader/Follower/Observer 都可直接處理讀請求,從本地內存中讀取數據並返回給客戶端即可。

由於處理讀請求不需要服務器之間的交互,Follower/Observer 越多,整體系統的讀請求吞吐量越大,也即讀性能越好。

圖片

3.2 寫操作

所有的寫請求實際上都要交給 Leader 處理。Leader 將寫請求以事務形式發給所有 Follower 並等待 ACK,一旦收到半數以上 Follower 的 ACK,即認爲寫操作成功。

3.2.1 寫 Leader

圖片

由上圖可見,通過 Leader 進行寫操作,主要分爲五步:

  1. 客戶端向 Leader 發起寫請求。

  2. Leader 將寫請求以事務 Proposal 的形式發給所有 Follower 並等待 ACK。

  3. Follower 收到 Leader 的事務 Proposal 後返回 ACK。

  4. Leader 得到過半數的 ACK(Leader 對自己默認有一個 ACK)後向所有的 Follower 和 Observer 發送 Commmit。

  5. Leader 將處理結果返回給客戶端。

注意

3.2.2 寫 Follower/Observer

圖片

Follower/Observer 均可接受寫請求,但不能直接處理,而需要將寫請求轉發給 Leader 處理。

除了多了一步請求轉發,其它流程與直接寫 Leader 無任何區別。

3.3 事務

對於來自客戶端的每個更新請求,ZooKeeper 具備嚴格的順序訪問控制能力。

爲了保證事務的順序一致性,ZooKeeper 採用了遞增的事務 id 號(zxid)來標識事務。

**Leader 服務會爲每一個 Follower 服務器分配一個單獨的隊列,然後將事務 Proposal 依次放入隊列中,並根據 FIFO(先進先出) 的策略進行消息發送。**Follower 服務在接收到 Proposal 後,會將其以事務日誌的形式寫入本地磁盤中,並在寫入成功後反饋給 Leader 一個 Ack 響應。**當 Leader 接收到超過半數 Follower 的 Ack 響應後,就會廣播一個 Commit 消息給所有的 Follower 以通知其進行事務提交,**之後 Leader 自身也會完成對事務的提交。而每一個 Follower 則在接收到 Commit 消息後,完成事務的提交。

所有的提議(proposal)都在被提出的時候加上了 zxid。zxid 是一個 64 位的數字,它的高 32 位是 epoch 用來標識 Leader 關係是否改變,每次一個 Leader 被選出來,它都會有一個新的 epoch,標識當前屬於那個 leader 的統治時期。低 32 位用於遞增計數。

詳細過程如下:

3.4 觀察

客戶端註冊監聽它關心的 znode,當 znode 狀態發生變化(數據變化、子節點增減變化)時,ZooKeeper 服務會通知客戶端。

客戶端和服務端保持連接一般有兩種形式:

Zookeeper 的選擇是服務端主動推送狀態,也就是觀察機制( Watch )。

ZooKeeper 的觀察機制允許用戶在指定節點上針對感興趣的事件註冊監聽,當事件發生時,監聽器會被觸發,並將事件信息推送到客戶端。

客戶端使用 getData 等接口獲取 znode 狀態時傳入了一個用於處理節點變更的回調,那麼服務端就會主動向客戶端推送節點的變更:

從這個方法中傳入的 Watcher 對象實現了相應的 process 方法,每次對應節點出現了狀態的改變,WatchManager 都會通過以下的方式調用傳入 Watcher 的方法:

Set<Watcher> triggerWatch(String path, EventType type, Set<Watcher> supress) {
    WatchedEvent e = new WatchedEvent(type, KeeperState.SyncConnected, path);
    Set<Watcher> watchers;
    synchronized (this) {
        watchers = watchTable.remove(path);
    }
    for (Watcher w : watchers) {
        w.process(e);
    }
    return

Zookeeper 中的所有數據其實都是由一個名爲 DataTree 的數據結構管理的,所有的讀寫數據的請求最終都會改變這顆樹的內容,在發出讀請求時可能會傳入 Watcher 註冊一個回調函數,而寫請求就可能會觸發相應的回調,由 WatchManager 通知客戶端數據的變化。

通知機制的實現其實還是比較簡單的,通過讀請求設置 Watcher 監聽事件,寫請求在觸發事件時就能將通知發送給指定的客戶端。

3.5 會話

ZooKeeper 客戶端通過 TCP 長連接連接到 ZooKeeper 服務集羣。會話 (Session) 從第一次連接開始就已經建立,之後通過心跳檢測機制來保持有效的會話狀態。通過這個連接,客戶端可以發送請求並接收響應,同時也可以接收到 Watch 事件的通知。

每個 ZooKeeper 客戶端配置中都配置了 ZooKeeper 服務器集羣列表。啓動時,客戶端會遍歷列表去嘗試建立連接。如果失敗,它會嘗試連接下一個服務器,依次類推。

一旦一臺客戶端與一臺服務器建立連接,這臺服務器會爲這個客戶端創建一個新的會話。**每個會話都會有一個超時時間,若服務器在超時時間內沒有收到任何請求,則相應會話被視爲過期。**一旦會話過期,就無法再重新打開,且任何與該會話相關的臨時 znode 都會被刪除。

通常來說,會話應該長期存在,而這需要由客戶端來保證。客戶端可以通過心跳方式(ping)來保持會話不過期。

圖片

ZooKeeper 的會話具有四個屬性:

Zookeeper 的會話管理主要是通過 SessionTracker 來負責,其採用了分桶策略(將類似的會話放在同一區塊中進行管理)進行管理,以便 Zookeeper 對會話進行不同區塊的隔離處理以及同一區塊的統一處理。

四、ZAB 協議

ZooKeeper 並沒有直接採用 Paxos 算法,而是採用了名爲 ZAB 的一致性協議。ZAB 協議不是 Paxos 算法,只是比較類似,二者在操作上並不相同。

ZAB 協議是 Zookeeper 專門設計的一種支持崩潰恢復的原子廣播協議。

ZAB 協議是 ZooKeeper 的數據一致性和高可用解決方案。

ZAB 協議定義了兩個可以無限循環的流程:

4.1 選舉 Leader

ZooKeeper 的故障恢復

ZooKeeper 集羣採用一主(稱爲 Leader)多從(稱爲 Follower)模式,主從節點通過副本機制保證數據一致。

ZAB 協議的選舉 Leader 機制簡單來說,就是:基於過半選舉機制產生新的 Leader,之後其他機器將從新的 Leader 上同步狀態,當有過半機器完成狀態同步後,就退出選舉 Leader 模式,進入原子廣播模式。

4.1.1 術語

**myid:**每個 Zookeeper 服務器,都需要在數據文件夾下創建一個名爲 myid 的文件,該文件包含整個 Zookeeper 集羣唯一的 ID(整數)。

**zxid:**類似於 RDBMS 中的事務 ID,用於標識一次更新操作的 Proposal ID。爲了保證順序性,該 zkid 必須單調遞增。因此 Zookeeper 使用一個 64 位的數來表示,高 32 位是 Leader 的 epoch,從 1 開始,每次選出新的 Leader,epoch 加一。低 32 位爲該 epoch 內的序號,每次 epoch 變化,都將低 32 位的序號重置。這樣保證了 zkid 的全局遞增性。

4.1.2 服務器狀態

4.1.3 選票數據結構

每個服務器在進行領導選舉時,會發送如下關鍵信息:

4.1.4 投票流程

(1)自增選舉輪次

Zookeeper 規定所有有效的投票都必須在同一輪次中。每個服務器在開始新一輪投票時,會先對自己維護的 logicClock 進行自增操作。

(2)初始化選票

每個服務器在廣播自己的選票前,會將自己的投票箱清空。該投票箱記錄了所收到的選票。例:服務器 2 投票給服務器 3,服務器 3 投票給服務器 1,則服務器 1 的投票箱爲 (2, 3), (3, 1), (1, 1)。票箱中只會記錄每一投票者的最後一票,如投票者更新自己的選票,則其它服務器收到該新選票後會在自己票箱中更新該服務器的選票。

(3)發送初始化選票

每個服務器最開始都是通過廣播把票投給自己。

(4)接收外部投票

服務器會嘗試從其它服務器獲取投票,並記入自己的投票箱內。如果無法獲取任何外部投票,則會確認自己是否與集羣中其它服務器保持着有效連接。如果是,則再次發送自己的投票;如果否,則馬上與之建立連接。

(5)判斷選舉輪次

收到外部投票後,首先會根據投票信息中所包含的 logicClock 來進行不同處理:

(6)選票 PK

選票 PK 是基於 (self_id, self_zxid) 與(vote_id, vote_zxid)的對比:

(7)統計選票

如果已經確定有過半服務器認可了自己的投票(可能是更新後的投票),則終止投票。否則繼續接收其它服務器的投票。

(8)更新服務器狀態

投票終止後,服務器開始更新自身狀態。若過半的票投給了自己,則將自己的服務器狀態更新爲 LEADING,否則將自己的狀態更新爲 FOLLOWING。

通過以上流程分析,我們不難看出:要使 Leader 獲得多數 Server 的支持,則 ZooKeeper 集羣節點數必須是奇數。且存活的節點數目不得少於 N + 1

每個 Server 啓動後都會重複以上流程。在恢復模式下,如果是剛從崩潰狀態恢復的或者剛啓動的 server 還會從磁盤快照中恢復數據和會話信息,zk 會記錄事務日誌並定期進行快照,方便在恢復時進行狀態恢復。

4.2 原子廣播(Atomic Broadcast)

ZooKeeper 通過副本機制來實現高可用。

那麼,ZooKeeper 是如何實現副本機制的呢?答案是:ZAB 協議的原子廣播。

圖片

ZAB 協議的原子廣播要求:

**所有的寫請求都會被轉發給 Leader,Leader 會以原子廣播的方式通知 Follow。當半數以上的 Follow 已經更新狀態持久化後,Leader 纔會提交這個更新,然後客戶端纔會收到一個更新成功的響應。**這有些類似數據庫中的兩階段提交協議。

在整個消息的廣播過程中,Leader 服務器會每個事物請求生成對應的 Proposal,併爲其分配一個全局唯一的遞增的事務 ID(ZXID),之後再對其進行廣播。

五、ZooKeeper 應用

ZooKeeper 可以用於發佈 / 訂閱、負載均衡、命令服務、分佈式協調 / 通知、集羣管理、Master 選舉、分佈式鎖和分佈式隊列等功能 。

5.1 命名服務

在分佈式系統中,通常需要一個全局唯一的名字,如生成全局唯一的訂單號等,ZooKeeper 可以通過順序節點的特性來生成全局唯一 ID,從而可以對分佈式系統提供命名服務。

圖片

5.2 配置管理

利用 ZooKeeper 的觀察機制,可以將其作爲一個高可用的配置存儲器,允許分佈式應用的參與者檢索和更新配置文件。

5.3 分佈式鎖

可以通過 ZooKeeper 的臨時節點和 Watcher 機制來實現分佈式鎖。

舉例來說,有一個分佈式系統,有三個節點 A、B、C,試圖通過 ZooKeeper 獲取分佈式鎖。

(1)訪問 /lock (這個目錄路徑由程序自己決定),創建 帶序列號的臨時節點(EPHEMERAL) 。

圖片

(2)每個節點嘗試獲取鎖時,拿到 /locks 節點下的所有子節點(id_0000,id_0001,id_0002),判斷自己創建的節點是不是最小的。

圖片

(3)釋放鎖,即刪除自己創建的節點。

圖片

圖中,NodeA 刪除自己創建的節點 id_0000,NodeB 監聽到變化,發現自己的節點已經是最小節點,即可獲取到鎖。

5.4 集羣管理

ZooKeeper 還能解決大多數分佈式系統中的問題:

5.5 選舉 Leader 節點

分佈式系統一個重要的模式就是主從模式 (Master/Salves),ZooKeeper 可以用於該模式下的 Matser 選舉。可以讓所有服務節點去競爭性地創建同一個 ZNode,由於 ZooKeeper 不能有路徑相同的 ZNode,必然只有一個服務節點能夠創建成功,這樣該服務節點就可以成爲 Master 節點。

5.6 隊列管理

ZooKeeper 可以處理兩種類型的隊列:

同步隊列用 ZooKeeper 實現的實現思路如下:

創建一個父目錄 /synchronizing,每個成員都監控標誌(Set Watch)位目錄 /synchronizing/start 是否存在,然後每個成員都加入這個隊列,加入隊列的方式就是創建 /synchronizing/member_i 的臨時目錄節點,然後每個成員獲取 / synchronizing 目錄的所有目錄節點,也就是 member_i。判斷 i 的值是否已經是成員的個數,如果小於成員個數等待 /synchronizing/start 的出現,如果已經相等就創建 /synchronizing/start。

參考資料

官方

書籍

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