從原始碼級別深挖Zookeeper監聽機制
從原始碼級別深挖Zookeeper監聽機制
監聽機制是Zookeeper的一個重要特性,例如:Zookeeper實現的高可用叢集、分散式鎖,就利用到了這一特性。
在Zookeeper被監聽的結點物件/資訊發生了改變,就會觸發監聽機制,通知註冊者。
註冊監聽機制
建立客戶端,建立預設監聽器
在建立zookeeper客戶端例項時,需要下列引數。
new ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)
三個引數分別的含義為:
connectString 服務端地址
sessionTimeout:超時時間
Watcher:監控器
這個 Watcher 將作為整個 ZooKeeper 會話期間的上下文 ,一直被儲存在客戶端 ZKWatchManager 的 defaultWatcher 中,在開啟對某個節點或資訊的監控後,但是並沒有指定額外的監控器,則會預設呼叫這個監控器的方法。
對指定結點進行特殊監聽處理
除此之外,ZooKeeper 客戶端也可以通過 getData、exists 和 getChildren 三個介面來向 ZooKeeper 伺服器註冊 Watcher,從而方便地在不同的情況下新增 Watch 事件:
getData(String path, Watcher watcher, Stat stat)
Zookeeper只能在成功連線上客戶端後,才能使得監控機制起作用;且僅支援4種事件的監聽。
- 結點的增加
- 結點的刪除
- 結點所攜帶資訊的更改
- 結點的子結點的更改
底層原理
Zookeeper監聽機制是觀察者模式實現的。
在觀察者模式中,最重要的一個屬性就是需要一個列表用於儲存觀察者。
而在Zookeeper監聽機制中,也實現了這個一個列表,在客戶端和服務端分別維護了ZKWatchManager
和WatchManager
。
客戶端Watch註冊實現過程
在傳送一個Watch事件的會話請求時,Zookeeper客戶端主要做了兩件事
- 標記該會話是一個帶有 Watch 事件的請求
- 將 Watch 事件儲存到 ZKWatchManager
以 getData 介面為例。當傳送一個帶有 Watch 事件的請求時,客戶端首先會把該會話標記為帶有 Watch 監控的事件請求,之後通過 DataWatchRegistration 類來儲存 watcher 事件和節點的對應關係:
public byte[] getData(final String path, Watcher watcher, Stat stat){
...
WatchRegistration wcb = null;
// 如果watcher不為null,即有watcher物件
if (watcher != null) {
wcb = new DataWatchRegistration(watcher, clientPath);
}
RequestHeader h = new RequestHeader();
// 標記請求為帶有監聽器的
request.setWatch(watcher != null);
...
GetDataResponse response = new GetDataResponse();
ReplyHeader r = cnxn.submitRequest(h, request, response, wcb);
}
之後客戶端向伺服器傳送請求時,是將請求封裝成一個 Packet 物件,並新增到一個等待傳送佇列 outgoingQueue 中:
public Packet queuePacket(RequestHeader h, ReplyHeader r,...) {
Packet packet = null;
...
packet = new Packet(h, r, request, response, watchRegistration);
...
outgoingQueue.add(packet);
...
return packet;
}
最後,ZooKeeper 客戶端就會向伺服器端傳送這個請求,完成請求傳送後。呼叫負責處理伺服器響應的 SendThread 執行緒類中的 readResponse 方法接收服務端的回撥,並在最後執行 finishPacket()方法將 Watch 註冊到 ZKWatchManager 中:
private void finishPacket(Packet p) {
int err = p.replyHeader.getErr();
if (p.watchRegistration != null) {
p.watchRegistration.register(err);
}
...
}
服務端 Watch 註冊實現過程
Zookeeper 服務端處理 Watch 事件基本有 2 個過程:
- 解析收到的請求是否帶有 Watch 註冊事件
- 將對應的 Watch 事件儲存到 WatchManager
服務端 Watch 事件的觸發過程
以 setData 介面即“節點資料內容發生變更”事件為例。
在 setData 方法內部執行完對節點資料的變更後,會呼叫 WatchManager.triggerWatch 方法觸發資料變更事件。
Set<Watcher> triggerWatch(String path, EventType type...) {
WatchedEvent e = new WatchedEvent(type,
KeeperState.SyncConnected, path);
Set<Watcher> watchers;
synchronized (this) {
watchers = watchTable.remove(path);
...
for (Watcher w : watchers) {
Set<String> paths = watch2Paths.get(w);
if (paths != null) {
paths.remove(path);
}
}
}
for (Watcher w : watchers) {
if (supress != null && supress.contains(w)) {
continue;
}
w.process(e);
}
return watchers;
}
watchers
與paths
的關係:
雙向繫結關係。
由於zk的監聽機制是一次性的(觸發即銷燬),當path2觸發了監聽事件後,立馬從watchTable
中銷燬監聽事件,獲取watchers
;並且path2
結點的事件已經出發了,所以也要將每個watcher
對應的paths
中去除path2
;然後呼叫watchers
中每個watcher
的process()
函式完成一次監聽回撥。
客戶端回撥的處理過程
SendThread
此方法是客戶端用於處理服務端的統一請求,replyHdr.getXid()
值為-1時,則響應為通知型別的資訊,最後呼叫eventThread.queueEvent()
將事件交由eventThread
處理。
if (replyHdr.getXid() == -1) {
...
WatcherEvent event = new WatcherEvent();
event.deserialize(bbia, "response");
...
if (chrootPath != null) {
String serverPath = event.getPath();
if(serverPath.compareTo(chrootPath)==0)
event.setPath("/");
...
event.setPath(serverPath.substring(chrootPath.length()));
...
}
WatchedEvent we = new WatchedEvent(event);
...
eventThread.queueEvent( we );
}
EventThread
根據觸發的事件型別,去監聽器列表查詢對應的路徑所對應的監聽器,並統一放到集合result
中,由於Zookeeper事件是一次觸發即銷燬,所以也要從watchManager
中移除監聽器。
public Set<Watcher> materialize(...)
{
Set<Watcher> result = new HashSet<Watcher>();
...
switch (type) {
...
case NodeDataChanged:
case NodeCreated:
synchronized (dataWatches) {
addTo(dataWatches.remove(clientPath), result);
}
synchronized (existWatches) {
addTo(existWatches.remove(clientPath), result);
}
break;
....
}
return result;
}
完成了對監聽器的取出後,將查詢到Watcher
放到對應的waitEvents
任務佇列中,呼叫 EventThread 類中的 run 方法對事件進行處理。
而處理事件,無非就是執行我們註冊事件時,寫下的process()
函式。
總結
Zookeeper的監聽機制是基於觀察者模式設計的。其方式就是通過在客戶端和服務端都維護一張表(zkWatcherManager
、watcherManager
),用於存放監聽器物件。
註冊監聽器過程,就是在呼叫介面的過程,將監聽器進行註冊,首先在本地客戶端進行一個註冊管理,然後傳遞服務端之後,又根據是否含有監聽器,在服務端進行註冊管理。
觸發監聽事件的過程:
- 服務端,通過觸發的路徑
path
,通過watcherManager
找到對應的監聽器集合,通過呼叫process()
方法將資訊傳送至每個監聽器原來的客戶端; - 客戶端,通過判斷是否是通知事件,通過
zkWatcherManager
找到對應的監聽器集合,通過呼叫process()
方法將執行對應的應答處理。
Watcher的客戶端實現和服務端使用不同的實現
當在監聽事件觸發之後,客戶端和服務端幾乎都做了同樣的事(通過Path
找到Watcher
然後執行process()
),但是他們做的事不同的事,服務端的Watcher
的process()
的作用是將path
和觸發的event
傳送至客戶端,然後再次通過path
和event
找到watcher
執行process()
,這時候,執行的程式碼即為開發者所需要執行的監聽事件對應的應答處理process()
。
思考 -> 為什麼Zookeeper要維護兩份Watcher清單(zkWatcherManager + WatcherManager)?
使用反證法,來證明這樣設計的優秀之處。
- 假設只在客戶端維護Watcher清單,當服務端的事件觸發之後,服務端沒有Watcher清單,不知道是哪幾個客戶端訂閱了這個事件,只能將事件傳送給所有的客戶端,既浪費了頻寬,也浪費了客戶端處理響應的資源。
- 假設只在服務端維護Watcher清單,當服務端的事件觸發之後,服務端傳送給訂閱了該事件的客戶端,客戶端確會因為沒有Watcher物件,而無法執行對應的事件應答處理,導致需要服務端將對應的處理方法,通過網路傳遞,則會加重網路的傳輸壓力。
相關文章
- Apache ZooKeeper - 事件監聽機制初探Apache事件
- Spring事件監聽機制原始碼解析Spring事件原始碼
- 原始碼級別的廣播與監聽實現原始碼
- zookeeper原始碼(10)node增刪改查及監聽原始碼
- Zookeeper的基本命令詳解和ACL和watch監聽機制
- 深挖 NPM 機制NPM
- 原始碼級深挖AQS佇列同步器原始碼AQS佇列
- 從原始碼分析Hystrix工作機制原始碼
- 從原始碼解析 Go 的切片型別以及擴容機制原始碼Go型別
- 【spring原始碼】十二、監聽器Spring原始碼
- Spring事件釋出與監聽機制Spring事件
- Spring 事件監聽機制及原理分析Spring事件
- SpringBoot事件監聽器原始碼分析Spring Boot事件原始碼
- flutter原始碼系列 PageView原始碼分析以及監聽事件Flutter原始碼View事件
- spring-event-事件監聽機制實現Spring事件
- 從原始碼全面剖析 React 元件更新機制原始碼React元件
- ZooKeeper Watcher機制
- 從原始碼角度剖析 setContentView() 背後的機制原始碼View
- [原始碼解析] 從TimeoutException看Flink的心跳機制原始碼Exception
- Zookeeper watch機制原理
- Zookeeper--Watch機制
- Spring筆記(7) - Spring的事件和監聽機制Spring筆記事件
- 從原始碼的角度解析Mybatis的會話機制原始碼MyBatis會話
- Android從原始碼角度剖析View事件分發機制Android原始碼View事件
- Runtime 從NullSafe原始碼看訊息轉發 機制Null原始碼
- Zookeeper原始碼分析原始碼
- Halo 開源專案學習(六):事件監聽機制事件
- Zookeeper原始碼分析(二) —– zookeeper日誌原始碼
- Zookeeper原始碼分析(二) ----- zookeeper日誌原始碼
- Qt原始碼解析之-從PIMPL機制到d指標QT原始碼指標
- Zookeeper(2)---節點屬性、監聽和許可權
- Rpc-實現Client對ZooKeeper的服務監聽RPCclient
- ZooKeeper原始碼解讀原始碼
- 淺析Vue原始碼(八)——依賴收集與監聽Vue原始碼
- Zookeeper watcher 事件機制原理剖析事件
- Timer機制原始碼淺析原始碼
- Android AccessibilityService機制原始碼解析Android原始碼
- Dubbo 原始碼分析 - SPI 機制原始碼