18

ZooKeeper原理|ZooKeeper状态变化应对法

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzU5OTQ1MDEzMA%3D%3D&%3Bmid=2247487529&%3Bidx=1&%3Bsn=1764ef20b72dfeed5580069115a03499
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文基于 ZooKeeper 3.6.0 版本介绍应对状态变化的方法。

ZooKeeper 的常见用途包括发布配置信息、进行服务发现和协同分布式过程,这些用途都要求应用程序能够知晓 ZooKeeper 集合的状态。为了达到这个目的, ZooKeeper 客户端可以定时轮询 ZooKeeper 集合以获取其状态。然而,轮询方式并非最佳的状态感知方式。对于频繁变化的状态,轮询可能会错过某些状态变化;对于偶尔变化的状态,轮询可能会导致不必要的开销。

基于这样的观察,ZooKeeper 提供了 Watcher 机制在不使用轮询的前提下应对状态变化。这个机制允许应用程序在特定的 ZNode 上注册 Watcher 以在 ZNode 的状态发生变化的时候收到通知。

应用 Watcher 来应对状态变化的框架通常形如以下的框架代码。

zk.exist("/myZnode", myWatcher, existsCallback, ctx);

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

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

这里我们设置了两个回调逻辑。 一个是 ZooKeeper 客户端异步请求的回调,这里是 exist 请求。 我们将会看到只有特性种类的请求可以进行 Watcher 的设置。 另一个就是 Watcher 的回调,处理接收到的 WatchedEvent 对象。 记住这里是两个不同的回调逻辑,一个在客户端请求完成是被调用,一个在监视 znode 状态发生变化是被调用。 这在后面讨论错误处理时会有帮助。

可以看到,在 Watcher 中我们主要处理的是 WatchedEvent 对象,它是一个包括 KeeperState 和 EventType 两个字段的数据对象。我们从分类的角度来看需要处理的 WatchedEvent 对象都有哪些可能。

从 KeeperState 的角度来说,总共包含这几种会话状态。

  • SyncConnected

  • ConnectedReadOnly

  • Disconnected

  • Expired

  • Closed

  • AuthFailed

  • SaslAuthFailed

所有的会话状态从名字即可看出其内容。除了 SyncConnected 之外,所有的 KeeperState 类型都会对应到 EventType.None 事件类型,这代表没有节点状态变化时间发生,而是会话状态发生了变化。 ZooKeeper 使用了相同的 Watcher 机制来处理应用程序相关事件的通知。这是某种程度的重载,在简化了类别区分和工程实现的同时也增加了用户需要了解的知识。

SyncConnected 状态代表会话正常,除了第一次连接上 ZooKeeper 时有一个 EventType.None 事件类型的通知,其后都会与其他事件类型相关联。

  • NodeCreated

  • NodeDeleted

  • NodeDataChanged

  • NodeChildrenChanged

  • DataWatchRemove

  • ChildWatchRemoved

  • PersistentWatchRemoved

其中最后三种事件对应 3.5 和 3.6 版本以后支持的 Watcher 移除功能和永久 Watcher 对象的移除功能。具体的内容将在后续展开。

前四种事件从名字即可看出其内容,分别代表节点被创建、删除、改变内容或节点的子节点发生改变。这里有三个点需要注意。

第一个,所有的事件在实现上都是一个单纯的不带信息的枚举。换句话说, ZooKeeper 为状态变化产生的时间仅仅代表事件已发生。特别是对于 NodeChildrenChanged 事件,仅代表节点的子节点发生改变,到底是新增子节点、子节点删除还是子节点数据变化,都不清楚。这就要求 ZooKeeper 客户端在收到事件时必须主动再次发出请求查询节点的状态或数据内容。

这不同于 etcd 中带有变更内容的事件。如果通知中包含变更内容,我们就有可能在客户端仅接受一次通知,即在本地维护数据缓存的状态,从而不需要再次请求查询节点。

