ZooKeeper是用來協調(同步)分散式程式的服務,提供了一個簡單高效能的協調核心,使用者可以在此之上構建更多複雜的分散式協調功能。
多個分散式程式通過ZooKeeper提供的API來操作共享的ZooKeeper記憶體資料物件ZNode來達成某種一致的行為或結果,這種模式本質上是基於狀態共享的併發模型,與Java的多執行緒併發模型一致,他們的執行緒或程式都是”共享式記憶體通訊“。Java沒有直接提供某種響應式通知介面來監控某個物件狀態的變化,只能要麼浪費CPU時間毫無響應式的輪詢重試,或基於Java提供的某種主動通知(Notif)機制(內建佇列)來響應狀態變化,但這種機制是需要迴圈阻塞呼叫。而ZooKeeper實現這些分散式程式的狀態(ZNode的Data、Children)共享時,基於效能的考慮採用了類似的非同步非阻塞的主動通知模式即Watch機制,使得分散式程式之間的“共享狀態通訊”更加實時高效,其實這也是ZooKeeper的主要任務決定的—協調。Consul雖然也實現了Watch機制,但它是阻塞的長輪詢。
- ZooKeeper VS JVM
從某種角度來說,可以這樣對比(個人看法,可以討論),ZooKeeper對等於JVM,ZooKeeper包含狀態物件(ZNode)和分散式程式的底層執行引擎Zab,而JVM內部包含堆(多執行緒共享的大量物件存放區域)和多執行緒執行正確性約束規範JMM(Java記憶體模型),JMM確保了多執行緒的執行順序是正確的。Zab協議使得ZooKeeper的內部修改狀態操作直接是有序序列的,而JVM內部則是亂序並行的,需要新增額外的機制才能保證時序(記憶體屏障、處理器原子指令),而狀態讀取時,JVM和ZooKeeper都存在直接讀取時讀到舊資料,但ZooKeeper有Watch機制使得響應式讀取更高效,而JVM只能使用底層的記憶體屏障重新整理共享狀態,以便其他執行緒再次讀取時獲得正確的新資料。
ZooKeeper提供的介面使得所有的分散式程式的執行都是非同步非阻塞的(WaitFree演算法),內部是基於Version的CAS操作,而JVM提供了阻塞的和非阻塞的多種介面,有Synchronized、Volatile、AtomicOperations。基於介面之上構建執行緒或分散式程式之間更復雜的同步或協調功能時,Java併發庫直接提供了閉鎖、迴圈柵欄、訊號量等同步工具以及基礎的抽象佇列同步器,而ZooKeeper則需要使用者基於介面自行構建各種分散式協調功能(分散式鎖、分散式釋出訂閱、叢集成員關係管理)。如下圖:
ZooKeeper | JVM | |
共享狀態物件 | ZNode | 堆中物件 |
底層執行模式 | Zab順序執行 | 多處理器併發執行(記憶體屏障、原子機器指令) |
API介面 | Get、Watch_Get、Cas_Set、Exist | Synchronized、volatile、final、Atomic |
協調或同步功能 | 分散式釋出訂閱、鎖、讀寫鎖 | 併發庫同步工具、基於抽象佇列同步器構建的同步元件 |
- ZooKeeper的Watch架構
Watch的整體流程如下圖所示,客戶端先向ZooKeeper服務端成功註冊想要監聽的節點狀態,同時客戶端本地會儲存該監聽器相關的資訊在WatchManager中,當ZooKeeper服務端監聽的資料狀態發生變化時,ZooKeeper就會主動通知傳送相應事件資訊給相關會話客戶端,客戶端就會在本地響應式的回撥相關Watcher的Handler。
- ZooKeeper的Watch特性
- Watch是一次性的,每次都需要重新註冊,並且客戶端在會話異常結束時不會收到任何通知,而快速重連線時仍不影響接收通知。
- Watch的回撥執行都是順序執行的,並且客戶端在沒有收到關注資料的變化事件通知之前是不會看到最新的資料,另外需要注意不要在Watch回撥邏輯中阻塞整個客戶端的Watch回撥
- Watch是輕量級的,WatchEvent是最小的通訊單元,結構上只包含通知狀態、事件型別和節點路徑。ZooKeeper服務端只會通知客戶端發生了什麼,並不會告訴具體內容。
- Watcher介面設計
如上圖所示,Watch被設計成一個介面,任何實現了Watcher介面的類就是一個新的Watcher,Watcher內部包含2個列舉類,一個KeeperState,表示當事件發生時ZooKeeper的狀態,另一個是事件發生的型別,主要分為2類(一類是ZNode內容的變化,另一類是ZNode子節點的變化),具體的描述見下表。
KeeperState | EventType | TriggerCondition | EnableCalls | Desc |
SyncConnected (3)
| None (-1) | 客戶端與伺服器成功建立會話 | 此時客戶端與伺服器處於連線狀態 | |
同上 | NodeCreated (1) | Watcher監聽的對應資料節點被建立 | Exists | 同上 |
同上 | NodeDeleted (2) | Watcher監聽的對應資料節點被刪除 | Exists, GetData, and GetChildren | 同上 |
同上 | NodeDataChanged (3) | Watcher監聽的資料節點的資料內容和資料版本號發生變化 | Exists and GetData | 同上 |
同上 | NodeChildrenChanged (4) | Watcher監聽的資料節點的子節點列表發生變化,子節點內容變化不會觸發 | GetChildren | 同上 |
Disconnected (0) | None (-1) | 客戶端與ZooKeeper伺服器斷開連線 | 此時客戶端與伺服器處於斷開連線的狀態 | |
Expried (-112) | None (-1) | 會話超時 | 此時客戶端會話失效,通常同時也會收到SessionExpiredException異常 | |
AuthFailed (4) | None (-1) | 通常有兩種情況: 1.使用錯誤的scheme進行許可權檢查 2.SASL許可權檢查失敗 | 收到AuthFailedException異常 |
- WatchEvent的設計
如上圖所示,WatchEvent有2種表示模式,一種是邏輯表示即WatchedEvent,是直接封裝了各種抽象的邏輯狀態(KeeperState,EventType),適用於客戶端和服務端各自內部處理,另一種是物理表示即封裝的更多是底層基礎的傳輸資料結構(int,String),並且實現了序列化介面,主要用來做底層的資料傳輸。