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 簡介及核心概念
- 詳解分散式協調服務 ZooKeeper
- 深入淺出 Zookeeper(一) Zookeeper 架構及 FastLeaderElection 機制
作者:ZhangPeng