ZooKeeper Watch 概述
ZooKeeper Watch 机制类似于 Java 设计模式中的观察者模式或者监听模式,唯一的不同是不再基于线程间通信,而是基于进程间通信。
ZooKeeper Watch 机制是指,客户端在所有的读命令上告知服务端:这个节点或者子节点变化时通知我,具体来说,支持的写操作有:
-
getData
-
getChildren
-
exists
例如,我们在命令行可以输入 get -w /foo
,其中 -w
参数就是用于告知 ZooKeeper 服务端,当前客户端想在 /foo
节点上设置一个监听器。
注意事项 :写操作不支持任何形式的 watch 注册。
另一方面,ZooKeeper 支持的事件监听类型与对应的注册方法有:
-
NodeCreated 节点创建:
exits()
-
NodeDataChanged 节点数据改变:
exits()
、getData()
-
NodeDeleted 节点删除:
exits()
、getData()
、getChildren()
-
NodeChildrenChanged 子节点改变:
getChildren()
注意事项:自节点数据的改变并不会引发 NodeChildrenChanged 子节点改变事件。
ZooKeeper Watch 机制的两个细节:
-
wactch 是一次性触发的(除了永久递归 watch),如果客户端需要在一个 watch 通知后继续收到相同节点的 watch 通知,那么必须再次注册 watch 一次
-
服务端发给客户端的 watch 通知并不包含具体的节点数据,其起到的作用非常存粹:告知客户端其关注的节点发生了 watch 事件
关于 ZooKeeper Watch ,我们需要解决如下模型的实现:
模型如下图所示:
-
服务端
-
如何为带有 watch 的读请求进行事件注册;
-
在节点的写操作发送时,如何触发事件,将事件通知发送给客户端;
-
-
客户端
-
命令的发送、注册用与序列化发送;
-
事件的监听与回调;
-
模型如下图所示:
这里的要点是:无论是客户端还是服务端,只有将 Watcher 进行注册,才能在事件发送时进行回调,否则不进行回调。
Watcher实现原理
client端连接后会注册一个事件,然后客户端会保存这个事件,通过zkWatcherManager保存客户端的事件注册,通知服务端Watcher为true,然后服务端会通过WahcerManager会绑定path对应的事件。如下图:
一个Watcher工作主要包含三步:客户端注册watcher、服务端处理watcher和客户端回调watcher事件。
客户端注册watcher
在通过客户端接口调用时,可以指定一个watcher接口,以接受服务器事件的回调。在注册watcher接口后,客户端首先会对当前客户端请求request进行标记,将其设置为"使用watcher监听",同时会封装一个watcher的注册信息watchRegistration对象,用于暂时保存数据数据节点和watcher的对象关系。由于zookeeper中的最小通讯单元为packet,因此,在clientCnxn中watchregistration又会被封装到Packet中,然后放入发送队列中等待客户端发送。随后,客户端会想服务端发送请求,并等待请求返回。完成请求之后,客户端的SendThread线程的readresponse方法负责接收服务端的请求,并将接收到的packet中的watcher注册到ZKWatcherManager中,并最终保存到dataWatchers中,用于服务端事件的回调的时候获取对应节点的对应watcher事件。
用户调用exists/getData/getChildren注册监听以后,会做几个事情
1.将请求数据封装为packet,添加到outgoingQueue队列中
2.SendThread这个线程会执行数据发送操作,主要是将outgoingQueue队列中的数据发送到服务端
3.通过clientCnxnSocket.doTransport(to,pendingQueue,ClientCnxn.this);其中ClientCnxnSocket是zookeeper客户端和服务端的连接通信的封装,有两个具体的实现类ClientCnxnSocketNetty和ClientCnxnSocketNIO;具体使用哪一个类来实现发送,是在初始化过程是在实例化Zookeeper的时候设置的,默认是ClientCnxnSocketNIO这个类
4.基于第3步,最终会在ClientCnxnSocketNetty方法中执行sendPkt将请求的数据包发送到服务端
服务端处理watcher
其实客户端注册的watcher并不会传递到服务端,只是在客户端进行了watcher的保存,所以服务端接受到watch注册事件之后,需要将该事件进行封装,在服务端也进行保存。
ServerCnxn存储
ServerCnxn是服务端与客户端进行网络交互的一个NIO接口,代表了客户端与服务端的连接。其底层采用netty实现。所以,在接受到注册请求之后,服务端会将ServerCnxn对象和数据阶段路径保存到WatchManager的watchTable和watch2Paths中。方便事件触发时的调用。
watcher触发
当指定的节点发生相关的事件时,通过调用WatchManager的triggerWatch方法触发相关的事件。其通过将节点信息和事件类型进行封装成为watchedevent,并查找到到对应节点的注册的watcher,然后分别调用watcher的回调函数process。而在process函数中其实就是通过封装的ServerCnxn向客户端发送watchedevent数据请求。具体的watcher业务处理则在客户端处理。
客户端回调watcher事件
SendThread是客户端开启的与服务端建立TCP长连接的线程,它会一直保持连接状态,采用心跳监测的方式确保与服务端的连接存活。
在客户端的SendThread中,当接收到服务端请求之后,会将请求反序列化成watchedevent对象,并将watchedevent对象转换为watcherevent对象,最后将watcherevent对象添加到EventThread线程,完成对watcher的回调。
EventThread是客户端的一个专门处理watcher时间的线程,其保持了一个待处理事件的队列。它根据传递的事件的类型和节点信息,从客户端的ZKWatcherManager中取出相关的watcher,将其添加到EventThread事件队列中,并在去run方法中不断取出watcher事件进行处理。
这里需要注意是:(1)事件注册是一次性的,因为在每次处理事件之后,就会将相应的watcher注册删除;(2)客户端接收到的服务端watcher事件中并不包含事件更改的具体内容,只是告知发生了这样一个watcher事件,所以,客户端在接收到watcher事件之后,需要在回调函数process中对服务端的数据进行重新获取,才能获得更改的具体内容。
ZooKeeper Watch 机制的特性总结
ZooKeeper 的 Watch 有如下几个特性需要注意:
-
一次性
无论是服务端还是客户端,一旦一个 Watcher 被触发,ZooKeeper都会将其从相应的存储中移除。因此,开发人员在 Watcher 的使用上要记住的一点是需要反复注册。这样的设计有效地减轻了服务端的压力。试想,如果注册一个 Watcher 之后一直有效,那么,针对那些更新非常频繁的节点,服务端会不断地向客户端发送事件通知,这无论对于网络还是服务端性能的影响都非常大。
需要注意的是:ZooKeeper 在 3.6.0 版本中引入了永久 Watcher 机制,利用这个机制可以避免反复注册 Watcher。
-
客户端串行执行
客户端 Watcher 回调的过程是一个串行同步的过程,这为我们保证了顺序,同时,需要开发人员注意的一点是,千万不要因为一个 Watcher 的处理逻辑影响了整个客户端的 Watcher 回调。
-
轻量
WatchedEvent 是 ZooKeeper 整个 Watcher 通知机制的最小通知单元,这个数据结构中只包含三部分内容:通知状态、事件类型和节点路径。也就是说,Watcher 通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。例如针对 NodeDataChanged 事件,ZooKeeper 的 Watcher 只会通知客户端指定数据节点的数据内容发生了变更,而对于原始数据以及变更后的新数据都无法从这个事件中直接获取到,而是需要客户端主动重新去获取数据。这也是ZooKeeper 的 Watcher机制的一个非常重要的特性。
另外,客户端向服务端注册 Watcher 的时候,并不会把客户端真实的 Watcher 对象传递到服务端,仅仅只是在客户端请求中使用 boolean 类型属性进行了标记,同时服务端也仅仅只是保存了当前连接的 ServerCnxn 对象。如此轻量的 Watcher 机制设计,在网络开销和服务端内存开销上都是非常廉价的。