ZK Watcher 的原理和實現

  • 2019 年 10 月 3 日
  • 筆記

什麼是 ZK Watcher

基於 ZK 的應用程序的一個常見需求是需要知道 ZK 集合的狀態。為了達到這個目的,一種方法是 ZK 客戶端定時輪詢 ZK 集合,檢查系統狀態是否發生了變化。然而,輪詢並不是一種高效的方式,尤其是在狀態變化的發生頻率很低的時候

因此,ZK 提供了一種通過通知客戶端感興趣的具體時間來避免輪詢造成的性能問題的方式,即設置 Watcher 的方式。通過設置 Watcher,ZK 客戶端可以對指定的 znode 註冊一個通知請求,在 znode 發生變化時收到一個單次的通知。例如,在 znode 被刪除時向 Watcher 發送節點被刪除的通知

應用 ZK Watcher 的代碼通常遵循如下的框架

zk.exists("myZnode", myWatcher, existsCallback, null);    Watcher myWatcher new Watcher() {    public void process(WatchedEvent event) {      // process the watch event    }  }    StatCallback existsCallback = new StatCallback() {    public void processResult(int rc, String path, Object ctx, Stat stat) {      // process the result of the exists call    }  }

上面的代碼框架中以 exists 操作為例,展示了異步調用 ZK 操作並註冊 Watcher 的一般用法

WatchedEvent 的分類

Watcher 的使用一個重要的內容就是了解 Watcher 如何設置以及何時觸發,並不是所有的 ZK 操作都可以設置 Watcher,Watcher 也不是會被所有事件觸發

拋開被重載的連接狀態的 WatchedEvent,業務過程中會遇到的 WatchedEvent 分為以下幾種

  • NodeCreated – 可以通過 exists 調用設置 Watcher,在 znode 從無到有創建的時候被觸發
  • NodeDeleted – 可以通過 exists 或者 getData 調用設置 Watcher,在 znode 被刪除時觸發
  • NodeDataChanged – 可以通過 exists 或者 getData 調用設置 Watcher,在 znode 數據發生變化時觸發
  • NodeChildrenChanged – 可以通過 getChildren 調用設置 Watcher,在 znode 的直接子節點創建或刪除時觸發
  • DataWatchRemoved – 在 exists 或者 getData 設置的 Watcher 被刪除時觸發對應的 Watcher
  • ChildWatchRemoved – 在 getChildren 設置的 Watcher 被刪除時觸發對應的 Watcher

可以看到,只有 exists 和 getData 和 getChildren 三種操作能夠設置 Watcher

注意,getData 創建的 Watcher 不會接收到 NodeCreated 事件,這是因為 getData 在節點不存在的時候會拋出 KeeperException.NoNodeException 異常,而不會設置 Watcher

Watcher 機制的實現與生命周期

從應用程序的角度來講,註冊完 Watcher 之後只要等待事件被觸發即可,無需關心 ZK 是怎麼實現這個過程的。不過了解 ZK 的具體實現機制有助於我們在面對錯誤或者異常的時候更好的理解問題的出處以及針對性的排查問題

Watcher 機制的實現最重要的問題就是 Watcher 究竟是註冊在哪裡的,以及 Watcher 究竟是如何觸發的。這兩個問題很難分開來解釋,因此下文會一併分析

原本講解原理的部分最好是結合對應的源代碼摘要來講解,但是 ZK 的源碼實在是難以閱讀,貼在這裡不但不能幫助理解,恐怕會讓讀者更加一頭霧水。我會從偽代碼的粒度介紹代碼邏輯並附上對應的源文件位置,有興趣的同學可以自行閱讀,祝身體健康

Watcher 機制的實現要從註冊講起,ZK 客戶端在執行 exists 或 getData 或 getChildren 操作的時候,可以設置一個自定義的 Watcher 或者通過 flag 復用創建客戶端時設置的 Watcher。後者實踐中比較少用,不做過多介紹。這個 Watcher 會被打包成 Packet 放進 ClientCnxnEventThread 中,在對應的操作完成時登記到客戶端的 Watches 集合里。在服務端,對應的 GetDataRequest 等請求有一個是否設置了 Watcher 的 flag,服務端由此來判斷是否要設置相應的 Watcher。這裡,ZK 為 ServerCnxn 實現了 Watcher 接口。ServerCnxn 是每個服務端上對於客戶端的連接對象,它的 process(WatchedEvent) 方法就是將對應的 WatchedEvent 打包為 WatcherEvent 然後發送給客戶端

Watcher 成功設置後需要關心的就是 Watcher 的觸發了,本質上 Watcher 是在 ZK 集合發生狀態變化的時候在客戶端回調對應處理邏輯的。但是 ZK 集合發生狀態變化要以服務端的狀態為準,服務器維護了 ZK 集合的狀態,這主要是由 ZKDatabaseDataTree 來實現的。當服務器判斷發生了需要出發 Watcher 的狀態變化時,服務器會遍歷異動節點上對應的 Watcher,在這裡就是對應的客戶端連接,回調它們的 process(WatchedEvent) 方法。如上所述,這就向客戶端發送了一個對應的 WatchedEvent

上面介紹的 Watcher 註冊和觸發的過程實際上就囊括了 Watcher 的整個生命周期,即 Watcher 的生命周期由對應操作在客服端成功時開始,到觸發後結束。也就是說,Watcher 是單次觸發的,觸發之後還想再次監聽對應節點的狀態需要重新設置 Watcher。Watcher 的生命周期結束還有另一個觸發條件,即 session 被關閉或過期。此外,在 3.5.0 之後的版本中,ZK 能夠主動執行 removeWatches 操作來移除不再感興趣的節點。

Watcher 的錯誤處理

如前所述,Watcher 是一種輕量級的相應變化的通知機制。由於其功能簡單,在實際應用當中為了構建更加複雜的語義,我們需要對 Watcher 在一些故障條件下的響應做對應的討論。

其中第一個是單次觸發和 WatchedEvent 攜帶的信息帶來的問題。由於 Watcher 是單次觸發的,所以我們可能會丟掉在前一個 Watcher 觸發後到後一個 Watcher 重新設置之前的事件。通常來說這不是問題,因為 ZK 的目標是實現一個分佈式環境下對狀態達成共識的存儲,而不保證每個事件都被客戶端記錄和處理。重新設置 Watcher 時附帶的動作足以保證我們同步了當時的最新狀態。因此,我們雖然漏掉了事件,但是那充其量只是一個中間狀態,ZK 提供的保證是關於一段時間內的最終狀態的。但是換個角度講,由於 WatchedEvent 只包含了【事件發生了】這個信息,所以任何新的狀態都需要重新從 ZK 集合上獲取,這是 ZK 為了實現的簡單在當初做的一個 trade-off

其中第二個是關於 CONNECTIONLOSS 異常的。嚴格來說這並不是 Watcher 應該關心的事情,因為操作由於 CONNECTIONLOSS 失敗時 Watcher 是無法被成功設置上去的。CONNECTIONLOSS 異常意味着客戶端和正在連接的服務器斷開連接,由於 ZK 服務端有若干個服務器,在這種情況下客戶端會嘗試連接其他的服務器。但是在這種情況下,由於 Watcher 沒有被成功設置,因此在重新連接成功後,應當重試剛才的操作,以正確的設置 Watcher。此外,此前已經成功設置的 Watcher 不會受到這種連接移動的影響,這是因為客戶端重連服務端時會將所有 Watcher 重新發送一遍,服務端比對 znode 狀態和 zxid 的相對值,推斷出需要觸發的 Watcher 進行觸發,其他 Watcher 正常設置

小結

ZK 的 Watcher 機制正常流程還是比較順暢的,但是 Watcher 觸發後需要主動再次拉去狀態這一點還是比較麻煩的,而且 ZK 的操作會出現各種各樣詭異的異常。關於 ZK 在網絡延遲或分區的情況下各種異常的處理,會有單獨的一篇文章來介紹。此外,ZK 的源代碼對身體有害,建議除了催吐最好不要閑着沒事去看