zookeeper的watcher是一次性的嗎?!

zookeeper

zookeeper 是流行的高性能分佈式協調工具,它提供了分佈式環境中需要的命名服務,配置管理,分佈式鎖,註冊中心,Leader 選舉等等功能,應用十分廣泛。

zookeeper 的 watcher

Client 可以在 zookeeper 的節點(znode)上設置監聽器(watcher),當節點被修改時,zookeeper 會向客戶端發送一個相應的通知。 可以通過 getData()getChildren()exists() 三個方法來設置 watcher。以 getData() 的方法簽名爲例:

public byte[] getData(String path, boolean watch, Stat stat);

方法的第二個參數便表示是否在當前 path 上註冊 watcher

watcher 流程

watcher 的註冊與觸發流程大致如下: Watcher Flow

爲了高性能考慮,zookeeperwatcher 被設計成很輕量級。上圖 Client 與 Server 交互過程中,Client 並沒有把 watcher 實例傳給 Server 端,而只是設置一個標識位告訴它要監聽某個節點的變化。同理,Server 端節點發生變化的時候,只能簡單“通知” Client 變化的事件,而具體發生了什麼變化,則需要 Client 自己去 Server 端再次獲取。

watcher 是一次性的嗎?

zookeeper 有過簡單瞭解的同學,估計都會認爲 watcher 是一次性的,包括官網都是這麼介紹 watcher 的:

One-time trigger: One watch event will be sent to the client when the data has changed. For example, if a client does a getData("/znode1", true) and later the data for /znode1 is changed or deleted, the client will get a watch event for /znode1. If /znode1 changes again, no watch event will be sent unless the client has done another read that sets a new watch.

是的,普通的 watcher 確實是一次性的,當事件被觸發之後,所對應的 watcher 會被立馬刪除。如此設計的原因,依然是出於性能考慮(咱 zookeeper 是一個高性能的分佈式協調工具!)。試想,如果 Server 端的每次數據變更都通知 Client,在更新非常頻繁的情況下會影響性能。同時,Client 其實並不一定想知道每次數據更新,更多的是需要知道最新的數據。 不過如果 Client 想繼續監聽節點變化,如何註冊一個永久的 watcher 呢?zookeeper 提供的方案是,在後續如 getData() 獲取 Server 端最新數據的時候再註冊一遍 watcher即可(同時這也是 Curator 添加永久 wathcher 的做法),這也是沒有單獨提供一個 addWatcher() 方法而需要與 getData() 綁定使用的原因。 所以 zookeeper 這麼設計是情有可原的。

持久遞歸監聽器(Persistent Recursive Watch)

在上文中我們知道,出於性能考慮,zookeeperwatcher 設計爲一次性的,如果需要繼續監聽節點變化,則需要調用如 getData() 重新註冊。然而在 zookeeper 3.6.0 中新增瞭如下方法:

private void addPersistentWatches(String clientPath, Set<Watcher> result);

官網的介紹如下:

New in 3.6.0: Clients can also set permanent, recursive watches on a znode that are not removed when triggered and that trigger for changes on the registered znode as well as any children znodes recursively.

也就是說,在 3.6.0 版本之後,zookeeper 支持設置永久的 watcher 了!來看看這個永久遞歸監聽器的特性:

  1. 支持 watcher 所支持的所有語義:CHILDRENDATAEXISTS
  2. 可以在任意節點上添加 CHILDRENDATA 遞歸監聽
  3. 可以在任意路徑上添加 EXISTS 遞歸監聽
  4. 添加一個遞歸監聽器相當於在其所有子節點上添加 watcher
  5. 跟普通的 watcher 一樣,當遞歸監聽器的某個子節點事件被觸發之後,在對其調用 getData() 之前,不會再次觸發事件。
  6. 遞歸監聽器會覆蓋其子節點上的所有普通 watcher

我們主要看第5條,舉兩個例子,假如我們在節點 /a 上添加了永久遞歸監聽器

情況一:

  • /a/a 變更時,將會觸發監聽器
  • /a/a 再次變更時,不會觸發監聽器
  • Client 調用 get(/a/a) 之後,該子節點上的監聽器被“自動重置”
  • /a/a 再次變更時,又會觸發監聽器

情況二:

  • /a/a 變更時,將會觸發監聽器
  • /a/b 變更時,將會觸發監聽器
  • Client 調用 get(/a/a) 之後,該子節點上的監聽器被“自動重置”
  • /a/b 再次變更時,不觸發監聽器
  • /a/a 再次變更時,又會觸發監聽器

不難看出,永久遞歸監聽器雖然是永久的,但它與普通的 watcher 設計理念其實是一致的,也就是說,它保證了在事件觸發Client 調用 getData() 獲取數據 兩個動作之間,沒有中間事件,依然能保持 zookeeper 的高性能。 其實是因爲該監聽器是遞歸的,所以將它設置成是永久的。作者權衡過兩種實現方式:

  1. 給節點的子節點分別添加一個 watcher
  2. 只在目標節點添加一個永久的 watcher,然後將已經觸發的子節點事件記錄在一個黑名單(blacklist)中

結果是採用了而第二種方式,因爲它所需的內存更小。而在 Server 端監聽器的重置,實際上只是將對應的子節點路徑從黑名單中移除。

Curator 的支持

當然,Curator 也在 5.0.0 版本中添加了對持久遞歸監聽器的支持:

/**
 * client - curator client
 * basePath - 目標節點路徑
 * recursive - 遞歸與否
 **/
public PersistentWatcher(CuratorFramework client, String basePath, boolean recursive);

總結

爲了實現高性能的目的,zookeeperwatcher 被設計成是輕量級,一次性的。如果客戶端需要持續訂閱某個節點的狀態,需要每次在 getData() 方法裏面重複註冊 watcher。 在 3.6.0 版本之後,zookeeper 提供了永久遞歸 watcher,它是永久的。但其行爲跟普通的 watcher 一致,即在通知發送給 ClientClient 調用 getData() 之間,不會有中間事件觸發。其原理其實是,在 Client 調用 getData() 的時候,在 Server 端自動將該 watcher 重新註冊了。

參考

Persistent Recursive Watch

ZooKeeper Watches

Persistent Recursive Watcher

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章