淺析ZooKeeper的Watcher機制

Watcher是實現ZooKeeper的發佈/訂閱功能最核心的一個角色。當我們需要對ZooKeeper的某一個節點的變化做出後續處理時,就需要使用到Watcher。

ZooKeeper的Watcher機制,總的來說可以分爲三個流程:client註冊Watcher,server處理Watcher,client回調Watcher。接下來我將從上面三個流程分析Watcher是如何工作的。


Client註冊Watcher

Client中有一個ZKWatcher Manager用來保存所有註冊的Watcher 。

1.使用默認的構造函數註冊:

public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)

這個Watcher 會作爲整個會話期間的默認的Watcher,保存在defaultWathcer。
2.使用getData、exist方法註冊,這裏以getData爲例子進行分析,getData有兩個重載的方法:

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

這個boolean 變量表示是否註冊默認的Watcher

public byte[] getData(final String path, Watcher watcher, Stat stat)

public byte[] getData(final String path, Watcher watcher, Stat stat)
        throws KeeperException, InterruptedException
     {
        final String clientPath = path;
        PathUtils.validatePath(clientPath);

        // the watch contains the un-chroot path
        //WatchRegistration 用來保存Watch和path的對應關係
        WatchRegistration wcb = null;
        if (watcher != null) {
            wcb = new DataWatchRegistration(watcher, clientPath);
        }

        final String serverPath = prependChroot(clientPath);

        RequestHeader h = new RequestHeader();
        h.setType(ZooDefs.OpCode.getData);
        GetDataRequest request = new GetDataRequest();
        request.setPath(serverPath);
        //對於request請求進行標記,表示此請求需要註冊watcher 
        request.setWatch(watcher != null);
        GetDataResponse response = new GetDataResponse();
        //設置ReplyHeader
        ReplyHeader r = cnxn.submitRequest(h, request, response, wcb);
        if (r.getErr() != 0) {
            throw KeeperException.create(KeeperException.Code.get(r.getErr()),
                    clientPath);
        }
        if (stat != null) {
            DataTree.copyStat(response.getStat(), stat);
        }
        return response.getData();
    }

WatchRegistration 用來保存Watch和path的對應關係的。在ZooKeeper中,最小的通信單元是Packet,所以需要把WatchRegistration用Packet進行包裝。包裝成Packet以後,放入隊列中等待發送。

Client發送這個Packet以後,由SendThread線程的readResponse()來接受響應,然後調用finishPacket(packet)方法從packet中提出Wathcer,並且註冊到ZKWatcherManager中。

private void finishPacket(Packet p) {
        if (p.watchRegistration != null) {
            //註冊watcher
            p.watchRegistration.register(p.replyHeader.getErr());
        }

        。。。
    }

WatchRegistration.register方法。

//取Wathcer,並且註冊到ZKWatcherManager中。
public void register(int rc) {
            if (shouldAddWatch(rc)) {
                Map<String, Set<Watcher>> watches = getWatches(rc);
                synchronized(watches) {
                    Set<Watcher> watchers = watches.get(clientPath);
                    if (watchers == null) {
                        watchers = new HashSet<Watcher>();
                        watches.put(clientPath, watchers);
                    }
                    watchers.add(watcher);
                }
            }
        }

服務端處理Watcher

客戶端註冊Watcher ,並不是真正的把完整的Watcher 進行註冊,而僅僅是將RequestHeader和requst兩個屬性進行網絡傳輸。

在服務端進行最後一步請求的處理的時,FinalRequestProcessor.processRequest()方法會判斷此時的請求是否需要註冊Watcher 。

case OpCode.getData: {
                ...
                //判斷此時的請求是否需要註冊Watcher 。
                byte b[] = zks.getZKDatabase().getData(getDataRequest.getPath(), stat,
                        getDataRequest.getWatch() ? cnxn : null);
               ...
            }

如果需要註冊,那麼就把對應了Watcher 的ServerCnxn以及path存儲在WatchManager中。


Watcher 觸發

通過上面兩步,Watcher 以及存在於client以及Server端了,那麼Watcher 如何觸發,以使得client回調呢?

我們進入WatcherManager的triggerWatch方法一探究竟。

public Set<Watcher> triggerWatch(String path, EventType type, Set<Watcher> supress) {
        //封裝WatchedEvent 
        WatchedEvent e = new WatchedEvent(type,
                KeeperState.SyncConnected, path);
        HashSet<Watcher> watchers;
        synchronized (this) {
            watchers = watchTable.remove(path);
            if (watchers == null || watchers.isEmpty()) {
                if (LOG.isTraceEnabled()) {
                    ZooTrace.logTraceMessage(LOG,
                            ZooTrace.EVENT_DELIVERY_TRACE_MASK,
                            "No watchers for " + path);
                }
                return null;
            }
            //根據Path查詢Watcher 
            for (Watcher w : watchers) {
                HashSet<String> paths = watch2Paths.get(w);
                if (paths != null) {
                    //刪除Watcher ,也就是說Watcher 是一次性的
                    paths.remove(path);
                }
            }
        }
        for (Watcher w : watchers) {
            if (supress != null && supress.contains(w)) {
                continue;
            }
            //調用process來真正的觸發Watcher 
            w.process(e);
        }
        return watchers;
    }