这又引出第二个点。即使我们在通知中包含变更内容,由于 ZooKeeper 广为人知的单次触发语义,即设置了 Watcher 在出现状态变化时触发一次并移除,这会导致此后发生的事件不再被监视。即使 ZooKeeper 客户端在收到事件后重新设置 Watcher 监视,由于网络传输天然的异步性质,仍然有可能错过事件。

ZooKeeper 的实践书籍《ZooKeeper 分布式过程协同技术详解》中,为这一点开脱时提到,既然每次都需要重新拉取状态,那么单次触发能够在事件频发的情况下减少事件产生的通知数量。但是实践当中这几乎不成为一个好处,而从用户角度来说却要麻烦地注意更多的异步情况。 ZooKeeper 3.6.0 版本引入了 Persistent (Recursive) Watcher 类型,能够支持 Watcher 多次触发。在下文中,我们会穿插着讨论其内容。

第三个,Watcher 在服务器分为 data/child 两类,在客户端对其注册分为 exist/data/child 三类。

这里的具体差别在于,DataWatcher 仅监视确切路径对应的节点,在节点创建、删除或者数据更改时产生通知,而 ChildWatcher 只有在节点的子节点发生变化时产生通知。从客户端的角度,exist 请求能够对任意路径进行 Watcher 的设置,而 getData 和 getChildren 只能对已经存在的节点进行 Watcher 的设置。

在服务器一侧,DataWatcher 和 ChildWatcher 由不同的集合管理,它们会响应不同的事件。在移除 Watcher 时,可以指定 Watcher 的类型是 DataWatcher 还是 ChildWatcher 又或者不做区分来移除。另外,上面提到的 PersistentWatcher 首先属于 DataWatcher 类型,如果设置了 Recursive 参数,则同时还是 ChildWatcher 类型。

接下来,我们针对 Watcher 的设置到触发的整个生命周期做介绍,同时对生命周期中可能遇到的异常进行阐述。

Watcher 的设置从 ZooKeeper 客户端开始,具体的分类不再做阐述,主要追踪代码的调用路径。

以 exist 为例,用户传入的 Watcher 对象或创建此客户端时设置的默认 Watcher 被使用时,有两个代码路径被激活。

其之一是发送到服务器的请求的 watch 字段将被设置为 true 以代表这次请求包含一个 Watcher 设置的请求。服务端的请求我们稍后展开。

其之二是 Watcher 被包装在 ExistsWatchRegistration 中。经过网络层面的等候,在设置 Watcher 的请求成功时,调用链进入ClientCnxn#finishPacket 方法中,调用 ExistsWatchRegistration 的 register 方法在客户端缓存 Watcher 的信息。

缓存的 Watcher 信息主要用于支持 Watcher 在网络错误的情况下重新设置以及 Watcher 的移除。

前者具体来说,是在发生 ConnectionLoss 异常时,在 ZooKeeper 客户端成功重新连接到服务器时,根据自己缓存的 Watcher 信息,向服务器发送一个 SetWatches 请求以重新设置此前还未触发的 Watcher 集合。这个功能减少了用户在 Watcher 中响应 ConnectionLoss 异常并重新设置 Watcher 的负担,尤其是在此种情况下无法预知节点发生了何种变化。在 ZooKeeper 服务器一侧,会比对 SetWatches 请求的各个 Watcher 对应路径节点的信息,根据是否存在节点以及节点最后处理的事务 ID 和子节点事务 ID 来判断是否应该产生事件。

这里有两个需要注意的点,一个是服务器判断是否应该产生事件存在 False Negative 误判的情况。具体来说,在通过 exist 设置 Watcher 时,对应节点如果此前不存在,对应 Watcher 将被分类为 SetWatches 请求中的 existWatcher 类型。此时,如果该节点经历了创建后又删除的事件,则无法被 Watcher 所感知。这也是分布式系统常见问题中所谓的 ABA 问题。另一个是 SetWatchers 包含的 Watcher 集合是设置 Watcher 的成功请求的 Watcher 集合。在前文我们提过,如果使用异步的 ZooKeeper 客户端接口,这里实际上有两个回调。虽然 ZooKeeper 能够自动处理已经注册上的 Watcher 的容错,但是如果异步请求本身没有成功,则无法被这个自动重设机制覆盖。在这种情况下,应用程序应该自行重新执行请求和设置 Watcher 的操作。

