ZooKeeper 允许客户端向服务端注册一个 Watcher 监听,当服务端的一些指定事件触发了这个 Watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能。

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

主要会涉及下面这些类

1. Watcher 接口

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

1.1 Watcher 事件

KeeperState EventType 触发条件 说明
  SyncConnected(0)   None(-1) 客户端与服务端成功建立连接   此时客户端和服务器处于连接状态  
NodeCreated(1) Watcher 监听的对应数据节点被创建
NodeDeleted(2) Watcher 监听的对应数据节点被删除
NodeDataChanged(3) Watcher 监听的对应数据节点的数据内容发生变更
NodeChildChanged(4) Wather 监听的对应数据节点的子节点列表发生变更
Disconnected(0) None(-1) 客户端与 ZooKeeper 服务器断开连接 此时客户端和服务器处于断开连接状态
Expired(-112) Node(-1) 会话超时 此时客户端会话失效,通常同时也会受到 SessionExpiredException 异常
AuthFailed(4) None(-1) 通常有两种情况。(1)使用错误的 schema 进行权限检查 (2)SASL 权限检查失败 通常同时也会收到 AuthFailedException 异常

1.2 回调方法 process()

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

org.apache.zookeeper.Watcher#process

abstract public void process(WatchedEvent event);

在这里提一下 WathcerEvent 实体。笼统地讲,两者表示的是同一个事物,都是对一个服务端事件的封装。不同的是,WatchedEvent 是一个逻辑事件,用于服务端和客户端程序执行过程中所需的逻辑对象,而 WatcherEvent 因为实现了序列化接口,因此可以用于网络传输。WatchedEvent 包含了每一个事件的三个基本属性:通知状态(keeperState),事件类型(EventType)和节点路径(path)。

org.apache.zookeeper.proto.WatcherEvent

public class WatcherEvent implements Record {
private int type;
private int state;
private String path;
}

2. 工作机制

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

2.1 客户端注册 Watcher

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

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

以 org.apache.zookeeper.ZooKeeper#getData(java.lang.String, org.apache.zookeeper.Watcher, org.apache.zookeeper.data.Stat) 为例:这个 Watcher 将作为整个 ZooKeeper 会话期间的默认 Watcher,会一直被保存在客户端 ZKWatchManager 的 defaultWatcher 中。另外,ZooKeeper 客户端也可以通过 getDatagetChildren 和 exist 三个接口来向 ZooKeeper 服务器注册 Watcher,无论使用哪种方式,注册 Watcher 的工作原理都是一致的。

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) {
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.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();
}

在 ZooKeeper 中,Packet 可以被看作一个最小的通信协议单元,用于进行客户端与服务端之间的网络传输,任何需要传输的对象都需要包装成一个 Packet 对象。因此,在 ClientCnxn 中 WatchRegistration 又会被封装到 Packet 中,然后放入发送队列中等待客户端发送:在向 getData 接口注册 Watcher 后,客户端首先会对当前客户端请求 request 进行标记,将其设置为 “使用 Watcher 监听”,同时会封装一个 Watcher 的注册信息 WatchRegistration 对象,用于暂时保存数据节点的路径和 Watcher 的对应关系。

org.apache.zookeeper.ClientCnxn#submitRequest

public ReplyHeader submitRequest(RequestHeader h, Record request,
Record response, WatchRegistration watchRegistration)
throws InterruptedException {
ReplyHeader r = new ReplyHeader();
Packet packet = queuePacket(h, r, request, response, null, null, null,
null, watchRegistration);
synchronized (packet) {
while (!packet.finished) {
packet.wait();
}
}
return r;
}

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

private void finishPacket(Packet p) {
if (p.watchRegistration != null) {
p.watchRegistration.register(p.replyHeader.getErr());
} if (p.cb == null) {
synchronized (p) {
p.finished = true;
p.notifyAll();
}
} else {
p.finished = true;
eventThread.queuePacket(p);
}
}

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

abstract protected Map<String, Set<Watcher>> getWatches(int rc);
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);
}
}
}

org.apache.zookeeper.ZooKeeper.ZKWatchManager

private static class ZKWatchManager implements ClientWatchManager {
private final Map<String, Set<Watcher>> dataWatches =
new HashMap<String, Set<Watcher>>();
private final Map<String, Set<Watcher>> existWatches =
new HashMap<String, Set<Watcher>>();
private final Map<String, Set<Watcher>> childWatches =
new HashMap<String, Set<Watcher>>(); private volatile Watcher defaultWatcher;
}

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

在 Packet.createBB() 中,ZooKeeper 只会将 requestHeader 和 reqeust 两个属性进行序列化,也就是说,尽管 WatchResgistration 被封装在了 Packet 中,但是并没有被序列化到底层字节数组中去,因此也就不会进行网络传输了。