我們看一下process()方法做了什麼。

abstract public void process(WatchedEvent event);

watcher的process是一個abstract方法。我們知道ServerCnxn是implemnt了Watcher接口的,而NIOServerCnxn又實現了ServerCnxn。所以看一下NIOServerCnxn中的process

synchronized public void process(WatchedEvent event) {
        //請求頭標-1,表示這是一個通知
        ReplyHeader h = new ReplyHeader(-1, -1L, 0);
        if (LOG.isTraceEnabled()) {
            ZooTrace.logTraceMessage(LOG, ZooTrace.EVENT_DELIVERY_TRACE_MASK,
                                     "Deliver event " + event + " to 0x"
                                     + Long.toHexString(this.sessionId)
                                     + " through " + this);
        }

        // Convert WatchedEvent to a type that can be sent over the wire
        //包裝成WatcherEvent ,以便於用來網絡傳輸
        WatcherEvent e = event.getWrapper();
        //向客戶端發送請求
        sendResponse(h, e, "notification");
    }

由此可知,process並不是真正執行Watcher的業務方法的,而是起到一個通知的作用。真正執行回調業務邏輯的在Client端。


Client端執行回調

Client端是通過ClientCnxn來管理客戶端底層的網絡通信的。而內部類SendThread是用來進行消息的接受以及發送。

SendThread.readRespose(),我截取了其中的一段代碼

if (replyHdr.getXid() == -1) {
                // -1 means notification
                //-1表明這是一個通知,與上面的process對應
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Got notification sessionid:0x"
                        + Long.toHexString(sessionId));
                }
                WatcherEvent event = new WatcherEvent();
                //反序列化成WatcherEvent
                event.deserialize(bbia, "response");

                // convert from a server path to a client path
                //處理chrootPath ,相對路徑變爲絕對路徑
                if (chrootPath != null) {
                    String serverPath = event.getPath();
                    if(serverPath.compareTo(chrootPath)==0)
                        event.setPath("/");
                    else
                        event.setPath(serverPath.substring(chrootPath.length()));
                }
                //WatcherEvent變成WatchedEvent 
                WatchedEvent we = new WatchedEvent(event);
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Got " + we + " for sessionid 0x"
                            + Long.toHexString(sessionId));
                }
                //將WatchedEvent 交給eventThread進行處理
                eventThread.queueEvent( we );
                return;
            }

由此可知,SendThread只是負責接受通知,真正的Watcher執行回調業務是在eventThread中。

EventThread.queueEvent

public void queueEvent(WatchedEvent event) {
            if (event.getType() == EventType.None
                    && sessionState == event.getState()) {
                return;
            }
            sessionState = event.getState();

            // materialize the watchers based on the event
            //找到此WatchedEvent相關所有watchers
            WatcherSetEventPair pair = new WatcherSetEventPair(
                    watcher.materialize(event.getState(), event.getType(),
                            event.getPath()),
                            event);
            // queue the pair (watch set & event) for later processing
            //放入waitingEvents隊列中
            waitingEvents.add(pair);
        }

找到此WatchedEvent相關所有watchers以後,會把watchers放入waitingEvents隊列中,EventThread的run()會不斷從隊列拿出任務執行。

EventThread.run()

@Override
        public void run() {
           try {
              isRunning = true;
              while (true) {
                 Object event = waitingEvents.take();
                 if (event == eventOfDeath) {
                    wasKilled = true;
                 } else {
                     //執行event
                    processEvent(event);
                 }
                 ......
        }

private void processEvent(Object event) {
          try {
              if (event instanceof WatcherSetEventPair) {
                  // each watcher will process the event
                  WatcherSetEventPair pair = (WatcherSetEventPair) event;
                  //找出所有的Watcher,然後一個一個執行process方法
                  //這裏纔是真正的執行了回調的業務邏輯
                  for (Watcher watcher : pair.watchers) {
                      try {
                          watcher.process(pair.event);
                      } catch (Throwable t) {
                          LOG.error("Error while calling watcher ", t);
                      }
                  }
                  ....

流程總結

1.客戶端有一個ZKWatcherManager用來存儲註冊的Watcher
2.客戶端發起一個帶有註冊Watcher的請求以後,首先會把此請求標記爲需要註冊Watcher,然後封裝爲Packet,發送給服務端
3.服務端接收到請求以後,讀取到需要註冊Watcher,那麼就會用一個類似Watcher的ServerCnxt放入WatcherManager中去。
4.當Watcher對應的節點發生了Event事件,服務端就會發送一個通知,告訴客戶端path發生了XXXEvent。
5.客戶端收到通知以後(SendThread來負責接受通知),找出所有的相關的Watcher,幷包裝爲WatcherSetEventPair,放入waiting隊列。
6.客戶端的EventThread執行Run方法,從waiting取出WatcherSetEventPair,找到所有的Watcher進行真正的回調業務的執行。

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