ZK Watcher 的原理和实现

什么是 ZK Watcher

基于 ZK 的应用程序的一个常见需求是需要知道 ZK 集合的状态。为了达到这个目的,一种方法是 ZK 客户端定时轮询 ZK 集合,检查系统状态是否发生了变化。然而,轮询并不是一种高效的方式,尤其是在状态变化的发生频率很低的时候

因此,ZK 提供了一种通过通知客户端感兴趣的具体时间来避免轮询造成的性能问题的方式,即设置 Watcher 的方式。通过设置 Watcher,ZK 客户端可以对指定的 znode 注册一个通知请求,在 znode 发生变化时收到一个单次的通知。例如,在 znode 被删除时向 Watcher 发送节点被删除的通知

应用 ZK Watcher 的代码通常遵循如下的框架

zk.exists("myZnode", myWatcher, existsCallback, null);

Watcher myWatcher new Watcher() {
  public void process(WatchedEvent event) {
    // process the watch event
  }
}

StatCallback existsCallback = new StatCallback() {
  public void processResult(int rc, String path, Object ctx, Stat stat) {
    // process the result of the exists call
  }
}

上面的代码框架中以 exists 操作为例,展示了异步调用 ZK 操作并注册 Watcher 的一般用法

WatchedEvent 的分类

Watcher 的使用一个重要的内容就是了解 Watcher 如何设置以及何时触发,并不是所有的 ZK 操作都可以设置 Watcher,Watcher 也不是会被所有事件触发

抛开被重载的连接状态的 WatchedEvent,业务过程中会遇到的 WatchedEvent 分为以下几种

  • NodeCreated - 可以通过 exists 调用设置 Watcher,在 znode 从无到有创建的时候被触发
  • NodeDeleted - 可以通过 exists 或者 getData 调用设置 Watcher,在 znode 被删除时触发
  • NodeDataChanged - 可以通过 exists 或者 getData 调用设置 Watcher,在 znode 数据发生变化时触发
  • NodeChildrenChanged - 可以通过 getChildren 调用设置 Watcher,在 znode 的直接子节点创建或删除时触发
  • DataWatchRemoved - 在 exists 或者 getData 设置的 Watcher 被删除时触发对应的 Watcher
  • ChildWatchRemoved - 在 getChildren 设置的 Watcher 被删除时触发对应的 Watcher

可以看到,只有 exists 和 getData 和 getChildren 三种操作能够设置 Watcher

注意,getData 创建的 Watcher 不会接收到 NodeCreated 事件,这是因为 getData 在节点不存在的时候会抛出 KeeperException.NoNodeException 异常,而不会设置 Watcher

Watcher 机制的实现与生命周期

从应用程序的角度来讲,注册完 Watcher 之后只要等待事件被触发即可,无需关心 ZK 是怎么实现这个过程的。不过了解 ZK 的具体实现机制有助于我们在面对错误或者异常的时候更好的理解问题的出处以及针对性的排查问题

Watcher 机制的实现最重要的问题就是 Watcher 究竟是注册在哪里的,以及 Watcher 究竟是如何触发的。这两个问题很难分开来解释,因此下文会一并分析

原本讲解原理的部分最好是结合对应的源代码摘要来讲解,但是 ZK 的源码实在是难以阅读,贴在这里不但不能帮助理解,恐怕会让读者更加一头雾水。我会从伪代码的粒度介绍代码逻辑并附上对应的源文件位置,有兴趣的同学可以自行阅读,祝身体健康

Watcher 机制的实现要从注册讲起,ZK 客户端在执行 exists 或 getData 或 getChildren 操作的时候,可以设置一个自定义的 Watcher 或者通过 flag 复用创建客户端时设置的 Watcher。后者实践中比较少用,不做过多介绍。这个 Watcher 会被打包成 Packet 放进 ClientCnxnEventThread 中,在对应的操作完成时登记到客户端的 Watches 集合里。在服务端,对应的 GetDataRequest 等请求有一个是否设置了 Watcher 的 flag,服务端由此来判断是否要设置相应的 Watcher。这里,ZK 为 ServerCnxn 实现了 Watcher 接口。ServerCnxn 是每个服务端上对于客户端的连接对象,它的 process(WatchedEvent) 方法就是将对应的 WatchedEvent 打包为 WatcherEvent 然后发送给客户端

