Watcher机制

应的处理。在ZooKeeper中,引入了Watcher机制来实现这种分布式的通知功能。ZooKeeper允许客户端向服务端注册一个Watcher监听,当服务端的一些指定事件触发了这个Watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能。整个Watcher注册与通知过程如下图:

zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_服务端

ZooKeeper的Watcher机制主要包括客户端线程、客户端WatchManager和ZooKeeper 服务器三部分。

在具体工作流程上,简单地讲,客户端在向ZooKeeper服务器注册Watcher的同时,会将Watcher对象存储在客户端的WatchManager中。当ZooKeeper服务器端触发Watcher 事件后,会向客户端发送通知,客户端线程从WatchManager中取出对应的Watcher对象来执行回调逻辑。

Watcher接口:

在ZooKeeper中,接口类Watcher用于表示一个标准的事件处理器,其定义了事件通知相关的逻辑,包含KeeperState和EventType两个枚举类,分别代表了通知状态和事件类型,同时定义了事件的回调方法: process (WatchedEvent event)

Watcher事件:

zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_Watcher机制_02

NodeChildrenChanged事件会在数据节点的子节点列表发生变更的时候被触发,这里说的子节点列表变化特指子节点个数和组成情况的变更,即新增子节点或删除子节点,而子节点内容的变化是不会触发这个事件的。

对于AuthFailed这个事件,需要注意的地方是,它的触发条件并不是简简单单因为当前客户端会话没有权限,而是授权失败。

//使用正确的Scheme进行授权
zkClient = new ZooKeeper(SERVER_LIST, 3000, new Sample_ AuthFailed1());
zkClient.addAuthInfo("digest","taokeeper:true".getBytes());
zkClient.create("/zk- book", "".getBytes(), acls, CreateMode.EPHEMERAL );
zkClient_error = new ZooKeeper(SERVER_ LIST, 3000, new Sample_AuthFailed1());
zkClient_ error.addAuthInfo("digest", "taokeeper:error".getBytes());
zkClient_ error.getData("/zk- book", true, null);