2.2 服务端处理 Watcher

2.2.1 服务端注册 Watcher

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

case OpCode.getData: {
lastOp = "GETD";
GetDataRequest getDataRequest = new GetDataRequest();
ByteBufferInputStream.byteBuffer2Record(request.request, getDataRequest);
DataNode n = zks.getZKDatabase().getNode(getDataRequest.getPath());
if (n == null) {
throw new KeeperException.NoNodeException();
}
PrepRequestProcessor.checkACL(zks, zks.getZKDatabase().aclForNode(n), ZooDefs.Perms.READ, request.authInfo);
Stat stat = new Stat();
byte b[] = zks.getZKDatabase().getData(getDataRequest.getPath(), stat, getDataRequest.getWatch() ? cnxn : null);
rsp = new GetDataResponse(b, stat);
break;
}

数据节点的节点路径和 ServerCnxn 最终会被存储在 WatcherManager 的 watchTable 和 watch2Paths 中。WatchManager 是 ZooKeeper 服务端 Watcher 的管理者,其内部管理的 watchTable 和 watch2Pashs 两个存储结构,分别从两个维度对 Watcher 进行存储。从 getData 请求的处理逻辑中,我们可以看到,当 getDataRequest.getWatch() 为 true 的时候,ZooKeeper 就认为当前客户端请求需要进行 Watcher 注册,于是就会将当前的 ServerCnxn 对象作为一个 Watcher 连同数据节点路径传入 getData 方法中去。注意到,抽象类 ServerCnxn 实现了 Watcher 接口。

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

org.apache.zookeeper.server.WatchManager#addWatch

public synchronized void addWatch(String path, Watcher watcher) {
HashSet<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<Watcher>(4);
watchTable.put(path, list);
}
list.add(watcher); HashSet<String> paths = watch2Paths.get(watcher);
if (paths == null) {
// cnxns typically have many watches, so use default cap here
paths = new HashSet<String>();
watch2Paths.put(watcher, paths);
}
paths.add(path);
}

2.2.2 Watcher 触发

org.apache.zookeeper.server.DataTree#setData

public Stat setData(String path, byte data[], int version, long zxid,
long time) throws KeeperException.NoNodeException {
Stat s = new Stat();
DataNode n = nodes.get(path);
if (n == null) {
throw new KeeperException.NoNodeException();
}
byte lastdata[] = null;
synchronized (n) {
lastdata = n.data;
n.data = data;
n.stat.setMtime(time);
n.stat.setMzxid(zxid);
n.stat.setVersion(version);
n.copyStat(s);
}
// now update if the path is in a quota subtree.
String lastPrefix;
if((lastPrefix = getMaxPrefixWithQuota(path)) != null) {
this.updateBytes(lastPrefix, (data == null ? 0 : data.length)
- (lastdata == null ? 0 : lastdata.length));
}
dataWatches.triggerWatch(path, EventType.NodeDataChanged);
return s;
}

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

public Set<Watcher> triggerWatch(String path, EventType type, Set<Watcher> supress) {
WatchedEvent e = new WatchedEvent(type, KeeperState.SyncConnected, path);
HashSet<Watcher> watchers;
synchronized (this) {
watchers = watchTable.remove(path);
if (watchers == null || watchers.isEmpty()) {
return null;
}
for (Watcher w : watchers) {
HashSet<String> paths = watch2Paths.get(w);
if (paths != null) {
paths.remove(path);
}
}
}
for (Watcher w : watchers) {
if (supress != null && supress.contains(w)) {
continue;
}
w.process(e);
}
return watchers;
}

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

    1. 封装 WatchedEvent

      首先将通知状态(KeeperState)、事件类型(EventType)以及节点路径(Path)封装成一个 WatchedEvent 对象。

    2. 查询 Watcher。

      根据数据节点的节点路径从 watchTable 中取出对应的 Watcher。如果没有找到 Watcher,说明没有任何客户端在该数据节点上注册过 Watcher,直接退出。而如果找到了这个 Watcher,会将其提取出来,同时会直接从 watchTable 和 watch2Paths 中将其删除——从这里我们也可以看出,Watcher 在服务端是一次性的,即触发一次就失效了。

调用 process 方法来触发 Watcher。

在这一步中,会逐个依次地调用从步骤2中找出的所有 Watcher 的 process 方法。这里的 process 方法,事实上就是 ServerCnxn 的对应方法:

org.apache.zookeeper.server.NIOServerCnxn#process

@Override
synchronized public void process(WatchedEvent event) {
ReplyHeader h = new ReplyHeader(-1, -1L, 0);
// Convert WatchedEvent to a type that can be sent over the wire
WatcherEvent e = event.getWrapper(); sendResponse(h, e, "notification");
}

