一文掌握 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 具有以下特性:
-
**順序一致性:**所有客戶端看到的服務端數據模型都是一致的;從一個客戶端發起的事務請求,最終都會嚴格按照其發起順序被應用到 ZooKeeper 中。具體的實現可見下文:原子廣播。
-
**原子性:**所有事務請求的處理結果在整個集羣中所有機器上的應用情況是一致的,即整個集羣要麼都成功應用了某個事務,要麼都沒有應用。實現方式可見下文:事務。
-
**單一視圖:**無論客戶端連接的是哪個 Zookeeper 服務器,其看到的服務端數據模型都是一致的。
-
**高性能:**ZooKeeper 將數據全量存儲在內存中,所以其性能很高。需要注意的是:由於 ZooKeeper 的所有更新和刪除都是基於事務的,因此 ZooKeeper 在讀多寫少的應用場景中有性能表現較好,如果寫操作頻繁,性能會大大下滑。
-
**高可用:**ZooKeeper 的高可用是基於副本機制實現的,此外 ZooKeeper 支持故障恢復,可見下文:選舉 Leader。
1.3 ZooKeeper 的設計目標
-
簡單的數據模型
-
可以構建集羣
-
順序訪問
-
高性能
二、ZooKeeper 核心概念
2.1 數據模型
ZooKeeper 的數據模型是一個樹形結構的文件系統。
樹中的節點被稱爲 znode,其中根節點爲 /,每個節點上都會保存自己的數據和節點信息。znode 可以用於存儲數據,並且有一個與之相關聯的 ACL(詳情可見 ACL)。ZooKeeper 的設計目標是實現協調服務,而不是真的作爲一個文件存儲,因此 znode 存儲數據的大小被限制在 1MB 以內。
**ZooKeeper 的數據訪問具有原子性。**其讀寫操作都是要麼全部成功,要麼全部失敗。
znode 通過路徑被引用。znode 節點路徑必須是絕對路徑。
znode 有兩種類型:
-
**臨時的( EPHEMERAL ):**戶端會話結束時,ZooKeeper 就會刪除臨時的 znode。
-
**持久的(PERSISTENT ):**除非客戶端主動執行刪除操作,否則 ZooKeeper 不會刪除持久的 znode。
2.2 節點信息
znode 上有一個順序標誌( SEQUENTIAL )。如果在創建 znode 時,設置了順序標誌( SEQUENTIAL ),那麼 ZooKeeper 會使用計數器爲 znode 添加一個單調遞增的數值,即 zxid。ZooKeeper 正是利用 zxid 實現了嚴格的順序訪問控制能力。
每個 znode 節點在存儲數據的同時,都會維護一個叫做 Stat 的數據結構,裏面存儲了關於該節點的全部狀態信息。如下:
2.3 集羣角色
Zookeeper 集羣是一個基於主從複製的高可用集羣,每個服務器承擔如下三種角色中的一種。
-
**Leader:**它負責 發起並維護與各 Follwer 及 Observer 間的心跳。所有的寫操作必須要通過 Leader 完成再由 Leader 將寫操作廣播給其它服務器。一個 Zookeeper 集羣同一時間只會有一個實際工作的 Leader。
-
**Follower:**它會響應 Leader 的心跳。Follower 可直接處理並返回客戶端的讀請求,同時會將寫請求轉發給 Leader 處理,並且負責在 Leader 處理寫請求時對請求進行投票。一個 Zookeeper 集羣可能同時存在多個 Follower。
-
**Observer:**角色與 Follower 類似,但是無投票權。
2.4 ACL
ZooKeeper 採用 ACL(Access Control Lists)策略來進行權限控制。
每個 znode 創建時都會帶有一個 ACL 列表,用於決定誰可以對它執行何種操作。
ACL 依賴於 ZooKeeper 的客戶端認證機制。ZooKeeper 提供了以下幾種認證方式:
-
**digest:**用戶名和密碼 來識別客戶端
-
**sasl:**通過 kerberos 來識別客戶端
-
**ip:**通過 IP 來識別客戶端
ZooKeeper 定義瞭如下五種權限:
-
**CREATE:**允許創建子節點;
-
**READ:**允許從節點獲取數據並列出其子節點;
-
**WRITE:**允許爲節點設置數據;
-
**DELETE:**允許刪除子節點;
-
**ADMIN:**允許爲節點設置權限。
三、ZooKeeper 工作原理
3.1 讀操作
Leader/Follower/Observer 都可直接處理讀請求,從本地內存中讀取數據並返回給客戶端即可。
由於處理讀請求不需要服務器之間的交互,Follower/Observer 越多,整體系統的讀請求吞吐量越大,也即讀性能越好。
3.2 寫操作
所有的寫請求實際上都要交給 Leader 處理。Leader 將寫請求以事務形式發給所有 Follower 並等待 ACK,一旦收到半數以上 Follower 的 ACK,即認爲寫操作成功。
3.2.1 寫 Leader
由上圖可見,通過 Leader 進行寫操作,主要分爲五步:
-
客戶端向 Leader 發起寫請求。
-
Leader 將寫請求以事務 Proposal 的形式發給所有 Follower 並等待 ACK。
-
Follower 收到 Leader 的事務 Proposal 後返回 ACK。
-
Leader 得到過半數的 ACK(Leader 對自己默認有一個 ACK)後向所有的 Follower 和 Observer 發送 Commmit。
-
Leader 將處理結果返回給客戶端。
注意
-
Leader 不需要得到 Observer 的 ACK,即 Observer 無投票權。
-
Leader 不需要得到所有 Follower 的 ACK,只要收到過半的 ACK 即可,同時 Leader 本身對自己有一個 ACK。上圖中有 4 個 Follower,只需其中兩個返回 ACK 即可,因爲 $$(2+1) / (4+1) > 1/2$$ 。
-
Observer 雖然無投票權,但仍須同步 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 位用於遞增計數。
詳細過程如下:
-
Leader 等待 Server 連接;
-
Follower 連接 Leader,將最大的 zxid 發送給 Leader;
-
Leader 根據 Follower 的 zxid 確定同步點;
-
完成同步後通知 follower 已經成爲 uptodate 狀態;
-
Follower 收到 uptodate 消息後,又可以重新接受 client 的請求進行服務了。
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 的會話具有四個屬性:
-
**sessionID:**會話 ID,唯一標識一個會話,每次客戶端創建新的會話時,Zookeeper 都會爲其分配一個全局唯一的 sessionID。
-
**TimeOut:**會話超時時間,客戶端在構造 Zookeeper 實例時,會配置 sessionTimeout 參數用於指定會話的超時時間,Zookeeper 客戶端向服務端發送這個超時時間後,服務端會根據自己的超時時間限制最終確定會話的超時時間。
-
**TickTime:**下次會話超時時間點,爲了便於 Zookeeper 對會話實行” 分桶策略” 管理,同時爲了高效低耗地實現會話的超時檢查與清理,Zookeeper 會爲每個會話標記一個下次會話超時時間點,其值大致等於當前時間加上 TimeOut。
-
**isClosing:**標記一個會話是否已經被關閉,當服務端檢測到會話已經超時失效時,會將該會話的 isClosing 標記爲” 已關閉”,這樣就能確保不再處理來自該會話的新請求了。
Zookeeper 的會話管理主要是通過 SessionTracker 來負責,其採用了分桶策略(將類似的會話放在同一區塊中進行管理)進行管理,以便 Zookeeper 對會話進行不同區塊的隔離處理以及同一區塊的統一處理。
四、ZAB 協議
ZooKeeper 並沒有直接採用 Paxos 算法,而是採用了名爲 ZAB 的一致性協議。ZAB 協議不是 Paxos 算法,只是比較類似,二者在操作上並不相同。
ZAB 協議是 Zookeeper 專門設計的一種支持崩潰恢復的原子廣播協議。
ZAB 協議是 ZooKeeper 的數據一致性和高可用解決方案。
ZAB 協議定義了兩個可以無限循環的流程:
-
**選舉 Leader:**用於故障恢復,從而保證高可用。
-
**原子廣播:**用於主從同步,從而保證數據一致性。
4.1 選舉 Leader
ZooKeeper 的故障恢復
ZooKeeper 集羣採用一主(稱爲 Leader)多從(稱爲 Follower)模式,主從節點通過副本機制保證數據一致。
-
如果 Follower 節點掛了 - ZooKeeper 集羣中的每個節點都會單獨在內存中維護自身的狀態,並且各節點之間都保持着通訊,只要集羣中有半數機器能夠正常工作,那麼整個集羣就可以正常提供服務。
-
如果 Leader 節點掛了 - 如果 Leader 節點掛了,系統就不能正常工作了。此時,需要通過 ZAB 協議的選舉 Leader 機制來進行故障恢復。
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 服務器狀態
-
**LOOKING:**不確定 Leader 狀態。該狀態下的服務器認爲當前集羣中沒有 Leader,會發起 Leader 選舉。
-
**FOLLOWING:**跟隨者狀態。表明當前服務器角色是 Follower,並且它知道 Leader 是誰。
-
**LEADING:**領導者狀態。表明當前服務器角色是 Leader,它會維護與 Follower 間的心跳。
-
**OBSERVING:**觀察者狀態。表明當前服務器角色是 Observer,與 Folower 唯一的不同在於不參與選舉,也不參與集羣寫操作時的投票。
4.1.3 選票數據結構
每個服務器在進行領導選舉時,會發送如下關鍵信息:
-
**logicClock:**每個服務器會維護一個自增的整數,名爲 logicClock,它表示這是該服務器發起的第多少輪投票。
-
**state:**當前服務器的狀態。
-
**self_id:**當前服務器的 myid。
-
**self_zxid:**當前服務器上所保存的數據的最大 zxid。
-
**vote_id:**被推舉的服務器的 myid。
-
**vote_zxid:**被推舉的服務器上所保存的數據的最大 zxid。
4.1.4 投票流程
(1)自增選舉輪次
Zookeeper 規定所有有效的投票都必須在同一輪次中。每個服務器在開始新一輪投票時,會先對自己維護的 logicClock 進行自增操作。
(2)初始化選票
每個服務器在廣播自己的選票前,會將自己的投票箱清空。該投票箱記錄了所收到的選票。例:服務器 2 投票給服務器 3,服務器 3 投票給服務器 1,則服務器 1 的投票箱爲 (2, 3), (3, 1), (1, 1)。票箱中只會記錄每一投票者的最後一票,如投票者更新自己的選票,則其它服務器收到該新選票後會在自己票箱中更新該服務器的選票。
(3)發送初始化選票
每個服務器最開始都是通過廣播把票投給自己。
(4)接收外部投票
服務器會嘗試從其它服務器獲取投票,並記入自己的投票箱內。如果無法獲取任何外部投票,則會確認自己是否與集羣中其它服務器保持着有效連接。如果是,則再次發送自己的投票;如果否,則馬上與之建立連接。
(5)判斷選舉輪次
收到外部投票後,首先會根據投票信息中所包含的 logicClock 來進行不同處理:
-
外部投票的 logicClock 大於自己的 logicClock。說明該服務器的選舉輪次落後於其它服務器的選舉輪次,立即清空自己的投票箱並將自己的 logicClock 更新爲收到的 logicClock,然後再對比自己之前的投票與收到的投票以確定是否需要變更自己的投票,最終再次將自己的投票廣播出去。
-
外部投票的 logicClock 小於自己的 logicClock。當前服務器直接忽略該投票,繼續處理下一個投票。
-
外部投票的 logickClock 與自己的相等。當時進行選票 PK。
(6)選票 PK
選票 PK 是基於 (self_id, self_zxid) 與(vote_id, vote_zxid)的對比:
-
外部投票的 logicClock 大於自己的 logicClock,則將自己的 logicClock 及自己的選票的 logicClock 變更爲收到的 logicClock。
-
若 logicClock 一致,則對比二者的 vote_zxid,若外部投票的 vote_zxid 比較大,則將自己的票中的 vote_zxid 與 vote_myid 更新爲收到的票中的 vote_zxid 與 vote_myid 並廣播出去,另外將收到的票及自己更新後的票放入自己的票箱。如果票箱內已存在 (self_myid, self_zxid) 相同的選票,則直接覆蓋。
-
若二者 vote_zxid 一致,則比較二者的 vote_myid,若外部投票的 vote_myid 比較大,則將自己的票中的 vote_myid 更新爲收到的票中的 vote_myid 並廣播出去,另外將收到的票及自己更新後的票放入自己的票箱。
(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),判斷自己創建的節點是不是最小的。
-
如果是,則拿到鎖。
釋放鎖:執行完操作後,把創建的節點給刪掉。
-
如果不是,則監聽比自己要小 1 的節點變化。
(3)釋放鎖,即刪除自己創建的節點。
圖中,NodeA 刪除自己創建的節點 id_0000,NodeB 監聽到變化,發現自己的節點已經是最小節點,即可獲取到鎖。
5.4 集羣管理
ZooKeeper 還能解決大多數分佈式系統中的問題:
-
如可以通過創建臨時節點來建立心跳檢測機制。如果分佈式系統的某個服務節點宕機了,則其持有的會話會超時,此時該臨時節點會被刪除,相應的監聽事件就會被觸發。
-
分佈式系統的每個服務節點還可以將自己的節點狀態寫入臨時節點,從而完成狀態報告或節點工作進度彙報。
-
通過數據的訂閱和發佈功能,ZooKeeper 還能對分佈式系統進行模塊的解耦和任務的調度。
-
通過監聽機制,還能對分佈式系統的服務節點進行動態上下線,從而實現服務的動態擴容。
5.5 選舉 Leader 節點
分佈式系統一個重要的模式就是主從模式 (Master/Salves),ZooKeeper 可以用於該模式下的 Matser 選舉。可以讓所有服務節點去競爭性地創建同一個 ZNode,由於 ZooKeeper 不能有路徑相同的 ZNode,必然只有一個服務節點能夠創建成功,這樣該服務節點就可以成爲 Master 節點。
5.6 隊列管理
ZooKeeper 可以處理兩種類型的隊列:
-
當一個隊列的成員都聚齊時,這個隊列纔可用,否則一直等待所有成員到達,這種是同步隊列。
-
隊列按照 FIFO 方式進行入隊和出隊操作,例如實現生產者和消費者模型。
同步隊列用 ZooKeeper 實現的實現思路如下:
創建一個父目錄 /synchronizing,每個成員都監控標誌(Set Watch)位目錄 /synchronizing/start 是否存在,然後每個成員都加入這個隊列,加入隊列的方式就是創建 /synchronizing/member_i 的臨時目錄節點,然後每個成員獲取 / synchronizing 目錄的所有目錄節點,也就是 member_i。判斷 i 的值是否已經是成員的個數,如果小於成員個數等待 /synchronizing/start 的出現,如果已經相等就創建 /synchronizing/start。
參考資料
官方
-
ZooKeeper 官網
-
ZooKeeper 官方文檔
-
ZooKeeper Github
書籍
-
《Hadoop 權威指南(第四版)》
-
《從 Paxos 到 Zookeeper 分佈式一致性原理與實踐》
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Mx-Ix1hczb2gOUnaO_63TQ