ZK源碼解析之Watcher機制

相關類

Watcher 接口

任何一個事件處理類都必須實現Watcher 接口,它有一個內部接口 Event,以及 process方法。前者定義了ZK的狀態事件類型的枚舉,後者則定義了事件的回調方法

@InterfaceAudience.Public
public interface Watcher {
    @InterfaceAudience.Public
    public interface Event {
        @InterfaceAudience.Public
        public enum KeeperState {/*...*/}
        
        @InterfaceAudience.Public
        public enum EventType {/*...*/}
    }
    
    abstract public void process(WatchedEvent event);
}

回調方法 process只有一個參數 WatchedEvent,這個類也很簡單,只有三個參數,分別是通知狀態、事件類型,以及節點路徑。

public class WatchedEvent {
    final private KeeperState keeperState;
    final private EventType eventType;
    private String path;
    /*...*/
}

WatchedEvent 緊密相關的還有 WatcherEvent,兩者都是對ZK服務端事件的封裝,不過後者實現了序列化接口,可用於網絡傳輸。
ZK服務端會將 WatchedEvent 包裝成 WatcherEvent進行傳輸,客戶端則需逆向處理,解包裝成WatchedEvent來處理事件。

可以看到,WatchedEventWatcherEvent 都只有簡單的事件本身的信息,而不包含具體的內容,因此需要客戶端主動去獲取感興趣的最新數據。


工作機制

客戶端註冊

客戶端可以通過 getData()getChildren()exist()三個接口來向服務端註冊 Watcher。註冊的原理都是相同的,這裏僅以 getData() 爲例進行分析。

getData 有兩個重載方法,區別在於第二個參數。

// 使用自定義的 watcher
public byte[] getData(final String path, Watcher watcher, Stat stat)
// 是否使用默認的 watcher
public byte[] getData(String path, boolean watch, Stat stat)

默認的 watcher 在實例化 ZooKeeper 的時候指定:

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

ZooKeeper 實例維護了一個 ZKWatchManager 類的實例,該類實現了ClientWatchManager接口,是客戶端的 watcher 管理器。
默認 watcher 就保存在 ZKWatchManagerdefaultWatcher 中。

private final ZKWatchManager watchManager = new ZKWatchManager();

private static class ZKWatchManager implements ClientWatchManager {
    private final Map<String, Set<Watcher>> dataWatches =
        new HashMap<String, Set<Watcher>>();
    private final Map<String, Set<Watcher>> existWatches =
        new HashMap<String, Set<Watcher>>();
    private final Map<String, Set<Watcher>> childWatches =
        new HashMap<String, Set<Watcher>>();

    private volatile Watcher defaultWatcher;
    /* ... */
}

回到getData,傳遞 watcher 參數後,首先會被封裝成 WatchRegistration 對象,並設置 request 對象爲“使用watcher監聽”:

public byte[] getData(final String path, Watcher watcher, Stat stat)
    throws KeeperException, InterruptedException {
    /* ... */
    WatchRegistration wcb = null;
    if (watcher != null) {
        wcb = new DataWatchRegistration(watcher, clientPath);
    }
    /* ... */
    request.setWatch(watcher != null);
    ReplyHeader r = cnxn.submitRequest(h, request, response, wcb);
    /* ... */
}

然後在客戶端連接對象cnxn(ClientCnxn)的 submitRequest()方法中,又被封裝成 Packet 對象進行網絡傳輸。

(PS:Packet在ZK中可以被看作最小的通信協議單元)

public ReplyHeader submitRequest(RequestHeader h, Record request,
        Record response, WatchRegistration watchRegistration)
        throws InterruptedException {
    /* ... */
    Packet packet = queuePacket(h, r, request, response, null, null, null,
                null, watchRegistration);
    /* ... */
}

發送完請求後,由ClientCnxnSendThread線程的 readResponse()方法負責接收服務端的響應。

void readResponse(ByteBuffer incomingBuffer) throws IOException {
    /* ... */
    finishPacket(packet);
}