在 process 方法中,主要逻辑如下。

    • 在请求头中标记 “-1”,表明当前是一个通知。
    • 将 WawtchedEvent 包装成 WatcherEvent,以便进行网络传输序列化。
    • 向客户端发送该通知。

3. 客户端回调 Watcher

3.1 SendThread 接收事件通知

对于一个来自服务端的响应,客户端都是由 org.apache.zookeeper.ClientCnxn.SendThread#readResponse 方法来统一进行处理的,如果响应头 replyHdr 中标识了 XID 为 -1,表明这是一个通知类型的响应。

if (replyHdr.getXid() == -1) {
// -1 means notification
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 if (serverPath.length() > chrootPath.length())
event.setPath(serverPath.substring(chrootPath.length()));
else {
LOG.warn("Got server path " + event.getPath()
+ " which is too short for chroot path "
+ chrootPath);
}
}
WatchedEvent we = new WatchedEvent(event);
eventThread.queueEvent( we );
return;
}

处理过程大体上分为以下 4 个主要步骤:

  1. 反序列化。

    将字节流转换成 WatcherEvent 对象。

  2. 处理 chrootPath。

    如果客户端设置了 chrootPath 属性,那么需要对服务端传过来的完整的节点路径进行 chrootPath 处理,生成客户端的一个相对节点路径。

  3. 还原 WatchedEvent

    将 WatcherEvent 对象转换成 WatchedEvent

  4. 回调 Watcher。

    将 WatchedEvent 对象交给 EventThread 线程,在下一个轮询周期中进行 Watcher 回调。

3.2 EventThread 处理事件通知

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

org.apache.zookeeper.ClientCnxn.EventThread#queueEvent

public void queueEvent(WatchedEvent event) {
if (event.getType() == EventType.None
&& sessionState == event.getState()) {
return;
}
sessionState = event.getState(); // materialize the watchers based on the event
WatcherSetEventPair pair = new WatcherSetEventPair(
watcher.materialize(event.getState(), event.getType(),event.getPath()), event);
// queue the pair (watch set & event) for later processing
waitingEvents.add(pair);
}

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

    @Override
public Set<Watcher> materialize(Watcher.Event.KeeperState state, Watcher.Event.EventType type, String clientPath) {
Set<Watcher> result = new HashSet<Watcher>(); switch (type) {
// ...
case NodeDataChanged:
case NodeCreated:
synchronized (dataWatches) {
addTo(dataWatches.remove(clientPath), result);
}
synchronized (existWatches) {
addTo(existWatches.remove(clientPath), result);
}
break;
// ...
} return result;
}
}

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

获取到相关的所有 Watcher 后,会将其放入 waitingEvents 这个队列中去。WaitingEvents 是一个待处理 Watcher 队列,EventThread 的 run 方法会不断对该队列进行处理。EventThread线程每次都会从 waitingEvents 队列中取出一个 Watcher,并进行串行同步处理。注意,此处 processEvent 方法中的 Watcher 才是之前客户端真正注册的 Watcher,调用其 process 方法就可以实现 Watcher 的回调了。

总结

1、一次性

Watch是一次性的,每次都需要重新注册,并且客户端在会话异常结束时不会收到任何通知,而快速重连接时仍不影响接收通知。

2、客户端串行处理

Watch的回调执行都是顺序执行的,并且客户端在没有收到关注数据的变化事件通知之前是不会看到最新的数据,另外需要注意不要在Watch回调逻辑中阻塞整个客户端的Watch回调。

3、轻量

Watch是轻量级的,WatchEvent是最小的通信单元,结构上只包含通知状态、事件类型和节点路径。ZooKeeper服务端只会通知客户端发生了什么,并不会告诉具体内容。

