ZooKeeper 如何使用Watcher
- 2019 年 11 月 26 日
- 筆記
1. 工作流程
ZooKeeper 允許客戶端向服務端註冊一個 Watcher 監聽,當服務端的一些指定事件觸發了這個 Watcher,那麼就向指定客戶端(註冊了對應 Watcher 監聽的客戶端)發送一個事件通知來實現分散式的通知功能。整個 Watcher 註冊與通知過程如下圖所示:
從上圖可以看出 ZooKeeper 的 Watcher 機制主要由客戶端執行緒、客戶端 WatchManager 以及 ZooKeeper 伺服器三部分組成。在具體流程上,客戶端在向 ZooKeeper 伺服器註冊 Watcher 的同時(步驟一),會將 Watcher 對象存儲在客戶端的 WatchManager 上(步驟二)。當 ZooKeeper 伺服器觸發了 Watcher 事件後,會向客戶端發送通知(步驟三)。客戶端執行緒從 WatchManager 取出對應的 Watcher 對象來執行回調邏輯(步驟四)。
2. Watcher介面
如果要想使用 Watcher 機制,我們需要實現 Watcher 介面類,實現其中的 process()
方法:
public void process(WatchedEvent event);
在 ZooKeeper 中,介面類 Watcher 用於表示一個標準的事件處理器,其定義了事件通知相關的邏輯,包含 KeeperState 和 EventType 兩個枚舉類,分別代表了通知狀態和事件類型。
2.1 Watcher類型
ZooKeeper
中有兩個重要的 Watcher,一個是數據監視點,另一個是子節點監視點:
public static enum WatcherType { // 子節點監視點 Children(1), // 數據監視點 Data(2), Any(3); ... }
getData()
和 exists()
可以設置數據監視點。getChildren()
可以設置子節點監視點。我們也可以根據方法返回的數據類型來判斷設置的監視點類型。getData()
和 exists()
返回有關節點的數據資訊,而 getChildren()
返回子節點列表。因此,可以輕鬆通過返回的數據類型判斷監視點類型。
創建、刪除或者設置一個 ZNode
節點的數據都會觸發其數據監視點。子節點監視點只有在 ZNode
的子節點創建或者刪除時才會被觸發。setData()
會觸發正在設置的 ZNode
節點的數據監視點。create()
會同時觸發正在創建的 ZNode
節點的數據監視點以及父 ZNode
節點的子節點監視點。delete()
會同時觸發正要刪除的 Znode
節點的數據監視點、子節點監視點,以及父 ZNode
節點的子節點監視點。
2.2 通知狀態與事件類型
ZooKeeper 通知狀態:
@Public public static enum KeeperState { @Deprecated Unknown(-1), Disconnected(0), @Deprecated NoSyncConnected(1), SyncConnected(3), AuthFailed(4), ConnectedReadOnly(5), SaslAuthenticated(6), Expired(-112), Closed(7); ... }
ZooKeeper 事件類型:
@Public public static enum EventType { NodeCreated(1), NodeDeleted(2), NodeDataChanged(3), NodeChildrenChanged(4), None(-1), DataWatchRemoved(5), ChildWatchRemoved(6); ... }
前三個事件類型都只涉及單個ZNode
節點,而第四個事件類型涉及監視的 ZNode
節點的子節點。
同一個事件類型在不同的通知狀態中代表的含義有所不同,下表列舉了常見的通知狀態和事件類型:
通知狀態 |
狀態說明 |
事件類型 |
設置方法 |
觸發條件 |
---|---|---|---|---|
Unknown(-1) |
從3.1.0版本開始被廢棄 |
|
|
|
Disconnected(0) |
客戶端和伺服器處於斷開連接狀態 |
None(-1) |
|
客戶端與ZooKeeper伺服器斷開連接 |
NoSyncConnected(1) |
從3.1.0版本開始被廢棄 |
|
|
|
SyncConnected(3) |
客戶端和伺服器處於連接狀態 |
None(-1) |
|
客戶端與伺服器成功建立會話 |
SyncConnected(3) |
客戶端和伺服器處於連接狀態 |
NodeCreated(1) |
通過exists調用設置 |
Watcher監聽的對應數據節點被創建,通過create調用觸發 |
SyncConnected(3) |
客戶端和伺服器處於連接狀態 |
NodeDeleted(2) |
通過exists或者getData調用設置 |
Watcher監聽的對應數據節點被刪除,通過delete調用觸發 |
SyncConnected(3) |
客戶端和伺服器處於連接狀態 |
NodeDataChanged(3) |
通過exists或者getData調用設置 |
Watcher監聽的對應數據節點的數據內容發生變更,通過setData調用觸發 |
SyncConnected(3) |
客戶端和伺服器處於連接狀態 |
NodeChildrenChanged(4) |
通過getChildren調用設置 |
Watcher監聽的對應數據節點的子節點列表發生變更,通過create、delete調用觸發 |
AuthFailed(4) |
許可權驗證失敗狀態,通常同時也會收到AuthFailedException異常 |
None(-1) |
|
通常有兩種情況:(1)使用錯誤的scheme進行許可權檢查。(2)SASL許可權檢查失敗。 |
Expired(-112) |
此時客戶端會話失效,通常同時也會收到SessionExpiredException異常 |
None(-1) |
|
會話超時 |
上表中列舉了 ZooKeeper 中最常見的幾個通知狀態和事件類型。對於 NodeDataChanged 事件類型,此處所說的變更包括節點的數據內容和數據的版本號 dataVersion 的變更。因此即使使用相同的數據內容來更新,也會觸發這個事件通知,因為對於 ZooKeeper 來說,無論數據內容是否變更,一旦有客戶端調用了數據更新的介面,且更新成功,就會更新 dataVersion 值。
NodeChildrenChanged 事件會在數據節點的子節點列表發生變更的時候被觸發,這裡說的子節點列表變化特指子節點個數和組合情況的變更,即新增子節點或刪除子節點,而子節點內容的變化是不會觸發這個事件的。
對於AuthFailed這個事件,需要注意的地方是,它的觸發條件並不是簡簡單單因為當前客戶端會話沒有許可權,而是授權失敗。
2.3 回調方法Process
Process 方法是 Watcher 介面中的一個回調方法,當 ZooKeeper 向客戶端發送一個 Watcher 事件通知時,客戶單就會對相應的 process 方法進行回調,從而實現對事件的處理。process方法的定義如下:
abstract public void process(WatchedEvent event);
這個回調方法的定義非常簡單,我們重點看下方法的參數定義:WatchedEvent。WatchedEvent 包含了每一個事件的三個基本屬性:通知狀態(keeperState)、事件類型(eventType)和節點路徑(path)。ZooKeeper 使用 WatchedEvent 對象來封裝服務端事件並傳遞給 Watcher,從而方便回調方法 process 對服務端事件進行處理。
Example:
public void process(WatchedEvent event) { Event.KeeperState state = event.getState(); String path = event.getPath(); // 連接狀態 if (state == Event.KeeperState.SyncConnected) { System.out.println("客戶端與ZooKeeper伺服器處於連接狀態"); connectedSignal.countDown(); if(event.getType() == Event.EventType.None && null == event.getPath()) { System.out.println("監控狀態變化"); } else if(event.getType() == Event.EventType.NodeCreated) { System.out.println("監控到節點[" + path + "]被創建"); } else if(event.getType() == Event.EventType.NodeDataChanged) { System.out.println("監控到節點[" + path + "]的數據內容發生變化"); } else if(event.getType() == Event.EventType.NodeDeleted) { System.out.println("監控到節點[" + path + "]被刪除"); } } // 斷開連接狀態 else if (state == Event.KeeperState.Disconnected){ System.out.println("客戶端與ZooKeeper伺服器處於斷開連接狀態"); } // 會話超時 else if (state == Event.KeeperState.Expired){ System.out.println("客戶端與ZooKeeper伺服器會話超時"); } }
3. 註冊Watcher
我們知道創建一個 ZooKeeper 客戶端對象實例時,可以向構造方法中傳入一個默認的Watcher:
public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher);
這個 Watcher 將作為整個 ZooKeeper 會話期間的默認 Watcher,會一直被保存在客戶端 ZKWatchManager 的 defaultWatcher 中。ZooKeeper 的API中所有讀操作: getData()
、getChildren()
以及 exists()
都可以選擇在讀取的 ZNode
節點上註冊 Watcher。對於 ZooKeeper 節點的事件通知,我們可以使用默認的 Watcher,也可以單獨實現一個 Watcher。例如,getData調用有兩種方式註冊 Watcher:
public byte[] getData(String path, boolean watch, Stat stat) public byte[] getData(final String path, Watcher watcher, Stat stat)
在這兩個介面上都可以進行 Watcher 的註冊,第一個介面通過一個 boolean 參數來標識是否使用上文提到的默認 Watcher 來進行註冊,具體的註冊邏輯和第二個介面是一致的。
4. Watcher特性
4.1 一次性
無論是服務端還是客戶端,一旦一個 Watcher 被觸發,ZooKeeper 都將其從相應的存儲中移除。因此,開發人員在 Watcher 的使用上要記住的一點是需要反覆註冊。例如,如果客戶端執行 getData("/znode1",true)
,後面對 /znode1
的更改或刪除,客戶端都會獲得 /znode1
的監控事件通知。如果 /znode1
再次更改,如果客戶端沒有執行新一次設置新監視點的讀取,是不會發送監視事件通知的。
這樣的設計有效地減輕了服務端的壓力。試想,如果註冊一個 Watcher 之後一直有效,那麼,針對那些更新非常頻繁的節點,服務端會不斷地向客戶端發送事件通知,這無論對於網路還是服務端性能的影響都非常大。
4.2 客戶端串列執行
客戶端Watcher回調的過程是一個串列同步的過程,這為我們保證了順序,同時,需要開發人員注意的一點是,千萬不要因為一個Watcher的處理邏輯影響了整個客戶端的Watcher回調。
4.3 輕量
WatchedEvent 是 ZooKeeper 整個 Watcher 通知機制的最小通知單元,這個數據結構中只包含三部分內容:通知狀態、事件類型和節點路徑。也就是說,Watcher 通知非常簡單,只會告訴客戶端發生了事件,而不會說明事件的具體內容。例如針對 NodeDataChanged 事件,ZooKeeper 的 Watcher 只會通知客戶端指定數據節點的數據內容發生了變更,而對於原始數據以及變更後的新數據都無法從這個事件中直接獲取到,而是需要客戶端主要重新去獲取數據——這也是 ZooKeeper 的 Watcher 機制的一個非常重要的特性。
另外,客戶端向服務端註冊 Watcher 的時候,並不會把客戶端真實的 Watcher 對象傳遞給服務端,僅僅只是在客戶端請求中使用 boolean 類型屬性進行了標記,同時服務端也僅僅只是保存了當前連接的 ServerCnxn 對象。如此輕量的Watcher機制設計,在網路開銷和服務端記憶體開銷上都是非常廉價的。
英譯對照:
Watch
: 監視點
參考:
- ZooKeeper分散式過程協同技術詳解
- 從Paxos到ZooKeeper分散式一致性原理與實踐