bigywater.tech
ZooKeeper · 100

DataTree 与 WatchManager 源码

上一篇读了选举的源码。这一篇读 watch 的存储(WatchManager)和内存数据树(DataTree)里创建、删除节点的几段代码,每个结论都用真实运行验证:数据结构决定的行为,比如重复注册会怎样、连接断了 watch 去哪了、delete 时 data watch 与 child watch 的通知如何合并。

100 → 1
同一连接对同一路径重复注册 100 次,服务端 watch 数增加 1
5002 → 2
500 条连接 × 10 个 watch 关闭后,zk_watch_count 立即回到基线
34 = 34 B
delete 时有无 child watch,服务端发给该连接的字节相同:只发一个通知

watchTable 与 Set

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,我没有测。)

持久递归 watch

实测 · 本机执行输出
  注册前 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 去重,所以这一项不能说明服务端行为;服务端的验证见第五项)

delete 时的 supress

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:父节点的 cversion 与 pzxid

同一文件,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 的来源。「只有变大才更新」的注释写的是快照回放的场景。

分步演示

读源码 + 验证
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / ZooKeeper 3.9.6(brew 安装的 jar,用 java -cp 直接启动自己的实例,OpenJDK 17 Temurin,JVM 堆 128 MiB)/ 本机回环端口 7300+,数据在临时目录;默认 tickTime=2000;计时用 time.perf_counter,数字每次运行会有小幅抖动。源码来自 Maven Central 的 zookeeper-3.9.6-sources.jar;watch 数取自 mntr 的 zk_watch_count;持久递归 watch 用 Curator 依赖里的 Java 原生客户端(kazoo 没有这个 API);「服务端发给连接的字节」用 TCP 代理统计
    • 没有验证:WatchManagerOptimized(BitHashSet 实现)与 WatcherCleaner 的延迟清理、PERSISTENT(非递归)watch 的行为、DataTree 中 ephemerals / ttls / containers 表的行为、快照回放时 cversion 的比较逻辑
    京ICP备2026067502号-1京公网安备11010502063487号