《从Paxos到ZooKeeper 分布式一致性原理与实践》阅读【Watcher】的更多相关文章

  1. 从Paxos到Zookeeper 分布式一致性原理与实践读书心得

    一 本书作者介绍 此书名为从Paxos到ZooKeeper分布式一致性原理与实践,作者倪超,阿里巴巴集团高级研发工程师,国家认证系统分析师,毕业于杭州电子科技大学计算机系.2010年加入阿里巴巴中间件 ...

  2. 《从Paxos到ZooKeeper分布式一致性原理与实践》学习笔记

    第一章 分布式架构 1.1 从集中式到分布式 集中式的特点: 部署结构简单(因为基于底层性能卓越的大型主机,不需考虑对服务多个节点的部署,也就不用考虑多个节点之间分布式协调问题) 分布式系统是一个硬件 ...

  3. 我读《从Paxos到zookeeper分布式一致性原理与实践》

    从年后拿到这本书开始阅读,到准备系统分析师考试之前,终于读完了一遍,对Zookeeper有了一个全面的认识,整本书从理论到应用再到细节的阐述,内容安排从逻辑性和实用性上都是很优秀的,对全面认识Zook ...

  4. 《从Paxos到ZooKeeper 分布式一致性原理与实践》读书笔记

    一.分布式架构 1.分布式特点 分布性 对等性.分布式系统中的所有计算机节点都是对等的 并发性.多个节点并发的操作一些共享的资源 缺乏全局时钟.节点之间通过消息传递进行通信和协调,因为缺乏全局时钟,很 ...

  5. [从Paxos到ZooKeeper][分布式一致性原理与实践]<二>一致性协议[Paxos算法]

    Overview 在<一>有介绍到,一个分布式系统的架构设计,往往会在系统的可用性和数据一致性之间进行反复的权衡,于是产生了一系列的一致性协议. 为解决分布式一致性问题,在长期的探索过程中 ...

  6. 2月22日 《从Paxos到Zookeeper 分布式一致性原理与实践》读后感

    zk的特点: 分布式一致性的解决方案,包括:顺序一致性,原子性,单一视图,可靠性,实时性 zk的基本概念: 集群角色:not Master/Slave,is Leader/Follower/Obser ...

  7. 从Paxos到Zookeeper分布式一致性原理与实践 读书笔记之(一) 分布式架构

    1.1 从集中式到分布式 1 集中式特点 结构简单,无需考虑对多个节点的部署和节点之间的协作. 2  分布式特点 分不性:在时间可空间上随意分布,机器的分布情况随时变动 对等性:计算机之间没有主从之分 ...

  8. 《从Paxos到ZooKeeper 分布式一致性原理与实践》阅读【Leader选举】

    从3.4.0版本开始,zookeeper废弃了0.1.2这3种Leader选举算法,只保留了TCP版本的FastLeaderElection选举算法. 当ZooKeeper集群中的一台服务器出现以下两 ...

  9. 《从Paxos到Zookeeper:分布式一致性原理与实践》【PDF】下载

    内容简介 Paxos到Zookeeper分布式一致性原理与实践从分布式一致性的理论出发,向读者简要介绍几种典型的分布式一致性协议,以及解决分布式一致性问题的思路,其中重点讲解了Paxos和ZAB协议. ...

随机推荐

  1. MongoDB小结22 - id生成规则

    MongoDB的文档必须有一个_id键. 目的是为了确认在集合里的每个文档都能被唯一标识. ObjectId 是 _id 的默认类型. ObjectId 采用12字节的存储空间,每个字节两位16进制数 ...

  2. how to read openstack code : paste deploy

    本篇分为以下几个部分 paste 是什么 怎样使用paste paste of neutron paste 是什么 WSGI 是python 中application 和 web server互通的标 ...

  3. psychology

    壹.自身(荣.命)   一.职业分析   ㈠.分析性格→分析长处和短处→分析大家都有的长处→确定自己最终发展的专业.   1 .性格--宗正   耐压力特强,即使肩头责任重大,也能够处理得稳稳当当,是 ...

  4. Dell PowerEdgeServerT110II USB Boot更新

    可引导USB设备更新Dell PowerEdge服务器 当显示Boot Options(“启动选项”)时,选择option 1(选项 1)以开始固件更新. 现在正在加载的Linux发行版本 然后固件更 ...

  5. Velocity高速新手教程

    变量 (1)变量的定义: #set($name = "hello")      说明:velocity中变量是弱类型的. 当使用#set 指令时,括在双引號中的字面字符串将解析和又 ...

  6. 我在CSDN开通博客啦!

    今天,我最终在CSDN开通博客啦! 

  7. 解决ubuntu中firefox浏览器总是提示找不到server的问题

    这个情况在我机器上常常出现,并且时不时的给你出点问题.可是有些时候等一下就好了.或者把引擎换到百度的话它就又行得通了.. 被这个问题搞得非常烦.上网查了下说是防火墙啊之类的出问题.可是自己弄了后这个问 ...

  8. Error处理: android.media.MediaRecorder.start(Native Method) 报错:start failed: -19【转】

    本文转载自:http://blog.csdn.net/netwalk/article/details/17686993 Error处理: android.media.MediaRecorder.sta ...

  9. 微软2016校园招聘在线笔试 [Recruitment]

    时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 A company plans to recruit some new employees. There are N ca ...

  10. [JSOI 2008] 星球大战

    [题目链接] https://www.lydsy.com/JudgeOnline/problem.php?id=1015 [算法] 考虑离线 , 将删点转化为加点 , 用并查集维护连通性即可 时间复杂 ...