finishPacket() 中註冊 watcher 到 ZKWatchManagerdataWatches 中。

private void finishPacket(Packet p) {
    // 註冊 watcher
    // p.replyHeader.getErr() 爲0,則代表服務端響應成功。
    if (p.watchRegistration != null) {
        p.watchRegistration.register(p.replyHeader.getErr());
    }
    /* ... */
}

ZooKeeper.java

public void register(int rc) {
    if (shouldAddWatch(rc)) {
        // 獲取已有的 watcher 映射列表,若無則新增
        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);
        }
    }
}

dataWatches 是一個 Map<String, Set<Watcher>>,保存了節點路徑和watcher的映射關係。

到此,客戶端註冊 Watcher 完畢。稍微總結一下:

  1. 調用客戶端API,傳入 watcher;
  2. 標記 request,封裝 watcher 到 WatcherRegistration;
  3. 向服務端發送 request;
  4. 若響應成功,則註冊 watcher 到 ZKWatcherManager 中進行管理;

值得一提的是,packet 對象在序列化時,並沒有把 watcher 對象也一併序列化,以此降低網絡傳輸的成本,以及服務端的內存壓力。

服務端處理

從客戶端接收到請求後,服務端會在 FinalRequestProcessor.processRequest() 方法中判斷是否需要註冊 watcher:

public void processRequest(Request request) {
    /* ... */
    switch (request.type) {
        /* ... */
        case OpCode.getData: {
            /* ... */
            byte b[] = zks.getZKDatabase().getData(getDataRequest.getPath(), stat,
                    getDataRequest.getWatch() ? cnxn : null);
            rsp = new GetDataResponse(b, stat);
            break;
        }
    }
    /* ... */
}

可以看到,當 getDataRequest.getWatch() 爲 true 時(也就是在客戶端標記的),就會將當前的 ServerCnxn 對象傳入 ZKDatabase.getData()方法中。

在這裏 ServerCnxnWatcher 接口的實現類,可以看做一個 Watcher

public abstract class ServerCnxn implements Stats, Watcher {
    /* ...*/
}

ZKDatabase.getData()

這個類維護了 ZK 的內存數據庫,包括Session、DataTree和committed logs

public byte[] getData(String path, Stat stat, Watcher watcher) 
throws KeeperException.NoNodeException {
    return dataTree.getData(path, stat, watcher);
}

DataTree.getData()

public byte[] getData(String path, Stat stat, Watcher watcher)
        throws KeeperException.NoNodeException {
    DataNode n = nodes.get(path);
    if (n == null) {
        throw new KeeperException.NoNodeException();
    }
    synchronized (n) {
        n.copyStat(stat);
        // 在這裏將 watcher 添加到了 WatcherManager 的 dataWatches 中了
        if (watcher != null) {
            dataWatches.addWatch(path, watcher);
        }
        return n.data;
    }
}

WatchManager.addWatch()

WatchManager 從兩個維度來保存 watcher

public synchronized void addWatch(String path, Watcher watcher) {
    HashSet<Watcher> list = watchTable.get(path);
    if (list == null) {
        // don't waste memory if there are few watches on a node
        // rehash when the 4th entry is added, doubling size thereafter
        // seems like a good compromise
        list = new HashSet<Watcher>(4);
        watchTable.put(path, list);
    }
    list.add(watcher);

    HashSet<String> paths = watch2Paths.get(watcher);
    if (paths == null) {
        // cnxns typically have many watches, so use default cap here
        paths = new HashSet<String>();
        watch2Paths.put(watcher, paths);
    }
    paths.add(path);
}

到這裏就完成了服務端的watcher註冊和存儲了。

再來看看是怎麼觸發的。

前面是註冊了監聽某節點數據變動的watcher,所以當在該節點修改數據時,會觸發NodeDataChanged事件:

DataTree.setData()