//使用错误的Scheme进行授权
zkClient = new ZooKeeper(SERVER_ LIST, 3000, new Sample_ AuthFailed2());
zkClient.addAuthInfo("digest", "taokeeper:true".getBytes());
zkClient.create("/zk-book", “".getBytes(), acls, CreateMode.EPHEMERAL);
zkClient_error = new ZooKeeper(SERVER_ LIST, 3000, new Sample_AuthFailed2());
zkClient_error.addAuthInfo("digest2",, "taokeeper :error" . getBytes());
zkClient_error.getData("/zk- book", true, null);

上面两个示例程序都创建了一个受到权限控制的数据节点,然后使用了不同的权限Scheme进行权限检查。在第一个示例程序中,使用了正确的权限Scheme: digest;而第二个示例程序中使用了错误的Scheme: digest2。

另外,无论哪个程序,都使用了错误的Auth: taokeeper:error, 因此在运行第一个程序的时候,会抛出NoAuthException异常,而第二个程序运行后,抛出的是AuthFailedException异常,同时,会收到对应的Watcher事件通知: (AuthFailed, None)。

回调方法process()
process方法是Watcher 接口中的一个回调方法,当ZooKeeper 向客户端发送一个Watcher事件通知时,客户端就会对相应的process方法进行回调,从而实现对事件的处理。process方法的定义如下:

abstract public void process(WatchedEnvent event);

WatchedEvent包含了每一个事件的三个基本属性:通知状态(keeperState)、 事件类型(eventType) 和节点路径(path),其数据结构如图下所示。ZooKeeper 使用WatchedEvent对象来封装服务端事件并传递给Watcher,从而方便回调方法process对服务端事件进行处理。

zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_服务端_03

提到WatchedEvent,不得不讲下WatcherEvent实体。笼统地讲,两者表示的是同一个事物,都是对一个服务端事件的封装。不同的是,WatchedEvent是一个逻辑事件,用于服务端和客户端程序执行过程中所需的逻辑对象,而WatcherEvent因为实现了序列化接口,因此可以用于网络传输,其数据结构如下图所示。

zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_客户端_04

服务端在生成WatchedEvent事件之后,会调用getWrapper方法将自己包装成一个可序列化的WatcherEvent事件,以便通过网络传输到客户端。客户端在接收到服务端的这个事件对象后,首先会将WatcherEvent事件还原成一个WatchedEvent事件,并传递给process方法处理,回调方法process根据入参就能够解析出完整的服务端事件了。

需要注意的一点是,无论是WatchedEvent还是WatcherEvent,其对ZooKeeper服务端事件的封装都是极其简单的。举个例子来说,当/zk-book这个节点的数据发生变更时,服务端会发送给客户端一个“ZNode 数据内容变更”事件,客户端只能够接收到如下信息:

KeeperState: SyncConnected
EventType: NodeDataChanged
Path: /zk-book

客户端无法直接从该事件中获取到对应数据节点的原始数据内容以及变更后的新数据内容,而是需要客户端再次主动去重新获取数据一这也是ZooKeeperWatcher机制的一个非常重要的特性。

工作机制:

Zookeeper的Watcher机制,总的说可以概括为以下三个过程:

  • 客户端注册Watcher
  • 服务端处理Watcher
  • 客户端回调Watcher

zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_Zookeeper_05

客户端注册Watcher:

在创建一个Zookeeper客户端对象实例时,可以向构造方法中传入一个默认的Watcher:

public Zookeeper(String connectString, int sessionTimeout, Watcher watcher);

这个Watcher将作为整个ZooKeeper会话期间的默认Watcher,会一直被保存在客户端ZKWatchManager的defaultWatcher 中。另外,ZooKeeper 客户端也可以通过getData. getChildren和exist三个接口来向ZooKeeper服务器注册Watcher, 无论使用哪种方式,注册Watcher的工作原理都是一致的,这里我们以getData这个接口为例来说明。

getData接口用于获取指定节点的数据内容,主要有两个方法:

public byte[] getData(String path, boolean watch, Stat stat);
public byte[] getData(final String path, Watcher watcher, Stat stat)

在这两个接口,上都可以进行Watcher的注册,第一个接口通过一个boolean参数来标识是否使用上文中提到的默认Watcher来进行注册,具体的注册逻辑和第二个接口是一致的。

在向getData接口注册Watcher后,客户端首先会对当前客户端请求request进行标记,将其设置为“使用Watcher监听”,同时会封装一个Watcher的注册信息WatchRegistration 对象,用于暂时保存数据节点的路径和Watcher的对应关系,具体的逻辑代码如下:

zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_Zookeeper_06

在ZooKeeper中,Packet可以被看作一个最小的通信协议单元,用于进行客户端与服务端之间的网络传输,任何需要传输的对象都需要包装成一个Packet对象。因此,在ClientCnxn中WatchRegistration又会被封装到Packet中去,然后放入发送队,列中等待客户端发送:

zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_Watcher机制_07


随后,ZooKeeper客户端就会向服务端发送这个请求,同时等待请求的返回。完成请求发送后,会由客户端SendThread 线程的readResponse 方法负责接收来自服务端的响应,fini shPacket方法会从Packet中取出对应的Watcher 并注册到ZKWatchManager中去:

zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_Zookeeper_08


从上面的内容中,我们已经了解到客户端已经将Watcher暂时封装在了WatchRegistration 对象中,现在就需要从这个封装对象中再次提取出Watcher来:

zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_数据_09

在register方法中,客户端会将之前暂时保存的Watcher对象转交给ZKWatchManager,并最终保存到dataWatches 中去。ZKWatchManager.dataWatches 是一个Map<String, Set<Watcher> > 类型的数据结构, 用于将数据节点的路 径和Watcher 对象进行一一映射后管理起来。整个客户端Watcher的注册流程如下图所示:

zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_服务端_10

我们可以发现,极端情况下,客户端每调用一次getData()接口,就会注册上一个Watcher,那么这些Watcher实体都会随着客户端请求被发送到服务端去吗?

答案是否定的。如果客户端注册的所有Watcher 都被传递到服务端的话,那么服务端肯定会出现内存紧张或其他性能问题了,幸运的是,在ZooKeeper的设计中充分考虑到了这个问题。在上面的流程中,我们]提到把WatchRegistration封装到了Packet 对象中去,但事实上,在底层实际的网络传输序列化过程中,并没有将WatchRegistration对象完全地序列化到底层字节数组中去。为了证实这一点,我们可以看下Packet内部的序列化过程:

zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_客户端_11

从上面的代码片段中,我们可以看到,在Packet.createBB()方法中,ZooKeeper只会将requestHeader 和request 两个属性进行序列化,也就是说,尽管WatchRegistration被封装在了Packet 中,但是并没有被序列化到底层字节数组中去,因此也就不会进行网络传输了。

服务端处理Watcher:

ServerCnxn存储:

服务端处理Watcher的序列图如下:

zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_Zookeeper_12

服务端在收到来自客户端的请求之后,在FinalRequestProcessor.processRequest()中会判断当前请求是否需要注册Watcher:

zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_客户端_13

从getData请求的处理逻辑中,可以看到,当getDataRequest.getWatch()为true, ZooKeeper就认为当前客户端请求需要进行Watcher 注册,于是就会将当前的ServerCnxn 对象和数据节点路径传入getData 方法中去

那么为什么要传入ServerCnxn呢? ServerCnxn 是一个 ZooKeeper客户端和服务器之间的连接接口,代表了一个客户端和服务器的连接。

ServerCnxn 接口的默认实现是NIOServerCnxn,同时从3.4.0版本开始,引入了基于Netty的实现: NettyServerCnxn。 无论采用哪种实现方式,都实现了Watcher的process接口,因此我们可以把ServerCnxn看作是一个Watcher对象。数据节点的节点路径和ServerCnxn最终会被存储在WatchManager的watchTable和watch2Paths中。

WatchManager是ZooKeeper服务端Watcher的管理者,其内部管理的watchTable 和watch2Paths两个存储结构,分别从两个维度对Watcher进行存储。

  • watchTable是从数据节点路径的粒度来托管Watcher。
  • watch2Paths是从Watcher的粒度来控制事件触发需要触发的数据节点。

同时,WatchManager 还负责Watcher事件的触发,并移除那些已经被触发的Watcher。注意, WatchManager只是一个统称, 在服务端,DataTree中会托管两个WatchManager,分别是dataWatches和childWatches,分别对应数据变更Watcher和子节点变更Watcher。在本例中,因为是getData接口,因此最终会被存储在dataWatches中,其数据结构如下图所示:

zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_Watcher机制_14

Watcher触发:

NodeDataChanged事件的触发条件是“Watcher监听的对应数据节点的数据内容发生变更”,其具体实现如下:

zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_Watcher机制_15


zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_服务端_16

在对指定节点进行数据更新后,通过调用WatchManager的triggerWatch 方法来触发相关的事件:

zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_服务端_17

无论是dataWatches还是childWatches 管理器,Watcher 的触发逻辑都是一致的,基本步骤如下:

  1. 封装WatchedEvent 首先将通知状态( KeeperState)、事件类型(EventType) 以及节点路径(Path)封装成一个WatchedEvent对象。
  2. 查询Watcher 根据数据节点的节点路径从watchTable中取出对应的Watcher。如果没有找到Watcher,说明没有任何客户端在该数据节点上注册过Watcher,直接退出。而如果找到了这个Watcher,会将其提取出来,同时会直接从watchTable和watch2Paths中将其删除——从这里我们也可以看出,Watcher在服务端是一次性的,即触发一次就失效了。
  3. 调用process方法来触发Watcher 在这一步中,会逐个依次地调用从步骤2中找出的所有Watcher的process方法。那么这里的process 方法究竟做了些什么呢?在上文中我们已经提到,对于需要注册Watcher的请求,ZooKeeper会把当前请求对应的ServerCnxn作为一个Watcher进行存储,因此,这里调用的process方法,事实上就是ServerCnxn的对应方法:

    在process()方法中,主要逻辑如下:
  • 在请求头中标记"-1",表明当前是一个通知
  • 将WatchedEvent包装成WatcherEvent,以便进行网络传输序列化
  • 向客户端发送该通知

从以上几个步骤中可以看到,ServerCnxn的process方法中的逻辑非常简单,本质上并不是处理客户端Watcher真正的业务逻辑,而是借助当前客户端连接的ServerCnxn对象来实现对客户端的WatchedEvent传递,真正的客户端Watcher回调与业务逻辑执行都在客户端。

