4.3 Watcher:一次性的传票送达 本节摘要:Watcher 是 ZooKeeper 的推送机制:客户端注册、服务端记录、变更发生时送达一张"一次性传票"。本节讲清注册表存放位置、三类事件的触发矩阵、通知先于新数据的顺序保证,以及"只响一次"背后的设计账本。 一纸传票的往返 3.3 节的 NodeCache 之所以好用,是因为它把本节的机制封装严实了。现在掀开封装。Watcher 的完整旅程四步:客户端在读取或判断节点时顺路注册监听;服务端把"哪个会话关注哪个路径的哪类变化"记进本地的监听表;变更发生时,触达该路径的服务器查表、生成事件、推给客户端;客户端回调执行,同时该监听作废。 先纠正一个位置误会:注册表不在 Leader、不在共享存储,而在每个客户端所连接的那台服务器本地。
本节摘要:Watcher 是 ZooKeeper 的推送机制:客户端注册、服务端记录、变更发生时送达一张"一次性传票"。本节讲清注册表存放位置、三类事件的触发矩阵、通知先于新数据的顺序保证,以及"只响一次"背后的设计账本。
3.3 节的 NodeCache 之所以好用,是因为它把本节的机制封装严实了。现在掀开封装。Watcher 的完整旅程四步:客户端在读取或判断节点时顺路注册监听;服务端把"哪个会话关注哪个路径的哪类变化"记进本地的监听表;变更发生时,触达该路径的服务器查表、生成事件、推给客户端;客户端回调执行,同时该监听作废。
先纠正一个位置误会:注册表不在 Leader、不在共享存储,而在每个客户端所连接的那台服务器本地。推论有三:断线重连到另一台时必须全量重注册(Curator 缓存组件的核心职责);某台服务器重启,其上所有监听一并蒸发;监听表不落盘,重启后靠客户端重挂恢复。
按注册时的 API 分三类:getData 与 exists 注册的是数据监听,getChildren 注册的是子节点监听。触发矩阵比背 API 重要——尤其"删除"与"创建"这类容易搞反的方向:
| 注册方式 | 数据变化 | 子节点增减 | 节点被删除 | 节点被创建 |
|---|---|---|---|---|
| getData 监听 | 触发 | 不触发 | 触发 | 不触发 |
| exists 监听 | 触发 | 不触发 | 触发 | 触发 |
| getChildren 监听 | 不触发 | 触发 | 触发 | 不触发 |
矩阵里两处最容易翻车。getData 不监听"创建"——监听一个可能尚不存在的节点要用 exists;getChildren 不监听"数据变化"——盯目录下成员名单用子节点监听,同时想盯某个成员的数据得再挂一层数据监听。5.4 节服务发现里这对组合会同时上场。

Watcher 最值钱的是两条顺序契约。其一,通知先于数据:客户端先收到事件、重读时必得新值,不会出现"通知到了读到的还是旧数据"的倒挂。其二,通知不落后于写确认:写方收到成功回复时,监听方的事件已发出。这两条让"写后通知"可以放心当同步信号用——5.2 节的锁释放、5.1 节的配置下发全靠它。
边界同样要背下来:事件是信号不是数据。通知里只有事件类型与路径,没有变更后的内容,收方必须自己重读;没有重试,重连窗口里错过的事件永久错过,所以连续监听的正确姿势永远是"重挂后全量拉一次",Curator 的缓存在重连后正是这么做的。
用 zkCli 两个终端就能复现全部矩阵。终端 A 注册监听,终端 B 施加变更:
# 终端 A:创建并注册数据监听(get 命令自动附带监听注册) [zkA] create /notice "v1" Created /notice [zkA] get /notice v1 # 终端 B:修改数据 [zkB] set /notice "v2" # 终端 A 立即收到事件,且监听已失效: WATCHER:: WatchedEvent state:SyncConnected type:NodeDataChanged path:/notice [zkA] get /notice # 注意这次 get 没有再挂监听的话,下次变更将收不到通知 v2 # 重读拿到的一定是新值 —— 顺序保证其一
再把子节点监听与 exists 补全,就能把触发矩阵逐格验证:
# 子节点监听:ls 附带注册,对"成员增减"敏感、对"数据变化"免疫 [zkA] ls /services [] [zkB] create /services/inst-1 "up" # A 立即收到 NodeChildrenChanged [zkB] set /services/inst-1 "up2" # A 毫无反应 —— 矩阵中"不触发"格 [zkB] delete /services/inst-1 # A 收到 NodeChildrenChanged # exists 监听:唯一能对"从无到有"触发事件的姿势 [zkA] stat /may-not-exist true # true 即同时注册监听 [zkB] create /may-not-exist "now" # A 立即收到 NodeCreated
问:成百上千个客户端盯同一个节点,会不会把服务端压垮? 会,这就是羊群效应——同一事件触发全员回调与全量重读,第六章 6.2 节专案审理,解法是"只监听自己的前序节点"。
下一个案件:这些传票的收件人——会话本身,如何登记、续租与吊销。
监听不是免费的。每个注册在受理服务器的监听表里占一项,每次触发都要查表、生成事件、序列化推送。三个数量级心里要有数:单台服务器承载的监听总数以十万计还算从容;单节点的监听者数量上千即进入羊群警戒线;事件频率乘以监听者的乘积长期上万则必须按 6.2 节改造。给应用定注册规范时,这三条就是红线。
另有一个少有人提的细节:监听触发不消耗提案。事件由受理节点本地生成推送,不走 ZAB 表决——这意味着通知的送达不受写路径拥堵影响,但也意味着它比写入更"尽力而为"。对通知到达率有苛刻要求的场景,轮询兜底是必要的;对一致性敏感的场景,永远以重读到的值为准,事件只是触发信号。
问:重挂监听时漏掉的事件怎么办? 承认它,然后绕过它。标准做法是"重挂后无条件全量拉取一次当前状态"——Curator 缓存组件的重连逻辑。想靠"记录最后版本号再增量补"需要服务端支持版本流,ZNode 没有这个能力,这是它与 etcd 流式 watch 的本质差距之一(7.3 节)。