public Stat setData(String path, byte data[], int version, long zxid,
        long time) throws KeeperException.NoNodeException {
    Stat s = new Stat();
    DataNode n = nodes.get(path);
    if (n == null) {
        throw new KeeperException.NoNodeException();
    }
    byte lastdata[] = null;
    synchronized (n) {
        lastdata = n.data;
        n.data = data;
        n.stat.setMtime(time);
        n.stat.setMzxid(zxid);
        n.stat.setVersion(version);
        n.copyStat(s);
    }
    // now update if the path is in a quota subtree.
    String lastPrefix;
    if((lastPrefix = getMaxPrefixWithQuota(path)) != null) {
      this.updateBytes(lastPrefix, (data == null ? 0 : data.length)
          - (lastdata == null ? 0 : lastdata.length));
    }
    // 在這裏, 由 WatcherManager 觸發
    dataWatches.triggerWatch(path, EventType.NodeDataChanged);
    return s;
}

WatchManager.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;
        }
        for (Watcher w : watchers) {
            HashSet<String> paths = watch2Paths.get(w);
            if (paths != null) {
                paths.remove(path);
            }
        }
    }
    for (Watcher w : watchers) {
        if (supress != null && supress.contains(w)) {
            continue;
        }
        // 這裏執行 ServerCnxn 的 process() 方法
        w.process(e);
    }
    return watchers;
}

ServerCnxn.process()

@Override
synchronized public void process(WatchedEvent event) {
    // 請求頭的xid(第一個參數)爲“-1”,代表是一個通知
    ReplyHeader h = new ReplyHeader(-1, -1L, 0);
    /* ... */
    // 前面說過,要將 WatchedEvent 包裝成 WatcherEvent
    WatcherEvent e = event.getWrapper();
    sendResponse(h, e, "notification");
}

可以看到,服務端觸發 watcher 的邏輯是比較簡單的。

客戶端回調

在客戶端,由SendThread.readResponse()處理服務端的的響應:

void readResponse(ByteBuffer incomingBuffer) throws IOException {
    /* ... */
    // -1 ,代表是個通知
    if (replyHdr.getXid() == -1) {
        WatcherEvent event = new WatcherEvent();
        // 反序列化
        event.deserialize(bbia, "response");
    
        // ChrootPath 處理
        if (chrootPath != null) {
            String serverPath = event.getPath();
            if(serverPath.compareTo(chrootPath)==0)
                event.setPath("/");
            else if (serverPath.length() > chrootPath.length())
                event.setPath(serverPath.substring(chrootPath.length()));
            else {
                LOG.warn("Got server path " + event.getPath()
                        + " which is too short for chroot path "
                        + chrootPath);
            }
        }
        // 這裏將 WatcherEvent 還原
        WatchedEvent we = new WatchedEvent(event);
        // 交給 EventThread,在下個輪詢週期回調
        eventThread.queueEvent( we );
        return;
    }
    /* ... */
}

EventThread 是ZK中專門用來處理服務端通知事件的線程:

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

    // 根據事件的各種屬性,取出所有watcher
    WatcherSetEventPair pair = new WatcherSetEventPair(
            watcher.materialize(event.getState(), event.getType(),
                    event.getPath()),
                    event);
    // queue the pair (watch set & event) for later processing
    waitingEvents.add(pair);
}

客戶端識別事件的類型,從響應的Watcher存儲中去除watcher,並加到結果中:

public Set<Watcher> materialize(Watcher.Event.KeeperState state,
                                Watcher.Event.EventType type,
                                String clientPath)
{
    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;
}

EventThreadrun()方法中,將會不斷地處理 waitingEvents 隊列中的watcher。

小結

由此,便完成了Watcher機制源碼的簡要分析,從中可以發現Watcher的幾個特性:

  1. 一次性,無論是客戶端還是服務端,一旦觸發一個watcher,就將被移除;
  2. 客戶端串行執行,所有的watcher回調都是在一個隊列中串行執行的,要注意不要因爲一個watcher的處理邏輯影響了整體的回調;
  3. 輕量,服務端不會講事件的具體內容告知客戶端,客戶端註冊watcher的時候也不會發送真實的watcher對象。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章