客户端回调Watcher:

SendThread接收事件通知:

zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_数据_18


对于一个来自服务端的响应,客户端都是由SendThread.readResponse(ByteBuffer incomingBuffer) 方法来统一进行处理的,如果响应头replyHdr中标识了XID 为-1,表明这是一个通知类型的响应,对其的处理大体上分为以下4个主要步骤。

  1. 反序列化 ZooKeeper客户端接到请求后,首先会将字节流转换成WatcherEvent对象。
  2. 处理chrootPath 如果客户端设置了chrootPath属性,那么需要对服务端传过来的完整的节点路径进行chrootPath处理,生成客户端的-一个相对节点路径。例如客户端设置了chrootPath为/appl,那么针对服务端传过来的响应包含的节点路径为/app/locks ,经过chrootPath处理后,就会变成-一个相对路径:/locks。关于ZooKeeper的chrootPath,将在7.3.2节中做详细讲解。
  3. 还原WatchedEvent process接口的参数定义是WatchedEvent,因此这里需要将WatcherEvent 对象转换成WatchedEvent。
  4. 回调Watcher 最后将WatchedEvent对象交给EventThread线程,在下一个轮询周期中进行Watcher回调。

EventThread处理事件通知:

EventThread线程是ZooKeeper客户端中专门用来处理服务端通知事件的线程,其数据结构如下图所示:

zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_客户端_19


zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_数据_20

SendThread 接收到服务端的通知事件后,会通过调用EventThread.queueEvent方法将事件传给EventThread线程,其逻辑如下:

zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_服务端_21


queueEvent方法首先会根据该通知事件,从ZKWatchManager中取出所有相关的Watcher:

zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_Zookeeper_22

客户端在识别出事件类型EventType 后,会从相应的Watcher 存储( 即dataWatches、existWatches 或childWatches中的一个或多个,本例中就是从dataWatches和existWatches两个存储中获取)中去除对应的Watcher。注意,此处使用的是remove接口,因此也表明了客户端的Watcher机制同样也是一次性的,即一旦被触发后,该Watcher就失效了

获取到相关的所有Watcher之后,会将其放入waitingEvents这个队列中去。

WaitingEvents是一个待处理Watcher的队列,EventThread的run方法会不断对该队列进行处理:

zookeeper leader故障后会重新按照规则进行选举 zookeeper state change_数据_23

EventThread线程每次都会从waiting Events队列中取出一一个Watcher,并进行串行同步处理。注意,此处processEvent方法中的Watcher才是之前客户端真正注册的Watcher,调用其process方法就可以实现Watcher的回调了。

Watcher特性总结
  • 一次性 无论是服务端还是客户端,一旦一个Watcher 被触发,ZooKeeper都会将其从相应的存储中移除。因此,开发人员在Watcher的使用,上要记住的一点是需要反复注册。这样的设计有效地减轻了服务端的压力。试想,如果注册一个Watcher之后-一直有效,那么,针对那些更新非常频繁的节点,服务端会不断地向客户端发送事件通知,这无论对于网络还是服务端性能的影响都非常大。
  • 客户端串行执行 客户端Watcher回调的过程是一个串行同步的过程,这为我们保证了顺序,同时,需要开发人员注意的一点是, 千万不要因为一个Watcher的处理逻辑影响了整个客户端的Watcher回调。
  • 轻量 WatchedEvent是ZooKeeper整个Watcher通知机制的最小通知单元,这个数据结构中只包含三部分内容:通知状态、事件类型和节点路径。也就是说,Watcher通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。
    例如针对NodeDataChanged事件, ZooKeeper的Watcher只会通知客户端指定数据节点的数据内容发生了变更,而对于原始数据以及变更后的新数据都无法从这个事件中直接获取到,而是需要客户端主动重新去获取数据一这也是ZooKeeper的Watcher机制的一个非常重要的特性。
    另外,客户端向服务端注册Watcher的时候,并不会把客户端真实的Watcher对象“传递到服务端,仅仅只是在客户端请求中使用boolean类型属性进行了标记,同时服务端也仅仅只是保存了当前连接的ServerCnxn对象
    如此轻量的Watcher机制设计,在网络开销和服务端内存开销上都是非常廉价的。