Watcher 成功设置后需要关心的就是 Watcher 的触发了,本质上 Watcher 是在 ZK 集合发生状态变化的时候在客户端回调对应处理逻辑的。但是 ZK 集合发生状态变化要以服务端的状态为准,服务器维护了 ZK 集合的状态,这主要是由 ZKDatabaseDataTree 来实现的。当服务器判断发生了需要出发 Watcher 的状态变化时,服务器会遍历异动节点上对应的 Watcher,在这里就是对应的客户端连接,回调它们的 process(WatchedEvent) 方法。如上所述,这就向客户端发送了一个对应的 WatchedEvent

上面介绍的 Watcher 注册和触发的过程实际上就囊括了 Watcher 的整个生命周期,即 Watcher 的生命周期由对应操作在客服端成功时开始,到触发后结束。也就是说,Watcher 是单次触发的,触发之后还想再次监听对应节点的状态需要重新设置 Watcher。Watcher 的生命周期结束还有另一个触发条件,即 session 被关闭或过期。此外,在 3.5.0 之后的版本中,ZK 能够主动执行 removeWatches 操作来移除不再感兴趣的节点。

Watcher 的错误处理

如前所述,Watcher 是一种轻量级的相应变化的通知机制。由于其功能简单,在实际应用当中为了构建更加复杂的语义,我们需要对 Watcher 在一些故障条件下的响应做对应的讨论。

其中第一个是单次触发和 WatchedEvent 携带的信息带来的问题。由于 Watcher 是单次触发的,所以我们可能会丢掉在前一个 Watcher 触发后到后一个 Watcher 重新设置之前的事件。通常来说这不是问题,因为 ZK 的目标是实现一个分布式环境下对状态达成共识的存储,而不保证每个事件都被客户端记录和处理。重新设置 Watcher 时附带的动作足以保证我们同步了当时的最新状态。因此,我们虽然漏掉了事件,但是那充其量只是一个中间状态,ZK 提供的保证是关于一段时间内的最终状态的。但是换个角度讲,由于 WatchedEvent 只包含了【事件发生了】这个信息,所以任何新的状态都需要重新从 ZK 集合上获取,这是 ZK 为了实现的简单在当初做的一个 trade-off

其中第二个是关于 CONNECTIONLOSS 异常的。严格来说这并不是 Watcher 应该关心的事情,因为操作由于 CONNECTIONLOSS 失败时 Watcher 是无法被成功设置上去的。CONNECTIONLOSS 异常意味着客户端和正在连接的服务器断开连接,由于 ZK 服务端有若干个服务器,在这种情况下客户端会尝试连接其他的服务器。但是在这种情况下,由于 Watcher 没有被成功设置,因此在重新连接成功后,应当重试刚才的操作,以正确的设置 Watcher。此外,此前已经成功设置的 Watcher 不会受到这种连接移动的影响,这是因为客户端重连服务端时会将所有 Watcher 重新发送一遍,服务端比对 znode 状态和 zxid 的相对值,推断出需要触发的 Watcher 进行触发,其他 Watcher 正常设置

小结

ZK 的 Watcher 机制正常流程还是比较顺畅的,但是 Watcher 触发后需要主动再次拉去状态这一点还是比较麻烦的,而且 ZK 的操作会出现各种各样诡异的异常。关于 ZK 在网络延迟或分区的情况下各种异常的处理,会有单独的一篇文章来介绍。此外,ZK 的源代码对身体有害,建议除了催吐最好不要闲着没事去看

你可能感兴趣的:(ZK Watcher 的原理和实现)