《从Paxos到ZooKeeper 分布式一致性原理与实践》阅读【Watcher】
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
因为实现了序列化接口,因此可以用于网络传输。WatchedEventkeeperState
),事件类型(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 客户端也可以通过 getData
,getChildren
和 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 的触发逻辑都是一致的,基本步骤如下。
封装
WatchedEvent
。首先将通知状态(
KeeperState
)、事件类型(EventType
)以及节点路径(Path
)封装成一个WatchedEvent
对象。查询 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 个主要步骤:
反序列化。
将字节流转换成
WatcherEvent
对象。处理 chrootPath。
如果客户端设置了 chrootPath 属性,那么需要对服务端传过来的完整的节点路径进行
chrootPath
处理,生成客户端的一个相对节点路径。还原
WatchedEvent
。将
WatcherEvent
对象转换成WatchedEvent
。回调 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 存储(即 dataWatches
,existWatches
或 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】的更多相关文章
- 从Paxos到Zookeeper 分布式一致性原理与实践读书心得
一 本书作者介绍 此书名为从Paxos到ZooKeeper分布式一致性原理与实践,作者倪超,阿里巴巴集团高级研发工程师,国家认证系统分析师,毕业于杭州电子科技大学计算机系.2010年加入阿里巴巴中间件 ...
- 《从Paxos到ZooKeeper分布式一致性原理与实践》学习笔记
第一章 分布式架构 1.1 从集中式到分布式 集中式的特点: 部署结构简单(因为基于底层性能卓越的大型主机,不需考虑对服务多个节点的部署,也就不用考虑多个节点之间分布式协调问题) 分布式系统是一个硬件 ...
- 我读《从Paxos到zookeeper分布式一致性原理与实践》
从年后拿到这本书开始阅读,到准备系统分析师考试之前,终于读完了一遍,对Zookeeper有了一个全面的认识,整本书从理论到应用再到细节的阐述,内容安排从逻辑性和实用性上都是很优秀的,对全面认识Zook ...
- 《从Paxos到ZooKeeper 分布式一致性原理与实践》读书笔记
一.分布式架构 1.分布式特点 分布性 对等性.分布式系统中的所有计算机节点都是对等的 并发性.多个节点并发的操作一些共享的资源 缺乏全局时钟.节点之间通过消息传递进行通信和协调,因为缺乏全局时钟,很 ...
- [从Paxos到ZooKeeper][分布式一致性原理与实践]<二>一致性协议[Paxos算法]
Overview 在<一>有介绍到,一个分布式系统的架构设计,往往会在系统的可用性和数据一致性之间进行反复的权衡,于是产生了一系列的一致性协议. 为解决分布式一致性问题,在长期的探索过程中 ...
- 2月22日 《从Paxos到Zookeeper 分布式一致性原理与实践》读后感
zk的特点: 分布式一致性的解决方案,包括:顺序一致性,原子性,单一视图,可靠性,实时性 zk的基本概念: 集群角色:not Master/Slave,is Leader/Follower/Obser ...
- 从Paxos到Zookeeper分布式一致性原理与实践 读书笔记之(一) 分布式架构
1.1 从集中式到分布式 1 集中式特点 结构简单,无需考虑对多个节点的部署和节点之间的协作. 2 分布式特点 分不性:在时间可空间上随意分布,机器的分布情况随时变动 对等性:计算机之间没有主从之分 ...
- 《从Paxos到ZooKeeper 分布式一致性原理与实践》阅读【Leader选举】
从3.4.0版本开始,zookeeper废弃了0.1.2这3种Leader选举算法,只保留了TCP版本的FastLeaderElection选举算法. 当ZooKeeper集群中的一台服务器出现以下两 ...
- 《从Paxos到Zookeeper:分布式一致性原理与实践》【PDF】下载
内容简介 Paxos到Zookeeper分布式一致性原理与实践从分布式一致性的理论出发,向读者简要介绍几种典型的分布式一致性协议,以及解决分布式一致性问题的思路,其中重点讲解了Paxos和ZAB协议. ...
随机推荐
- Ubuntu 16.04安装SwitchHosts
下载: https://github.com/oldj/SwitchHosts/releases 解压: unzip SwitchHosts-linux-x64_v3.3.6.5287.zip 移动: ...
- MicroPython实现wifi干扰与抓包
0×00前言 之前做的WIFI攻击实验都是基于arduino环境开发的,最近想尝试一下使用micropython完成deautch(解除认证)攻击.本次开发板使用的还是TPYBoardv202. 0× ...
- laravel5.4新特性
http://www.cnblogs.com/webskill/category/1067140.html laravel 5.4 新特性 component and slot 使用: 1.compo ...
- 001 Cisco router prewired
Cisco router 预配: Router>en Router#config t Enter configuration commands, one per line. End with ...
- samba服務器下文件夾chmod權限技巧
需要的效果: samba下文件夹(abc)不可以被重命名.不可以被刪除,其所有子目录可读可写. 如何做到: chmod 777 -R abc # -R 使得abc下所有数据继承可读可写权限 chm ...
- 笔记本电脑 联想 Thinkpad E420 无法打开摄像头怎么办
1 计算机管理-右击USB视频设备(应该显示为黄色问号,表示驱动安装不成功),点击浏览计算机以查找驱动程序软件 2 选择"从计算机的设备驱动程序列表中选择",然后选择Microso ...
- 网页瞬间转换成桌面应用级程序(IOS/Win/Linux)
首先下载node,并且安装. 安装检测 检测完成后,执行下面这条命令 npm i -g nativefier 安装完成后 执行下面的命令+网址即可生成任意的桌面级程序 示例:nativefier &q ...
- bootstrap 时间控件
近期使用了bootstrap的UI感觉确实非常美丽,非常值得学习和使用. 以下先简单了解下bootstrap的时间控件. 这个时间控件使用起来还是很的简单.仅仅须要引入主要的css和js就能够了 须要 ...
- png图片解码
PNG.可移植网络图形格式(Portable Network Graphic Format,PNG)名称来源于非官方的"PNG's Not GIF",是一种位图文件(bitmap ...
- 前端开发本地环境配置(Apache+Dreamweaver)
一.安装apache服务器 1.下载apache软件: 2.安装,直接下一步就好: 3.安装好后找到安装文件夹下的conf文件中的httpd.conf: 4.打开httpd.conf文件,做以下修改: ...