概念了解
介绍
在Zookeeper中所有的读操作都可以设置监听,它可以实时监听节点和子节点的变化,一旦发生变化将通知到客户端。
这个Watch是一次性的触发,当设置了Watch的数据发生改变时,则服务器将会把这个改变发送给设置了该watch的客户端。监听机制在ZK中占了很重要的地位,在许多地方都有应用。
三个关键点
(1)一次性的触发
当数据发生改变的时候,监听事件将会发送给客户端。如:客户端做了get -w /znode1
操作,之后/znode1的数据发生了改变,这个客户端将会得到/znode1的监听事件。如果/znode1的数据再次放生改变,客户端则不会再次得到这个监听事件,除非在这之前再次加了监听事件
(2)发生到客户端
监听是异步发送给设置监听客户端的。但是又保证了有序性,只有客户端先看到监听事件才能知道设置了简单的节点发送改变。网络延迟或者其他因素可能导致不同的客户端在不同的时刻感知某一监视事件,但是关键的是不同的客户端所看到的一切具有一致的顺序。
(3)被设置了监听的数据
可以设置两种监听:数据监听和子节点监听。getData()
和exists()
中设置的监听是数据监听,getChildren()
中设置的监听是子节点监听。
setData()
将为正在设置的znode触发数据监视(假设设置成功)。一个成功的create()
将为创建的znode触发一个数据监视,并为父znode触发一个子监视。
一个成功的delete()
将为一个被删除的znode触发一个数据监听和一个子监听(因为没有更多的子监听了),以及一个父znode的子监听。
命令对应关系补充
getData()
:get -w /path
监听数据改变和节点删除
exists()
:stat -w /path
监听数据改变和节点删除
getChildren()
:ls -w /path
监听/path节点的子节点变化和删除/path节点。意思监听的是子节点的数据变化(创建子节点、删除子节点)和当前节点的删除,不监听子节点的数据变化
持久递归的监听
从3.6开始可以设置一个持久监听和一个持久的并递归到子节点的监听。在监听被触发时不会被删除。这三种类型会被触发:NodeCreated, NodeDeleted, and NodeDataChanged 。 意思是当节点创建、节点删除、节点数据更新时会触发,同时也包含了子节点的创建、删除、和数据更新。这个事件是递归到每个子节点上的。即使节点被删除了,这个监听还是存在的。
使用addWatch
命令添加持久递归监听
addWatch /gougou2
####监听显示效果
[zk: 127.0.0.1:2181(CONNECTED) 5]
WATCHER::
WatchedEvent state:SyncConnected type:NodeDataChanged path:/gougou2
WATCHER::
WatchedEvent state:SyncConnected type:NodeCreated path:/gougou2/i
WATCHER::
WatchedEvent state:SyncConnected type:NodeDataChanged path:/gougou2/i
WATCHER::
WatchedEvent state:SyncConnected type:NodeDataChanged path:/gougou2/i
WATCHER::
WatchedEvent state:SyncConnected type:NodeDeleted path:/gougou2/i
WATCHER::
WatchedEvent state:SyncConnected type:NodeCreated path:/gougou2/i
WATCHER::
WatchedEvent state:SyncConnected type:NodeDataChanged path:/gougou2/i
WATCHER::
WatchedEvent state:SyncConnected type:NodeCreated path:/gougou2/i/o
移除监听
可以使用removewatches /path
移除监听。
监听移除后触发的事件:
(1)子节点监听移除事件 ChildWatchRemoved
(2)数据监听移除事件 DataWatchRemoved
(3)持久监听移除事件 PersistentWatchRemoved
[zk: 127.0.0.1:2181(CONNECTED) 6] stat -w /gougou2
cZxid = 0xb000002bc
ctime = Tue Mar 02 22:46:55 CST 2021
mZxid = 0xb00000a4f
mtime = Wed Mar 03 16:26:33 CST 2021
pZxid = 0xb00000a4e
cversion = 4
dataVersion = 2
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 1
numChildren = 2
[zk: 127.0.0.1:2181(CONNECTED) 7] removewatches /gougou2
WATCHER::
WatchedEvent state:SyncConnected type:DataWatchRemoved path:/gougou2
[zk: 127.0.0.1:2181(CONNECTED) 8] ls -w /gougou2
[i, u]
[zk: 127.0.0.1:2181(CONNECTED) 9] removewatches /gougou2
WATCHER::
WatchedEvent state:SyncConnected type:ChildWatchRemoved path:/gougou2
[zk: 127.0.0.1:2181(CONNECTED) 10] addWatch /gougou2
[zk: 127.0.0.1:2181(CONNECTED) 11] removewatches /gougou2
WATCHER::
WatchedEvent state:SyncConnected type:PersistentWatchRemoved path:/gougou2
监听保证
- 监听是根据其他事件、其他监听和异步响应来排序的。Zookeeper客户端库确保所有事件是被有序的调度的
- 客户端在看到znode回复的新的数据之前,是先看到znode的监听事件
- ZooKeeper的监听事件的顺序对应于ZooKeeper服务看到的更新的顺序。
监听机制
Watch使用了推拉相结合发布/订阅的模式。客户端主动向服务器注册监听的节点,一旦节点发生变化,服务器就会主动向客户端发送watcher事件通知,客户端接收到这个通知后,主动到服务器获取最新的数据。
源码分析
使用3.6版本
上面的概念中以提到过在zkClient中getData、exists、getChildren可以设置监听。同时new Zookeeper的时候可以创建一个全局的默认监听。它们的监听机制基本上都是一致的,只是监听的对象不同。本次以getData为例
客户端注册Watcher
(1)入口
byte[] data = zk.getData("/xsh1", new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println(event.getPath());
}
}, new Stat());
(2)在向getData注册Watcher后,会封装一个WatchRegistration对象,进行保存watcher和监听路径的关系,然后等待向服务器注册成功后进行主题中注册。同时将request请求标记成监听状态。并提交请求
public byte[] getData(final String path, Watcher watcher, Stat stat) throws KeeperException, InterruptedException {
final String clientPath = path;
PathUtils.validatePath(clientPath);
// the watch contains the un-chroot path
WatchRegistration wcb = null;
if (watcher != null) {
//封装一个WatchRegistration对象,进行保存watcher和监听路径的关系
wcb = new DataWatchRegistration(watcher, clientPath);
}
final String serverPath = prependChroot(clientPath);
RequestHeader h = new RequestHeader();
h.setType(ZooDefs.OpCode.getData);
GetDataRequest request = new GetDataRequest();
request.setPath(serverPath);
//标记request为监听状态
request.setWatch(watcher != null);
GetDataResponse response = new GetDataResponse();
//向服务器提交请求
ReplyHeader r = cnxn.submitRequest(h, request, response, wcb);
if (r.getErr() != 0) {
throw KeeperException.create(KeeperException.Code.get(r.getErr()), clientPath);
}
if (stat != null) {
DataTree.copyStat(response.getStat(), stat);
}
//获取到服务器返回的数据
return response.getData();
}
(3)向服务器提交请求时,会将RequestHeader、request、response、WatchRegistration、WatchDeregistration封装到Packet,然后放入发送队列中进行发送。Packet是Zookeeper客户端和服务器进行网络传输的最小通信协议单元。
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);
...
outgoingQueue.add(packet);
...
sendThread.getClientCnxnSocket().packetAdded();
return packet;
}
SendThread.readResponse用来接收服务端的响应,接受到响应后会从(2)中创建的watcher注册到ZKWatchManager中
protected void finishPacket(Packet p) {
int err = p.replyHeader.getErr();
if (p.watchRegistration != null) {
p.watchRegistration.register(err);
}
...
}
public void register(int rc) {
if (shouldAddWatch(rc)) {
Map<String, Set<Watcher>> watches = getWatches(rc);
synchronized (watches) {
Set<Watcher> watchers = watches.get(clientPath);
if (watchers == null) {
watchers = new HashSet<Watcher>();
watches.put(clientPath, watchers);
}
watchers.add(watcher);
}
}
}
服务器处理Watcher
FinalRequestProcessor.processRequest用来处理客户端发过来的请求。handleGetDataRequest处理getData类型请求,如果客户端添加监听,则将结点的path和客户端传来的ServerCnxn对象存储到watchTable和watch2Paths中
- watchTable: 是一个粗粒度的存储,存储path和在其绑定的所有监听
- watch2Paths:是细粒度的存储,存储当前watcher和paths的对应关系
public void processRequest(Request request) {
....
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;
}
....
}
private Record handleGetDataRequest(Record request, ServerCnxn cnxn, List<Id> authInfo) throws KeeperException, IOException {
...
byte[] b = zks.getZKDatabase().getData(path, stat, getDataRequest.getWatch() ? cnxn : null);
....
}
// DataTree.class
public byte[] getData(String path, Stat stat, Watcher watcher) throws KeeperException.NoNodeException {
DataNode n = nodes.get(path);
byte[] data = null;
if (n == null) {
throw new KeeperException.NoNodeException();
}
synchronized (n) {
n.copyStat(stat);
if (watcher != null) {
dataWatches.addWatch(path, watcher);
}
data = n.data;
}
updateReadStat(path, data == null ? 0 : data.length);
return data;
}
// WatchManager.class
public synchronized boolean addWatch(String path, Watcher watcher, WatcherMode watcherMode) {
if (isDeadWatcher(watcher)) {
LOG.debug("Ignoring addWatch with closed cnxn");
return false;
}
Set<Watcher> list = watchTable.get(path);
if (list == null) {
// don't waste memory if there are few watches on a node
// rehash when the 4th entry is added, doubling size thereafter
// seems like a good compromise
list = new HashSet<>(4);
watchTable.put(path, list);
}
list.add(watcher);
Set<String> paths = watch2Paths.get(watcher);
if (paths == null) {
// cnxns typically have many watches, so use default cap here
paths = new HashSet<>();
watch2Paths.put(watcher, paths);
}
watcherModeManager.setWatcherMode(watcher, path, watcherMode);
return paths.add(path);
}
服务端触发监听
当客户端执行一个path的setData后,服务器触发这个path的监听
public Stat setData(String path, byte[] data, int version, long zxid, long time) throws KeeperException.NoNodeException {
.....
updateWriteStat(path, dataBytes);
dataWatches.triggerWatch(path, EventType.NodeDataChanged);
return s;
}
先去watchTable中取出该path的所有监听,进行遍历添加到一个需要触发的wather集合中。如果不是是持久化循环监听或者持久化监听,则将其从watchTable和watch2Paths移除。收集完成后循环通知客户端调用监听。
public WatcherOrBitSet triggerWatch(String path, EventType type, WatcherOrBitSet supress) {
WatchedEvent e = new WatchedEvent(type, KeeperState.SyncConnected, path);
//收集需要触发的监听
Set<Watcher> watchers = new HashSet<>();
PathParentIterator pathParentIterator = getPathParentIterator(path);
synchronized (this) {
for (String localPath : pathParentIterator.asIterable()) {
Set<Watcher> thisWatchers = watchTable.get(localPath);
if (thisWatchers == null || thisWatchers.isEmpty()) {
continue;
}
Iterator<Watcher> iterator = thisWatchers.iterator();
while (iterator.hasNext()) {
Watcher watcher = iterator.next();
WatcherMode watcherMode = watcherModeManager.getWatcherMode(watcher, localPath);
//判断是不是一个持久循环监听
if (watcherMode.isRecursive()) {
//并且不是孩子结点的改变
if (type != EventType.NodeChildrenChanged) {
watchers.add(watcher);
}
} else if (!pathParentIterator.atParentPath()) {
watchers.add(watcher);
//判断如果不是一个持久监听则将其从watchTable中移除。并且从watch2Paths中移除
if (!watcherMode.isPersistent()) {
iterator.remove();
Set<String> paths = watch2Paths.get(watcher);
if (paths != null) {
paths.remove(localPath);
}
}
}
}
//如果没有可以触发的监听,则将path从watchTable移除
if (thisWatchers.isEmpty()) {
watchTable.remove(localPath);
}
}
}
if (watchers.isEmpty()) {
if (LOG.isTraceEnabled()) {
ZooTrace.logTraceMessage(LOG, ZooTrace.EVENT_DELIVERY_TRACE_MASK, "No watchers for " + path);
}
return null;
}
//循环通知客户端调用监听
for (Watcher w : watchers) {
if (supress != null && supress.contains(w)) {
continue;
}
w.process(e);
}
.....
return new WatcherOrBitSet(watchers);
}
客户端接收来自服务端通知并触发监听事件
客户端使用SendThread#readResponse接收服务端的事件通知。收集需要触发的监听,并包装成WatcherSetEventPair对象,放到waitingEvents队列中以便稍后处理
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) {
//获取需要触发的监听,如果是非持久化的监听,则从ZKWatchManager中移除
watchers = watcher.materialize(event.getState(), event.getType(), event.getPath());
} else {
watchers = new HashSet<Watcher>();
watchers.addAll(materializedWatchers);
}
WatcherSetEventPair pair = new WatcherSetEventPair(watchers, event);
// 建监听pair对象放到队列中稍后进程处理
waitingEvents.add(pair);
}
public Set<Watcher> materialize(
Watcher.Event.KeeperState state,
Watcher.Event.EventType type,
String clientPath) {
Set<Watcher> result = new HashSet<Watcher>();
.....
case NodeDataChanged:
//收集需要触发的watcher,并从dataWatches中移除
case NodeCreated:
synchronized (dataWatches) {
addTo(dataWatches.remove(clientPath), result);
}
synchronized (existWatches) {
addTo(existWatches.remove(clientPath), result);
}
addPersistentWatches(clientPath, result);
break;
...
return result;
而EventThread#run()会不断的处理waitingEvents队列,每次从waitingEvents队列中取出一个,并串行的去触发
public void run() {
try {
isRunning = true;
// 不断的从waitingEvents取出,并触发事件
while (true) {
Object event = waitingEvents.take();
if (event == eventOfDeath) {
wasKilled = true;
} else {
processEvent(event);
}
if (wasKilled) {
synchronized (waitingEvents) {
if (waitingEvents.isEmpty()) {
isRunning = false;
break;
}
}
}
}
}
....
}
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 {
//调用具体的监听回调
watcher.process(pair.event);
} catch (Throwable t) {
LOG.error("Error while calling watcher ", t);
}
}
................
}
总结
总的一句话就是:
Watch使用了推拉相结合发布/订阅的模式。客户端主动向服务器注册监听的节点,一旦节点发生变化,服务器就会主动向客户端发送watcher事件通知,客户端接收到这个通知后,可以主动到服务器获取最新的数据。而且监听是有时序性和顺序性的保证。监听是一次性的,触发后就会移除掉,除非重新注册监听,但是在3.6版本开始可以使用addWatch设置持久递归的监听。