上一篇读了选举的源码。这一篇读 watch 的存储(WatchManager)和内存数据树(DataTree)里创建、删除节点的几段代码,每个结论都用真实运行验证:数据结构决定的行为,比如重复注册会怎样、连接断了 watch 去哪了、delete 时 data watch 与 child watch 的通知如何合并。
org/apache/zookeeper/server/watch/WatchManager.java(3.9.6,addWatch 节选) Set<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<>(4); ... list.add(watcher); Map<String, WatchStats> paths = watch2Paths.get(watcher); if (paths == null) { // cnxns typically have many watches, so use default cap here paths = new HashMap<>(); watch2Paths.put(watcher, paths);
两张表互为反向索引:从路径找 watcher(触发时用),从 watcher 找路径(连接关闭时用)。路径对应的是 HashSet,所以重复添加同一个 watcher 不会增加条目。
同一连接对 /w 注册 100 次 exists watch:zk_watch_count 0 → 1 另一条连接注册 1 次:→ 2 同一连接再对 /w 注册 get_children(child watch):→ 3
DataTree 里有 dataWatches 和 childWatches 两个独立实例(创建于 WatchManagerFactory.createWatchManager(),默认类是 WatchManager)。watch 数量对内存的影响就是这两个表的大小。
同一文件,triggerWatch 节选 if (!pathParentIterator.atParentPath()) { watchers.add(watcher); WatchStats newStats = stats.removeMode(WatcherMode.STANDARD); if (newStats == WatchStats.NONE) { iterator.remove(); paths.remove(localPath); } else if (newStats != stats) { paths.put(localPath, newStats); } } else if (stats.hasMode(WatcherMode.PERSISTENT_RECURSIVE)) {
触发前 zk_watch_count=4,set 之后=3,回调收到 ['CHANGED']
所以「一次性」不是客户端约定,而是服务端触发时就从表里删掉。而持久 watch 保留(下面)。
基线 2 → 500 条连接注册后 5002 500 次 close() 本身用了 1.6 s;全部返回后第一次读到的 zk_watch_count = 2,0 ms 内回到 2(基线 2)
清理由 removeWatcher(Watcher) 完成;验证显示关闭返回后没有残留。(默认的 WatchManager 之外还有 WatchManagerOptimized,它会延迟清理已死 watcher,我没有测。)
注册前 zk_watch_count=0,注册后 zk_watch_count=1
一连串变化(建 3 层、改 5 次、删 1 次)共收到 9 个事件,按类型:{NodeCreated=3, NodeDataChanged=5, NodeDeleted=1}
前几个事件:[NodeCreated /p/a, NodeCreated /p/a/b, NodeCreated /p/a/b/c, NodeDataChanged /p/a/b/c, NodeDataChanged /p/a/b/c]
全部触发之后 zk_watch_count=1(没有被移除)这个 API 是 ZooKeeper 3.6 加入的(addWatch),常见的 Python 客户端 kazoo 没有提供。
exists(/d, w) + getChildren(/d, w)(同一个 Watcher 对象):zk_watch_count 1 → 3 delete /d 后该连接收到的事件:[NodeDeleted /d](客户端库自己也会对同一个 Watcher 去重,所以这一项不能说明服务端行为;服务端的验证见第五项)
org/apache/zookeeper/server/DataTree.java(3.9.6,deleteNode 节选) WatcherOrBitSet processed = dataWatches.triggerWatch(path, EventType.NodeDeleted, zxid, acl); childWatches.triggerWatch(path, EventType.NodeDeleted, zxid, acl, processed); childWatches.triggerWatch("".equals(parentName) ? "/" : parentName, EventType.NodeChildrenChanged, zxid, parentAcl);
delete /x 后服务端发给 A(data+child 两种 watch)的字节 34,发给 B(只有 data watch)的字节 34(相等 → 只发了一个 NodeDeleted 通知,与 supress 一致)
同一条连接如果既有 data watch 又有 child watch,只会收到一次 NodeDeleted。这个行为是在服务端合并的,不依赖客户端去重。
同一文件,createNode 节选 if (parentCVersion > parent.stat.getCversion()) { parent.stat.setCversion(parentCVersion); parent.stat.setPzxid(zxid); } ... dataWatches.triggerWatch(path, Event.EventType.NodeCreated, zxid, acl); childWatches.triggerWatch(parentName.equals("") ? "/" : parentName, Event.EventType.NodeChildrenChanged, zxid, parentAcl);
创建前 cversion=0 pzxid=0x3fa;创建 /par/c 后 cversion=1 pzxid=0x3fb(子节点 czxid=0x3fb);删除后 cversion=2 pzxid=0x3fc 创建后 pzxid == 子节点 czxid:True;cversion 每次 +1:True
这就是 stat 里 cversion 与 pzxid 的来源。「只有变大才更新」的注释写的是快照回放的场景。