关于 Watcher 的移除,这是在 3.5.0 引入的新功能。在此前的 ZooKeeper 版本中,Watcher 一旦设置就无法主动移除,Watcher 的移除只有两种途径,一是 Watcher 被触发,二是会话超时或被关闭。应用程序在某些情况下希望根据外部状态的变化主动移除 Watcher,尤其是 Watcher 数量巨大,而外部状态指示这些 Watcher 再也不可能被触发的情况下防止内存泄漏的场景。移除 Watcher 指令确定移除集合也依赖于前文提到的本地缓存信息。同时需要定义新的客户端和服务器的通信文本,在成功执行时将产生前文所提的几种 WatchRemoved 事件。

现在我们来看服务器处理 Watcher 的逻辑。

前面提到,在客户端调用接口是设置 Watcher 的场景下,网络请求包中的 watch 字段将被设置。这一信息将在服务器被处理。

具体来说,经过一系列序列化和请求处理责任链的检查和装饰后,设置 Watcher 的请求最终被发往 ZKDatabase 并委托给 DataTree 处理。DataTree 是服务器管理节点视图的类。在进行请求响应的调用时,会根据请求中 watch 字段的布尔值来判断是否要设置一个服务器一侧的 Watcher 对象。

服务器一侧的 Watcher 对象实际上是一个 ServerCnxn 的实例,它继承自 Watcher 接口。当然,从实现角度来说这样的继承有点勉强,更好的实现方式是采用组合以明确 ServerCnxn 本身不是 Watcher 接口,而是代行 Watcher 的职责。具体来说,ServerCnxn 是服务器一侧维护的网络连接对象,DataTree 在处理设置 Watcher 的请求时会将路径和 ServerCnxn 这个 Watcher 相关联,并且在 DataTree 的内部维护一系列的 Watcher 对象的信息。在处理节点创建、删除或数据改变等操作时,根据节点视图和 Watcher 信息判断应该触发哪些 Watcher,而 Watcher 的触发正是执行 ServerCnxn 的 process 方法的逻辑,即向客户端发送相应的事件。

DataTree 触发相应事件将委托到服务器一侧的 WatchManager 处理,WatcherManager 在事件产生时调用 triggerWatch 方法来调用 ServerCnxn 的 process 方法。同时对于单次触发的 Watcher 对象,将其从 Watcher 集合中移除,而对于 Persistent 的 Watcher 对象,则进行保留。

关于 Persistent Watcher,最后还有几个需要讨论的点,具体可以参考 JIRA ticket[1][2]和 GitHub PR[3]上的讨论。

第一个,Persistent Watcher 具有 Recursive 和非 Recursive 的选项,Recursive 将同时监听给定路径对应的节点及其子节点,否则只监听给定路径对应的节点。对于监听子节点的情况,为了减少每次递归查找的负担,实现上有一个 PathParentIterator 的类来迭代获取变更节点的父节点的优化。

第二个,Persistent Watcher 对于前文和 etcd 做对比的情况,仅解除了单次触发的限制,能够对多个状态变化对应的产生事件。但是,事件的内容仍然仅是事件已发生,而不包括事件的具体变更内容。客户端在收到事件后仍然要重新请求获取节点状态。为了追溯变更内容,通常来说需要引入某种 MVCC 的存储。

第三个,Persistent Watcher 和前文所提的 Watcher 自动容忍网络错误有一定的冲突。在具体的实现中,发生网络错误并重设 Watcher 集合的时候,仅仅把 Persistent Watcher 重新设置,而不会检测是否需要触发期间错过的事件。这并没有什么功能上的考量或者优点,仅仅是实现上会更复杂而其需求来源 Curator 能够主动的处理这个问题。

[1] https://issues.apache.org/jira/browse/ZOOKEEPER-153 [2] https://issues.apache.org/jira/browse/ZOOKEEPER-1416 [3] https://github.com/apache/zookeeper/pull/1106


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK