ZooKeeper 是一個分佈式協調服務 ,由 Apache 進行維護。ZooKeeper 能夠視爲一個高可用的文件系統。html
ZooKeeper 能夠用於發佈/訂閱、負載均衡、命令服務、分佈式協調/通知、集羣管理、Master 選舉、分佈式鎖和分佈式隊列等功能 。node
ZooKeeper 是 Apache 的頂級項目。ZooKeeper 爲分佈式應用提供了高效且可靠的分佈式協調服務,提供了諸如統一命名服務、配置管理和分佈式鎖等分佈式的基礎服務。在解決分佈式數據一致性方面,ZooKeeper 並無直接採用 Paxos 算法,而是採用了名爲 ZAB 的一致性協議。git
ZooKeeper 主要用來解決分佈式集羣中應用系統的一致性問題,它能提供基於相似於文件系統的目錄節點樹方式的數據存儲。可是 ZooKeeper 並非用來專門存儲數據的,它的做用主要是用來維護和監控存儲數據的狀態變化。經過監控這些數據狀態的變化,從而能夠達到基於數據的集羣管理。github
不少大名鼎鼎的框架都基於 ZooKeeper 來實現分佈式高可用,如:Dubbo、Kafka 等。算法
ZooKeeper 具備如下特性:數據庫
ZooKeeper 的數據模型是一個樹形結構的文件系統。apache
樹中的節點被稱爲 znode,其中根節點爲 /,每一個節點上都會保存本身的數據和節點信息。znode 能夠用於存儲數據,而且有一個與之相關聯的 ACL(詳情可見 ACL)。ZooKeeper 的設計目標是實現協調服務,而不是真的做爲一個文件存儲,所以 znode 存儲數據的大小被限制在 1MB 之內。服務器
ZooKeeper 的數據訪問具備原子性。其讀寫操做都是要麼所有成功,要麼所有失敗。session
znode 經過路徑被引用。znode 節點路徑必須是絕對路徑。數據結構
znode 有兩種類型:
znode 上有一個順序標誌( SEQUENTIAL )。若是在建立 znode 時,設置了順序標誌( SEQUENTIAL ),那麼 ZooKeeper 會使用計數器爲 znode 添加一個單調遞增的數值,即 zxid。ZooKeeper 正是利用 zxid 實現了嚴格的順序訪問控制能力。
每一個 znode 節點在存儲數據的同時,都會維護一個叫作 Stat 的數據結構,裏面存儲了關於該節點的所有狀態信息。以下:
Zookeeper 集羣是一個基於主從複製的高可用集羣,每一個服務器承擔以下三種角色中的一種。
ZooKeeper 採用 ACL(Access Control Lists)策略來進行權限控制。
每一個 znode 建立時都會帶有一個 ACL 列表,用於決定誰能夠對它執行何種操做。
ACL 依賴於 ZooKeeper 的客戶端認證機制。ZooKeeper 提供瞭如下幾種認證方式:
ZooKeeper 定義了以下五種權限:
Leader/Follower/Observer 均可直接處理讀請求,從本地內存中讀取數據並返回給客戶端便可。
因爲處理讀請求不須要服務器之間的交互,Follower/Observer 越多,總體系統的讀請求吞吐量越大,也即讀性能越好。
全部的寫請求實際上都要交給 Leader 處理。Leader 將寫請求以事務形式發給全部 Follower 並等待 ACK,一旦收到半數以上 Follower 的 ACK,即認爲寫操做成功。
由上圖可見,經過 Leader 進行寫操做,主要分爲五步:
注意
Follower/Observer 都可接受寫請求,但不能直接處理,而須要將寫請求轉發給 Leader 處理。
除了多了一步請求轉發,其它流程與直接寫 Leader 無任何區別。
對於來自客戶端的每一個更新請求,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 位用於遞增計數。
詳細過程以下:
客戶端註冊監聽它關心的 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 監聽事件,寫請求在觸發事件時就能將通知發送給指定的客戶端。
ZooKeeper 客戶端經過 TCP 長鏈接鏈接到 ZooKeeper 服務集羣。會話 (Session) 從第一次鏈接開始就已經創建,以後經過心跳檢測機制來保持有效的會話狀態。經過這個鏈接,客戶端能夠發送請求並接收響應,同時也能夠接收到 Watch 事件的通知。
每一個 ZooKeeper 客戶端配置中都配置了 ZooKeeper 服務器集羣列表。啓動時,客戶端會遍歷列表去嘗試創建鏈接。若是失敗,它會嘗試鏈接下一個服務器,依次類推。
一旦一臺客戶端與一臺服務器創建鏈接,這臺服務器會爲這個客戶端建立一個新的會話。每一個會話都會有一個超時時間,若服務器在超時時間內沒有收到任何請求,則相應會話被視爲過時。一旦會話過時,就沒法再從新打開,且任何與該會話相關的臨時 znode 都會被刪除。
一般來講,會話應該長期存在,而這須要由客戶端來保證。客戶端能夠經過心跳方式(ping)來保持會話不過時。
ZooKeeper 的會話具備四個屬性:
Zookeeper 的會話管理主要是經過 SessionTracker 來負責,其採用了分桶策略(將相似的會話放在同一區塊中進行管理)進行管理,以便 Zookeeper 對會話進行不一樣區塊的隔離處理以及同一區塊的統一處理。
ZooKeeper 並無直接採用 Paxos 算法,而是採用了名爲 ZAB 的一致性協議。ZAB 協議不是 Paxos 算法,只是比較相似,兩者在操做上並不相同。
ZAB 協議是 Zookeeper 專門設計的一種支持崩潰恢復的原子廣播協議。
ZAB 協議是 ZooKeeper 的數據一致性和高可用解決方案。
ZAB 協議定義了兩個能夠無限循環的流程:
ZooKeeper 的故障恢復
ZooKeeper 集羣採用一主(稱爲 Leader)多從(稱爲 Follower)模式,主從節點經過副本機制保證數據一致。
ZAB 協議的選舉 Leader 機制簡單來講,就是:基於過半選舉機制產生新的 Leader,以後其餘機器將重新的 Leader 上同步狀態,當有過半機器完成狀態同步後,就退出選舉 Leader 模式,進入原子廣播模式。
myid:每一個 Zookeeper 服務器,都須要在數據文件夾下建立一個名爲 myid 的文件,該文件包含整個 Zookeeper 集羣惟一的 ID(整數)。
zxid:相似於 RDBMS 中的事務 ID,用於標識一次更新操做的 Proposal ID。爲了保證順序性,該 zkid 必須單調遞增。所以 Zookeeper 使用一個 64 位的數來表示,高 32 位是 Leader 的 epoch,從 1 開始,每次選出新的 Leader,epoch 加一。低 32 位爲該 epoch 內的序號,每次 epoch 變化,都將低 32 位的序號重置。這樣保證了 zkid 的全局遞增性。
每一個服務器在進行領導選舉時,會發送以下關鍵信息:
(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 會記錄事務日誌並按期進行快照,方便在恢復時進行狀態恢復。
ZooKeeper 經過副本機制來實現高可用。
那麼,ZooKeeper 是如何實現副本機制的呢?答案是:ZAB 協議的原子廣播。
ZAB 協議的原子廣播要求:
全部的寫請求都會被轉發給 Leader,Leader 會以原子廣播的方式通知 Follow。當半數以上的 Follow 已經更新狀態持久化後,Leader 纔會提交這個更新,而後客戶端纔會收到一個更新成功的響應。這有些相似數據庫中的兩階段提交協議。
在整個消息的廣播過程當中,Leader 服務器會每一個事物請求生成對應的 Proposal,併爲其分配一個全局惟一的遞增的事務 ID(ZXID),以後再對其進行廣播。
ZooKeeper 能夠用於發佈/訂閱、負載均衡、命令服務、分佈式協調/通知、集羣管理、Master 選舉、分佈式鎖和分佈式隊列等功能 。
在分佈式系統中,一般須要一個全局惟一的名字,如生成全局惟一的訂單號等,ZooKeeper 能夠經過順序節點的特性來生成全局惟一 ID,從而能夠對分佈式系統提供命名服務。
利用 ZooKeeper 的觀察機制,能夠將其做爲一個高可用的配置存儲器,容許分佈式應用的參與者檢索和更新配置文件。
能夠經過 ZooKeeper 的臨時節點和 Watcher 機制來實現分佈式鎖。
舉例來講,有一個分佈式系統,有三個節點 A、B、C,試圖經過 ZooKeeper 獲取分佈式鎖。
(1)訪問 /lock (這個目錄路徑由程序本身決定),建立 帶序列號的臨時節點(EPHEMERAL) 。
(2)每一個節點嘗試獲取鎖時,拿到 /locks節點下的全部子節點(id\_0000,id\_0001,id_0002),判斷本身建立的節點是否是最小的。
釋放鎖:執行完操做後,把建立的節點給刪掉。
(3)釋放鎖,即刪除本身建立的節點。
圖中,NodeA 刪除本身建立的節點 id_0000,NodeB 監聽到變化,發現本身的節點已是最小節點,便可獲取到鎖。
ZooKeeper 還能解決大多數分佈式系統中的問題:
分佈式系統一個重要的模式就是主從模式 (Master/Salves),ZooKeeper 能夠用於該模式下的 Matser 選舉。可讓全部服務節點去競爭性地建立同一個 ZNode,因爲 ZooKeeper 不能有路徑相同的 ZNode,必然只有一個服務節點可以建立成功,這樣該服務節點就能夠成爲 Master 節點。
ZooKeeper 能夠處理兩種類型的隊列:
同步隊列用 ZooKeeper 實現的實現思路以下:
建立一個父目錄 /synchronizing,每一個成員都監控標誌(Set Watch)位目錄 /synchronizing/start 是否存在,而後每一個成員都加入這個隊列,加入隊列的方式就是建立 /synchronizing/member\_i 的臨時目錄節點,而後每一個成員獲取 / synchronizing 目錄的全部目錄節點,也就是 member\_i。判斷 i 的值是否已是成員的個數,若是小於成員個數等待 /synchronizing/start 的出現,若是已經相等就建立 /synchronizing/start。
官方
書籍
文章
做者:ZhangPeng