Zookeeper提供了分布式数据的发布/订阅功能,多个订阅者同时监听某一个主题对象,当这个主题对象自身状态变化时,会通知所有订阅者,使它们作出相应的处理,而ZooKeeper实现这一功能的根本就是Watcher机制。
ZooKeeper的Watcher机制主要包括客户端线程、客户端WatchManager和ZooKeeper服务器三部分。具体的流程主要是客户端向ZooKeeper服务器注册Watcher的同时,会将Watcher对象存储在客户端的WatchManager中。当ZooKeeper服务器端触发Watcher事件后,会向客户端发送通知,客户端线程从WatchManager中取出对应的Watcher对象来执行回调逻辑。
1.客户端注册Watcher
首先,注册一个Watcher对象可以通过创建一个ZooKeeper客户端对象实例时或者使用ZooKeeper客户端的getData、getChildren和exist三个接口来向ZooKeeper服务器注册Watcher。在注册Wacher后,客户端首先会将当前客户端请求request进行标记,将其设置为Watcher监听,同时会封装一个Watcher的注册信息WatchRegistration对象,用于暂时保存数据节点路径和Watcher的对应关系。
在ZooKeeper中,Packet可以看作一个最小通信协议单元,用于进行客户端与服务端之间的网络传输,任何需要传输的对象都需要包装成一个Packet对象。在ZooKeeper源码中,当使用异步操作时,都会出现一行代码:
cnxn.queuePacket(h, new ReplyHeader(), request, null, cb, clientPath,
serverPath, ctx, null);
这说明操作结束之后会调用cnxn对象的queuePacket方法:
Packet queuePacket(RequestHeader h, ReplyHeader r, Record request,
Record response, AsyncCallback cb, String clientPath,
String serverPath, Object ctx, WatchRegistration watchRegistration)
{
Packet packet = null;
synchronized (outgoingQueue) {
if (h.getType() != OpCode.ping && h.getType() != OpCode.auth) {
h.setXid(getXid());
}
packet = new Packet(h, r, request, response, null,
watchRegistration);
packet.cb = cb;
packet.ctx = ctx;
packet.clientPath = clientPath;
packet.serverPath = serverPath;
if (!zooKeeper.state.isAlive() || closing) {
conLossPacket(packet);
} else {
// If the client is asking to close the session then
// mark as closing
if (h.getType() == OpCode.closeSession) {
closing = true;
}
outgoingQueue.add(packet);
}
}
sendThread.wakeup();
return packet;
}
从以上可以看出通过这个方法,WatchRegistration会被封装到Packet中,然后放入发送队列中等待客户端发送。随后,ZooKeeper客户端就会向服务端发送这个请求,同时等待请求的返回。这时,激活了客户端的sendThreand线程,该线程的readResponse方法负责接收来自服务端的响应,然后将Watcher注册到ZKWatchManager中进行管理。这里需要注意的是,在底层实际的网络传输序列化过程中,并没有将WatchRegistration对象完全地序列化到底层字节数组中去。这样可以减轻服务端资源开销以及网络传输开销。
2.服务端处理Watcher
Watcher触发
1.封装WatchedEvent
首先将通知状态、事件类型以及节点路径封装成一个WatchedEvent。
2.查询Watcher
根据数据节点的节点路径从watchTable中取出对应的Watcher。如果没有找到Watcher,说明没有任何客户端在该数据节点上注册过Watcher,直接退出。如果找到了Watcher,将其提取出来,同时会直接从watchTable和watch2Paths中将其删除。
注意:WatchManager是ZooKeeper服务端Watcher的管理者,其内部管理的watchTable和watch2Paths两个存储结构,其中watchTable是从数据节点路径的粒度来托管Watcher。watch2Paths是从Watcher的粒度来控制事件触发需要触发的数据节点。
3.调用process方法来触发Watcher
依次调用步骤2中找出的所有Watcher的process方法,这是ServerCnxn对应的process方法,而该process方法中,主要工作如下:
a.在请求头中标记"-1",表明当前是一个通知。
b.将WatchedEvent包装成WatcherEvent,以便进行网络传输序列化。
c.向客户端发送该通知。
因此可以看出,该方法并不是处理客户端Watcher真正的业务逻辑,而是借助当前客户端连接的ServerCnxn对象来实现对客户端的WatchedEvent传递,真正的客户端Watcher回调与业务逻辑执行都在客户端。
注意:以上服务端只是给客户端发送一个通知,内容仅包括说明这是什么引起的Watcher。而WatchedEvent会被包装成可序列化的WatcherEvent,可以在网络中进行传输。
3.客户端回调Watcher
SendThread接收事件通知
void readResponse() throws IOException {
ByteBufferInputStream bbis = new ByteBufferInputStream(
incomingBuffer);
BinaryInputArchive bbia = BinaryInputArchive.getArchive(bbis);
ReplyHeader replyHdr = new ReplyHeader();
replyHdr.deserialize(bbia, "header");
if (replyHdr.getXid() == -2) {
// -2 is the xid for pings
if (LOG.isDebugEnabled()) {
LOG.debug("Got ping response for sessionid: 0x"
+ Long.toHexString(sessionId)
+ " after "
+ ((System.nanoTime() - lastPingSentNs) / 1000000)
+ "ms");
}
return;
}
if (replyHdr.getXid() == -4) {
// -4 is the xid for AuthPacket
if(replyHdr.getErr() == KeeperException.Code.AUTHFAILED.intValue()) {
zooKeeper.state = States.AUTH_FAILED;
eventThread.queueEvent( new WatchedEvent(Watcher.Event.EventType.None,
Watcher.Event.KeeperState.AuthFailed, null) );
}
if (LOG.isDebugEnabled()) {
LOG.debug("Got auth sessionid:0x"
+ Long.toHexString(sessionId));
}
return;
}
if (replyHdr.getXid() == -1) {
// -1 means notification
if (LOG.isDebugEnabled()) {
LOG.debug("Got notification sessionid:0x"
+ Long.toHexString(sessionId));
}
WatcherEvent event = new WatcherEvent();
event.deserialize(bbia, "response");
// convert from a server path to a client path
if (chrootPath != null) {
String serverPath = event.getPath();
if(serverPath.compareTo(chrootPath)==0)
event.setPath("/");
else
event.setPath(serverPath.substring(chrootPath.length()));
}
WatchedEvent we = new WatchedEvent(event);
if (LOG.isDebugEnabled()) {
LOG.debug("Got " + we + " for sessionid 0x"
+ Long.toHexString(sessionId));
}
eventThread.queueEvent( we );
return;
}
if (pendingQueue.size() == 0) {
throw new IOException("Nothing in the queue, but got "
+ replyHdr.getXid());
}
Packet packet = null;
synchronized (pendingQueue) {
packet = pendingQueue.remove();
}
/*
* Since requests are processed in order, we better get a response
* to the first request!
*/
try {
if (packet.header.getXid() != replyHdr.getXid()) {
packet.replyHeader.setErr(
KeeperException.Code.CONNECTIONLOSS.intValue());
throw new IOException("Xid out of order. Got "
+ replyHdr.getXid() + " expected "
+ packet.header.getXid());
}
packet.replyHeader.setXid(replyHdr.getXid());
packet.replyHeader.setErr(replyHdr.getErr());
packet.replyHeader.setZxid(replyHdr.getZxid());
if (replyHdr.getZxid() > 0) {
lastZxid = replyHdr.getZxid();
}
if (packet.response != null && replyHdr.getErr() == 0) {
packet.response.deserialize(bbia, "response");
}
if (LOG.isDebugEnabled()) {
LOG.debug("Reading reply sessionid:0x"
+ Long.toHexString(sessionId) + ", packet:: " + packet);
}
} finally {
finishPacket(packet);
}
}
SendThread的response()方法负责接收这个客户端事件通知。首先会对replyHdr中xid进行判断,如果是-1,则说明是一个通知类型的响应。作以下处理:
1.反序列化:ZooKeeper客户端接到请求后,首先将字节流转换成WatcherEvent对象。
2.处理chrootPath:如果客户端设置了chrootPath属性,那么需要对服务端传过来的完整的节点路径进行chrootPath处理,生成客户端的一个相对节点路径。
3.还原WatchedEvent:将WatcherEvent对象转换成WatchedEvent。
4.回调Watcher:将WatchedEvent对象交给EventThread线程,在下一个轮询周期中进行Watcher回调。
EventThread处理事件通知
SendThread通过调用EventThread.queueEvent方法将事件传给EventThread线程。queueEvent方法会根据通知事件,从ZKWatchManager中取出所有相关的watcher,并将其放入waitingEvents队列中,进行串行同步处理。
4.总结
了解了Watcher机制的工作机制后,有以下几点总结比较重要。
1.Watcher具有一次性,无论是服务端还是客户端,一旦一个Watcher被触发,ZooKeeper都会将其从相应的存储中移除。因此Watcher需要反复注册。
2.客户端串行执行:最终Watcher会被放入一个队列中串行执行。
3.WatchedEvent是ZooKeeper整个Watcher通知机制的最小单元,结构只包含:通知状态、事件类型和节点路径。Watcher只会通知客户端发生的事件,不会说明事件的具体内容。具体内容的逻辑都在客户端进行回调获取。同时,客户端向服务端注册Watcher的时候,并不会把真实的Watcher对象传递给服务端,仅仅只是在客户端请求中使用boolean类型属性进行了标记,同时服务端也仅仅保存了当前连接的ServerCnxn对象。这样设计大大减轻了网络开销和服务端内存开销。
4.WatchedEvent需要先转换为WatcherEvent序列化进行网络传输,然后客户端获取之后要反序列化然后再转换成WatchedEvent进行后续逻辑操作。
原文链接:https://blog.csdn.net/carson0408/article/details/84142079