Watch机制与其源码分析
- Watch机制
- 简单使用
- 底层机制源码分析
- 客户端请求逻辑
- 服务器
- 客户端回调逻辑
- 使用场景
- 参考
Watch机制
之前的内容介绍了节点的内部属性,如节点里面存储了什么信息,以及对节点的访问控制。那么接下来介绍节点的watch机制。
watch机制,顾名思义是一个监听机制。主要可以通过exists、getData和getChildren启用。
监听的对象是事件,那么事件主要有以下几种:
事件 | 解释 |
创建 | 创建节点事件,通过调用exists启用 |
删除 | 创建节点,通过调用exists、getData和getChildren启用 |
修改 | 创建节点通过调用exists和getData启用 |
子事件 | 创建节点,通过getChildren启用 |
这个监听机制需要注意以下几点:
- 单次有效
例如,在客户端1创建了一个监听事件对node1,在node1进行修改时,会通知客户端1,但是下次再修改的时候,不会再通知客户端1了,因为watch单次有效。如果需要再次监听需要再次注册。 - 顺序回调
是为了防止网络时延对客户端的影响。ZooKeeper客户端库保证所有事件都会按顺序分发;客户端会保障它在看到相应的znode的新数据之前接收到watch事件。从ZooKeeper接收到的watch事件顺序一定和ZooKeeper服务所看到的事件顺序是一致的。 - 两个监听列表
Znode改变有很多种方式,例如:节点创建,节点删除,节点改变,子节点改变等等。Zookeeper维护了两个Watch列表,一个节点数据Watch列表,另一个是子节点Watch列表。getData()和exists()设置数据Watch,getChildren()设置子节点Watch。所以可以存在两个watch同时监听当前节点或子节点。
需要注意的是,在ZooKeeper3.6以后,允许在NodeCreated、NodeDeleted和NodeDataChanged事件上创建触发后不会被移除的watch。并且,可以选择递归注册watch,也就是从当前znode开始所有znode都注册watch。
对于一个已经创建的watch,可以对其进行删除。即使客户端没有连接服务器,也是可以删除watch的,即在本地标志位设置位true来删除。
那么还有一个问题:如果客户端和服务器之间断开了连接会发生什么呢?
根据文档以及接下来的代码可以知道:与服务器断开连接时(例如,当服务器发生故障时),在重新建立连接之前,无法获得任何监视。
简单使用
仍然使用上一节的树形结构,不过由于重启了ZooKeeper,所以图上所有的临时节点都已经不存在了,可以进行验证:
[zk: 127.0.0.1:2181(CONNECTED) 8] ls /locks
[]
可以看到重启客户端后,之前客户端的临时节点都被删除了。
接下来使用命令行注册一个watch在之前的持久节点/work上,/work下面还有三个持久节点(上一篇的容器节点)/work/zs-work、/work/ls-work、/work/lw-work
# 在/work上设置一个watch,注意可以先输入printwatches命令看watch输出有没有被打开
[zk: 127.0.0.1:2181(CONNECTED) 6] ls -w /work
[ls-work, lw-work, zs-work]
接下来删除/work/zs-work
# 删除/work/zs-work
[zk: 127.0.0.1:2181(CONNECTED) 7] delete /work/zs-work
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/work
可以看到监听事件被触发了,此时我们再创建zs-work
[zk: 127.0.0.1:2181(CONNECTED) 9] create -c /work/zs-work
Created /work/zs-work
创建成功,而且也没有监听了,因为监听仅单次生效。
底层机制源码分析
源码来自版本ZooKeeper 3.8.0
客户端请求逻辑
首先,客户端需要将带watch的请求传给服务器。
/**
* 版本:ZooKeeper 3.8.0
* org.apache.zookeeper
* ZooKeeper.java
* 1952行
*/
public byte[] getData(final String path, Watcher watcher, Stat stat) throws KeeperException, InterruptedException {
final String clientPath = path;
PathUtils.validatePath(clientPath);
// 在客户端注册一个watch
// the watch contains the un-chroot path
WatchRegistration wcb = null;
if (watcher != null) {
wcb = new DataWatchRegistration(watcher, clientPath);
}
final String serverPath = prependChroot(clientPath);
// 装配request
RequestHeader h = new RequestHeader();
h.setType(ZooDefs.OpCode.getData);
GetDataRequest request = new GetDataRequest();
request.setPath(serverPath);
request.setWatch(watcher != null);
GetDataResponse response = new GetDataResponse();
// submit请求
ReplyHeader r = cnxn.submitRequest(h, request, response, wcb);
// 处理返回逻辑....
return response.getData();
}
可以看到,客户端调用了函数submitRequest()来submit此次请求,此函数的逻辑是:
/**
* 版本:ZooKeeper 3.8.0
* org.apache.zookeeper
* ClientCnxn.java
* 1557行
*/
public ReplyHeader submitRequest(
RequestHeader h,
Record request,
Record response,
WatchRegistration watchRegistration) throws InterruptedException {
return submitRequest(h, request, response, watchRegistration, null);
}
public ReplyHeader submitRequest(
RequestHeader h,
Record request,
Record response,
WatchRegistration watchRegistration,
WatchDeregistration watchDeregistration) throws InterruptedException {
ReplyHeader r = new ReplyHeader();
// 调用queuePacket
Packet packet = queuePacket(
h,
r,
request,
response,
null,
null,
null,
null,
watchRegistration,
watchDeregistration);
//处理请求超时
//.....
if (r.getErr() == Code.REQUESTTIMEOUT.intValue()) {/*处理请求失败*/}
return r;
}
/**
* 版本:ZooKeeper 3.8.0
* org.apache.zookeeper
* ClientCnxn.java
* 1648行
*/
public Packet queuePacket(
RequestHeader h,
ReplyHeader r,
Record request,
Record response,
AsyncCallback cb,
String clientPath,
String serverPath,
Object ctx,
WatchRegistration watchRegistration,
WatchDeregistration watchDeregistration) {
Packet packet = null;
packet = new Packet(h, r, request, response, watchRegistration);
// 装配packet
// ....
synchronized (state) {
if (!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中
outgoingQueue.add(packet);
}
}
// sendThread线程去做具体的发包
sendThread.getClientCnxnSocket().packetAdded();
return packet;
}
在请求发送完成后,才能将Watch注册到ZKWatchManager中:
/**
* 版本:ZooKeeper 3.8.0
* org.apache.zookeeper
* ClientCnxn.java
* 737行
*/
protected void finishPacket(Packet p) {
int err = p.replyHeader.getErr();
if (p.watchRegistration != null) {
// 将watch注册
p.watchRegistration.register(err);
}
// 其他逻辑
// ........
}
服务器
服务端将带有watch的请求发给服务器之后,拿getData举例,会首先进入操作代码判断模块,一个swich-case判断操作类型是getData并进入相应逻辑:
/**
* 版本:ZooKeeper 3.8.0
* org.apache.zookeeper.server
* FinalRequestProcessor.java
* 378行
*/
public void processRequest(Request request) {
// .....
// 判断请求的类型
switch (request.type){
//.....
// 操作类型为getDate的逻辑
case OpCode.getData: {
lastOp = "GETD";
GetDataRequest getDataRequest = new GetDataRequest();
ByteBufferInputStream.byteBuffer2Record(request.request, getDataRequest);
path = getDataRequest.getPath();
rsp = handleGetDataRequest(getDataRequest, cnxn, request.authInfo);
requestPathMetricsCollector.registerRequest(request.type, path);
break;
}
}
}
可以看到这里写了个函数handleGetDataRequest()来处理request,此函数的逻辑是:
/**
* 版本:ZooKeeper 3.8.0
* org.apache.zookeeper.server
* FinalRequestProcessor.java
* 662行
*/
private Record handleGetDataRequest(Record request, ServerCnxn cnxn, List<Id> authInfo) throws KeeperException, IOException {
GetDataRequest getDataRequest = (GetDataRequest) request;
String path = getDataRequest.getPath();
DataNode n = zks.getZKDatabase().getNode(path);
// 处理路径不存在的逻辑
if (n == null) {
throw new KeeperException.NoNodeException();
}
// 检查访问控制
zks.checkACL(cnxn, zks.getZKDatabase().aclForNode(n), ZooDefs.Perms.READ, authInfo, path, null);
Stat stat = new Stat();
// 注意:这里判断是否传入了watch
byte[] b = zks.getZKDatabase().getData(path, stat, getDataRequest.getWatch() ? cnxn : null);
return new GetDataResponse(b, stat);
}
在这里判断了是否传入了watch。
当节点发生事件时,例如还是以节点删除为例:
/**
* 版本:ZooKeeper 3.8.0
* org.apache.zookeeper.server
* DataTree.java
* 543行
*/
public void deleteNode(String path, long zxid) throws KeeperException.NoNodeException {
// 删除逻辑
// ........
// 触发节点删除事件
WatcherOrBitSet processed = dataWatches.triggerWatch(path, EventType.NodeDeleted);
childWatches.triggerWatch(path, EventType.NodeDeleted, processed);
childWatches.triggerWatch("".equals(parentName) ? "/" : parentName, EventType.NodeChildrenChanged);
}
这里的dataWatches和childWatches类型是WatchManager,实现自IWatchManager接口。具体调用的是WatchManager.triggerWatch()函数
/**
* 版本:ZooKeeper 3.8.0
* org.apache.zookeeper.server
* DataTree.java
* 117行
*/
@Override
public WatcherOrBitSet triggerWatch(String path, EventType type) {
return triggerWatch(path, type, null);
}
/**
* 版本:ZooKeeper 3.8.0
* org.apache.zookeeper.server
* DataTree.java
* 122行
*/
public WatcherOrBitSet triggerWatch(String path, EventType type, WatcherOrBitSet supress) {
// WatchedEvent参数:事件类型,会话状态,节点
WatchedEvent e = new WatchedEvent(type, KeeperState.SyncConnected, path);
Set<Watcher> watchers = new HashSet<>();
// 获取所有的parent
PathParentIterator pathParentIterator = getPathParentIterator(path);
synchronized (this) {
// 遍历所有上层的节点,将上层所有的watch加入局部变量watchers中
for (String localPath : pathParentIterator.asIterable()) {
// 获取上层某路径下的所有watch
Set<Watcher> thisWatchers = watchTable.get(localPath);
if (thisWatchers == null || thisWatchers.isEmpty()) {
continue;
}
// 遍历该节点下的所有watch
Iterator<Watcher> iterator = thisWatchers.iterator();
while (iterator.hasNext()) {
Watcher watcher = iterator.next();
// 获取watch类型
// 这里的类型是:STANDARD(标准)、PERSISTENT(持久)、PERSISTENT_RECURSIVE(持久递归)
WatcherMode watcherMode = watcherModeManager.getWatcherMode(watcher, localPath);
// 如果是递归watch
if (watcherMode.isRecursive()) {
if (type != EventType.NodeChildrenChanged) {
// 添加到watch
watchers.add(watcher);
}
// 不是递归watch,且
} else if (!pathParentIterator.atParentPath()) {
// 将这个watch加入局部变量watchers中
watchers.add(watcher);
if (!watcherMode.isPersistent()) {
// 如果不是持久watch,用这一次就要删除
iterator.remove();
Set<String> paths = watch2Paths.get(watcher);
if (paths != null) {
paths.remove(localPath);
}
}
}
}
// 某路径下的所有watch都删除了,应该在watchTable中删除这个路径
if (thisWatchers.isEmpty()) {
watchTable.remove(localPath);
}
}
}
// 如果没有没有watch,返回
if (watchers.isEmpty()) {// .....}
for (Watcher w : watchers) {
// .....
// 调用process方法向客户端发送通知
w.process(e);
}
// .....
return new WatcherOrBitSet(watchers);
}
客户端回调逻辑
服务端watch事件触发后,需要客户端执行回调逻辑。客户端使用 SendThread.readResponse() 方法来统一处理服务端的相应。
/**
* 版本:ZooKeeper 3.8.0
* org.apache.zookeeper
* ZooKeeper.java
* 874行
*/
void readResponse(ByteBuffer incomingBuffer) throws IOException {
// 反序列化逻辑
// ...
replyHdr.deserialize(bbia, "header");
// 根据Xid判断这个reply的请求响应类型
// 相应类型有:ping response、通知类型、Auth包
switch (replyHdr.getXid()) {
case PING_XID:
// ....
case AUTHPACKET_XID:
// ....
case NOTIFICATION_XID:
LOG.debug("Got notification session id: 0x{}",
Long.toHexString(sessionId));
WatcherEvent event = new WatcherEvent();
event.deserialize(bbia, "response");
// convert from a server path to a client path
// ......
WatchedEvent we = new WatchedEvent(event);
LOG.debug("Got {} for session id 0x{}", we, Long.toHexString(sessionId));
// 将接收到的事件交给EventThread线程进行处理
eventThread.queueEvent(we);
return;
default:
break;
}
// .....
}
可以看到,处理逻辑中将接收到的事件交给eventThread线程进行处理
/**
* 版本:ZooKeeper 3.8.0
* org.apache.zookeeper
* ClientCnxn.java
* 503行
*/
private void queueEvent(WatchedEvent event, Set<Watcher> materializedWatchers) {
if (event.getType() == EventType.None && sessionState == event.getState()) {
return;
}
sessionState = event.getState();
final Set<Watcher> watchers;
if (materializedWatchers == null) {
// watch的实现逻辑
watchers = watchManager.materialize(event.getState(), event.getType(), event.getPath());
}
// 其他逻辑
// ...
}
实现逻辑是调用了watchManager.materialize()函数
/**
* 版本:ZooKeeper 3.8.0
* org.apache.zookeeper
* ClientCnxn.java
* 503行
*/
public Set<Watcher> materialize(
Watcher.Event.KeeperState state,
Watcher.Event.EventType type,
String clientPath
) {
final Set<Watcher> result = new HashSet<>();
switch (type) {
case NodeDataChanged:
case NodeCreated:
synchronized (dataWatches) {
addTo(dataWatches.remove(clientPath), result);
}
synchronized (existWatches) {
addTo(existWatches.remove(clientPath), result);
}
addPersistentWatches(clientPath, result);
break;
}
case NodeChildrenChanged:// ...
case NodeDeleted://...
}
// 其他逻辑
// .....
}
将查询到的Watcher存储到waitingEvents队列中,调用EventThread类中的run方法会循环取出在waitingEvents队列中等待的Watcher事件进行处理。
处理的过程就是调用watcher接口的process()接口:
/**
* 版本:ZooKeeper 3.8.0
* org.apache.zookeeper
* ClientCnxn.java
* 571行
*/
private void processEvent(Object event) {
try {
if (event instanceof WatcherSetEventPair) {
// each watcher will process the event
WatcherSetEventPair pair = (WatcherSetEventPair) event;
for (Watcher watcher : pair.watchers) {
try {
// 调用process()接口
watcher.process(pair.event);
} catch (Throwable t) {
LOG.error("Error while calling watcher.", t);
}
}
}
}
// .....
//其他逻辑
使用场景
作为一个典型的观察者模式,根据之前的设计模式-观察者模式的内容,观察者模式可以分为推模式和拉模式,也就是说被观察者subject可以主动选择将所需数据传递给观察者,或者也可以将此变化通知给观察者,让观察者去自己取值。
ZooKeeper可以支持这两种形式的观察者模式。
例如,一个典型的配置管理功能,我们可以使用ZooKeeper做一个配置中心:将数据库、配置文件等各种信息存储在ZooKeeper的节点上,服务器集群的各个服务对该节点注册watch,客户端在得知信息发生改变后,去ZooKeeper上拉取最新信息。