导语:分析Zookeeper内部如何做到分布式数据一致性,将从系统模型、序列化与协议、客户端工作原理、会话、服务端工作原理及数据存储来分析Zookeeper技术底层实现。
系统模型:
首先从数据模型,节点特性、版本、Wather和ACL特性来分析Zookeeper的系统模型。
数据模型:
Zookeeper使得分布式程序能够通过一个共享的、树形结构的名字空间来进行相互协调。它采用了类似文件系统的目录树型结构的数据模型,我们称为ZNode节点,ZNode是Zookeeper中数据最小的单元,每个节点都可以保存数据同时也可以挂载子节点。因此构成层次化的命名空间,类似于节点数
ZNode:
zookeeper名字空间由节点znode构成,其组织方式类似文件系统,其中各个节点相当于目录和文件,通过路径作为唯一标识。
事务ID:
在传统数据库中事务具有所谓的ACID特性:即原子性、一致性、隔离性、和持久性。
在Zookeeper中,事务是指能够改变Zookeeper服务器状态的操作,称为事务操作或者更新操作。对于每一个事务的请求,Zookeeper都会为其分配一个全局唯一的事务ID(ZXID),通常是一个64位的数字。每一个ZXID对应一次更新操作,Zookeeper根据这些全局唯一的ZXID请求来处理更新请求的全局顺序。
节点特性:
节点类型:
ZooKeeper 节点是有生命周期的,这取决于节点的类型。在 ZooKeeper 中,节点类型可以分为持久节点(PERSISTENT )、临时节点(EPHEMERAL),以及时序节点(SEQUENTIAL ),具体在节点创建过程中,一般是组合使用,可以生成以下 4 种节点类型。
持久节点(PERSISTENT)
所谓持久节点,是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点——不会因为创建该节点的客户端会话失效而消失
持久顺序节点(PERSISTENT_SEQUENTIAL)
在ZK中,每个父节点会为他的第一级子节点维护一份时序,会记录每个子节点创建的先后顺序。基于这个特性,在创建子节点的时候,可以设置这个属性,那么在创建节点过程中,ZK会自动为给定节点名加上一个数字后缀,作为新的节点名。这个数字后缀的范围是整型的最大值。
临时节点(EPHEMERAL)
和持久节点不同的是,临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。注意,这里提到的是会话失效,而非连接断开。另外,在临时节点下面不能创建子节点,注意是更具Session会话的失效时间来设定的。
临时顺序节点(EPHEMERAL_SEQUENTIAL)
临时顺序节点的特性和临时节点一致,同时是在临时节点的基础上,添加了顺序的特性。
状态信息:
在Zookeeper维护数据节点的同时,每个节点除了存储数据内容之外,还存储了数据节点本身一些状态信息,如图所示:
第一行是当前节点的数据内容,第二行开始就是节点的状态信息,Zookeeper客户端Stat类的数据结构:
ZooKeeper中每个znode的Stat结构体由下述字段构成:
l czxid:创建节点的事务的zxid
l mzxid:对znode最近修改的zxid
l ctime:以距离时间原点(epoch)的毫秒数表示的znode创建时间
l mtime:以距离时间原点(epoch)的毫秒数表示的znode最近修改时间
l version:znode数据的修改次数
l cversion:znode子节点修改次数
l aversion:znode的ACL修改次数
l ephemeralOwner:如果znode是临时节点,则指示节点所有者的会话ID;如果不是临时节点,则为零。
l dataLength:znode数据长度。
l numChildren:znode子节点个数。
版本:
用于保证分布式数据原子性操作。
Zookeeper会为每个Znode维护一个叫作Stat的数据结构,结构如图:
|
|
|
|
|
|
|
|
version是表示对数据节点数据内容的变更次数,强调的是变更次数,因此就算数据内容的值没有发生变化,version的值也会递增。
在介绍version时,我们可以简单的了解在数据库技术中,通常提到的“悲观锁”和“乐观锁”:
悲观锁:具有严格的独占和排他特性,能偶有效的避免不同事务在同一数据并发更新而造成的数据一致性问题。实现原理就是:假设A事务正在对数据进行处理,那么在整个处理过程中,都会将数据处于锁定的状态,在这期间,其他事务将无法对这个数据进行更新操作,直到事务A完成对該数据的处理,释放对应的锁。一份数据只会分配一把钥匙,如数据库的表锁或者行锁(for update).
乐观锁:具体实现是,表中有一个版本字段,第一次读的时候,获取到这个字段。处理完业务逻辑开始更新的时候,需要再次查看该字段的值是否和第一次的一样。如果一样更新,反之拒绝。
Zookeeper的版本作用就是类似于乐观锁机制,用于实现乐观锁机制的“写入校验”,
在Zookeeper的内部实现,Zookeeper通过链式的processor来处理业务请求,每个processor负责处理特定的功能。不同的Zookeeper角色的服务器processor链是不一样的,在PrepRequestProcessor请求处理链中,在处理数据更新的时候会去检查版本。
PrepRequestProcessor:
Watcher-数据变更通知:
Zookeeper的Watcher机制主要包括客户端线程、客户端WatchManager和Zookeeper服务器三部分。在具体的流程上,客户端向Zookeeper服务器注册Watcher事件监听的同时,会将Watcher对象存储在 客户端WatchManager中。当Zookeeper服务器触发Watcher事件后,会向客户端发送通知,客户端线程从WatchManager中取出对应的Watcher对象执行回调逻辑。
客户端注册Watcher源码剖析
在开始下面的讲解之前,先了解一下几个概念:
Watcher:
Watcher接口类用于表示一个标准的事件处理器,定义事件通知的相关逻辑,包含KeeperState和EventType两个枚举,分别代表通知状态和事件类型,同时定义事件的回调方法:process方法;
WatcherEvent:
包含每一事件的三种基本属性:通知状态、事件类型、节点路径。Zookeeper使用Watcher对象来封装服务器端事件,并传递给Watcher,从而方便回调方法process对服务器事件进行处理。
Packet:
Packet是Zookeeper中用来通信的最小单元,所以任何需要网络进行传输的对象都需要包装成Packet对象。
SendThead:
SendThread是Zookeeper中专门用来接收事件通知的线程,当服务端响应了客户端的请求后,会交给SendThread处理。
EventThread:
EventThread是Zookeeper专门用来处理事件通知的线程,SendThread在接收到通知事件后会将事件传给EventThread进行处理。
ServerCnxn:
ServerCnxn代表的是一个客户端和服务端的连接,客户端像服务端注册Watcher的时候,并不会真正将Watcher对象传递到服务端,而服务端也仅仅是保存了与当前客户端连接的ServerCnxn对象。默认实现是NIOServerCNXN.,3.4.0版本开始引用Netty的实现:NettyServerCnxn.
WatcherManager:
Zookeeper服务端Watcher的管理者,其内部管理的WatchTable和Watch2Paths两个存储结构,两个纬度对Watcher进行管理:
1、watchTable是数据节点路径粒度来托管Watch
2、watch2Paths是从Watcher的粒度来控制事件触发需要触发的数据节点
Watcher 接口
同一个事件的类型在不同的通知状态中代表的含义有所不同:
NodeDataChanged事件:此处的变更包括数据节点内容和数据的版本号DateVersion。因此,对于Zookeeper来说,无论数据内容是否更改,
还是会触发这个事件的通知,一旦客户端调用了数据更新接口,且更新成功,就会更新dataversion值。
nodeChildrenCahnged事件会在数据节点的子节点列表发生 变更的时候被触发,这里说的子节点列表变化特指子节点个数和组成情况的变更,如新增和删除,而子节点内容的变化是不会触发这个事件的。
AuthFailed这个事件触发的条件并不是客户端会话没有权限,而是授权失败,就是使用了错误的schema进行授权。
process 方法是 Watcher 接口中的一个回调方法,当 ZooKeeper 向客户端发送一个 Watcher 事件通知时,客户端就会对相应的 process 方法进行回调,从而实现对事件的处理。
abstractpublicvoid process(WatchedEvent event);
process 方法包含 WatcherEvent 类型的参数,WatchedEvent 包含了每一个事件的三个基本属性:通知状态(KeeperState)、事件类型(EventType)和节点路径(Path),ZooKeeper 使用 WatchedEvent 对象来封装服务端事件并传递给 Watcher,从而方便回调方法 process 对服务端事件进行处理。
WatchedEvent 和 WatcherEvent 都表示的是同一个事物,都是对一个服务端事件的封装。不同的是,WatchedEvent 是一个逻辑事件,用于服务端和客户端程序执行过程中所需的逻辑对象,而 WatcherEvent 因为实现了序列化接口,因此可以用于网络传输。
服务端在线程 WatchedEvent 事件之后,会调用 getWrapper 方法将自己包装成一个可序列化的 WatcherEvent 事件,以便通过网络传输到客户端。客户端在接收到服务端的这个事件对象后,首先会将 WatcherEvent 事件还原成一个 WatchedEvent 事件,并传递给 process 方法处理,回调方法 process 根据入参就能够解析出完整的服务端事件了。
注:Zookeeper Watcher的一个重要特性:客户端无法直接从WatchedEvent事件中获取到对应数据节点的原始数据内容,以及变更后的数据内容,而是客户端再次主动去重新获取数据。
Zookeeper Watcher工作机制:
客户端注册 Watcher :
这个 Watcher 将作为整个 ZooKeeper 会话期间的默认 Watcher,会一直被保存在客户端 ZKWatchManager 的 defaultWatcher 里面。另外Zookeeper客户端也可以通过getData、getChildren和Exist三个接口向Zookeeper服务器注册Watcher.
在向getData接口注册Watcher后,客户端首先会向客户端请求request进行标记,将其设置为“使用watch监听”,同时会封装一个Watcher的注册信息DataWatchRegistration对象,用于暂时保存数据节点的路径和Watcher的对应关系;
publicbyte[] 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 = newDataWatchRegistration(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 是一个最小的通信协议单元,即数据包。Pakcet 用于进行客户端与服务端之间的网络传输,任何需要传输的对象都需要包装成一个 Packet 对象。在 ClientCnxn 中 WatchRegistration 也会被封装到 Pakcet 中,然后由 SendThread 线程调用queuePacket方法把 Packet 放入发送队列中等待客户端发送,这又是一个异步过程,分布式系统采用异步通信是一个普遍认同的观念。
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;
}
Packet queuePacket(RequestHeader h, ReplyHeader r, Record request,
Record response, AsyncCallback cb, String clientPath,
String serverPath, Object ctx, WatchRegistration watchRegistration)
{
Packet packet = null;
// Note that we do not generate the Xid for the packet yet. It is
// generated later at send-time, by an implementation of ClientCnxnSocket::doIO(),
// where the packet is actually sent.
synchronized (outgoingQueue) {
packet = new Packet(h, r, request, response, watchRegistration);
packet.cb = cb;
packet.ctx = ctx;
packet.clientPath = clientPath;
packet.serverPath = serverPath;
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.add(packet);
}
}
sendThread.getClientCnxnSocket().wakeupCnxn();
return packet;
}
<span style="font-size:18px;">ClientCnXn -----> sendThread = new SendThread(clientCnxnSocket); 发送</span>
随后、Zookeeper客户端就会向服务端发送请求,同时等待请求的返回,完成请求发送后,会有客户端SendThread 线程的 readResponse 方法接收来自服务端的响应,异步地调用 finishPacket 方法从 Packet 中取出对应的 Watcher 并注册到 ZKWatchManager 中去:
void readResponse(ByteBuffer incomingBuffer) throws IOException {
............
} finally {
finishPacket(packet);
}
}
privatevoid finishPacket(Packet p) {
if (p.watchRegistration != null) {
p.watchRegistration.register(p.replyHeader.getErr());
}
if (p.cb == null) {
.........
} else {
p.finished = true;
eventThread.queuePacket(p);
}
}
从上面内容中,我们可以了解到客户端已经将Watcher暂时封装在watchRegistration对象中,现在需要从这个封装对象再次提取出Watcher来:
abstractprotected Map<String, Set<Watcher>> getWatches(int rc);
/**
* Register the watcher with the set of watches on path.
* @param rc the result code of the operation that attempted to
* add the watch on the path.
*/
publicvoidregister(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);
}
}
}
在register方法中,客户端会将之前暂时保存的Watcher对象转交给ZKWatchManager,并最终保存到dataWatches中,ZKWatchManager. dataWatches是一个Map<String, Set<Watcher>>类型的数据结构,用于将数据节点的路径与Watcher对象映射后管理起来。
整个客户端Watcher注册流程:
问题:
1、客户端每调用一次getData()接口,就会注册上一次Watcher,那么这些Watcher实体都会被客户端请求发送值服务端吗?
答:并不是;如果所有的Watcher都传递个服务端,那么服务端肯定会出现内存紧张或者其他性能问题,那么Zookeeper在watchRegistration封装到了Packet对象中去,在通过SendThread发送出去,事实上,在底层网络传输序列化过程中,并没有将watchRegistration对象完全的序列化到底层字节数组中,:
Packet:
publicvoid createBB() {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BinaryOutputArchive boa = BinaryOutputArchive.getArchive(baos);
boa.writeInt(-1, "len"); // We'll fill this in later
if (requestHeader != null) {
requestHeader.serialize(boa, "header");
}
if (request instanceof ConnectRequest) {
request.serialize(boa, "connect");
// append "am-I-allowed-to-be-readonly" flag
boa.writeBool(readOnly, "readOnly");
} else if (request != null) {
request.serialize(boa, "request");
}
baos.close();
this.bb = ByteBuffer.wrap(baos.toByteArray());
this.bb.putInt(this.bb.capacity() - 4);
this.bb.rewind();
} catch (IOException e) {
LOG.warn("Ignoring unexpected exception", e);
}
}
只是将requestHeader和request两个属性进行序列化传输。
服务端处理Watcher:
当服务端收到了客户端的请求后,如果客户端标记了需要使用Watcher监听,服务端会触发相应的事件,整个主干流程很简单,可以简单理解为下图的方式:
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) {
thrownew KeeperException.NoNodeException();
}
Long aclL;
synchronized(n) {
aclL = n.acl;
}
PrepRequestProcessor.checkACL(zks, zks.getZKDatabase().convertLong(aclL),
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;
}
对于注册 Watcher 请求,FinalRequestProcessor 的 ProcessRequest 方法会判断当前请求是否需要注册 Watcher,如果为 true,就会将当前的 ServerCnxn 对象和数据节点路径传入 getData 方法中去。ServerCnxn 是一个 ZooKeeper 客户端和服务器之间的连接接口,代表了一个客户端和服务器的连接,我们后面讲到的 process 回调方法,实际上也是从这里回调的,所以可以把 ServerCnxn 看作是一个 Watcher 对象。数据节点的节点路径和 ServerCnxn 最终会被存储在 WatchManager 的 watchTable 和 watch2Paths 中。在服务端,DataTree会托管两个WatchManager、分别是dataWatches、childWatches
对服务端Watcher的触发:当发生 Create、Delete、NodeChange(数据变更)这样的事件后,DataTree 会调用相应方法去触发 WatchManager 的 triggerWatch 方法,该方法返回 ZNODE 的信息,自此进入到回调本地 process 的序列。
public ProcessTxnResult processTxn(TxnHeader header, Record txn)
{
ProcessTxnResult rc = new ProcessTxnResult();
try {
......
switch (header.getType()) {
...
case OpCode.setData:
SetDataTxn setDataTxn = (SetDataTxn) txn;
rc.path = setDataTxn.getPath();
rc.stat = setData(setDataTxn.getPath(), setDataTxn
.getData(), setDataTxn.getVersion(), header
.getZxid(), header.getTime());
break;
.....
}
} catch (KeeperException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Failed: " + header + ":" + txn, e);
}
rc.err = e.code().intValue();
} catch (IOException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Failed: " + header + ":" + txn, e);
}
}
..............
return rc;
}
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) {
thrownew 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);
}
..............
dataWatches.triggerWatch(path, EventType.NodeDataChanged);
return s;
}<span style="color:#7f055;"><span style="font-size:14px;"><strong></strong></span></span>
在对指定节点进行数据更新后,通过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);
//如果不存在watchers直接返回。
if (watchers == null || watchers.isEmpty()) {
if (LOG.isTraceEnabled()) {
ZooTrace.logTraceMessage(LOG,
ZooTrace.EVENT_DELIVERY_TRACE_MASK,
"No watchers for " + path);
}
returnnull;
}
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;
}
//对于需要注册 Watcher 的请求,ZooKeeper 会把请求对应的 ServerCnxn 作为一个 Watcher 存储,所以这里调用的 process 方法实质上是 ServerCnxn 的对应方法
w.process(e);
}
return watchers;
}
/*
* (non-Javadoc)
*
* @see org.apache.zookeeper.server.ServerCnxnIface#process(org.apache.zookeeper.proto.WatcherEvent)
*/
@Override
synchronizedpublicvoidprocess(WatchedEvent event) {
ReplyHeader h = new ReplyHeader(-1, -1L, 0);
if (LOG.isTraceEnabled()) {
ZooTrace.logTraceMessage(LOG, ZooTrace.EVENT_DELIVERY_TRACE_MASK,
"Deliver event " + event + " to 0x"
+ Long.toHexString(this.sessionId)
+ " through " + this);
}
// Convert WatchedEvent to a type that can be sent over the wire
WatcherEvent e = event.getWrapper();
sendResponse(h, e, "notification");
}
从上面的代码我们可以总结出,如果想要处理一个 Watcher,需要执行的步骤如下所示:
1. 将事件类型(EventType)、通知状态(WatchedEvent)、节点路径封装成一个 WatchedEvent 对象。
2. 根据数据节点的节点路径从 watchTable 里面取出对应的 Watcher。如果没有找到 Watcher 对象,说明没有任何客户端在该数据节点上注册过 Watcher,直接退出。如果找到了 Watcher 就将其提取出来,同时会直接从 watchTable 和 watch2Paths 里删除 Watcher,即 Watcher 是一次性的,触发一次就失效了。
3. 对于需要注册 Watcher 的请求,ZooKeeper 会把请求对应的 ServerCnxn 作为一个 Watcher 存储,所以这里调用的 process 方法实质上是 ServerCnxn 的对应方法,如上 所示,在请求头标记“-1”表示当前是一个通知,将 WatchedEvent 包装成 WatcherEvent 用于网络传输序列化,向客户端发送通知,真正的回调方法在客户端,就是我们Watcher接口定义的 process() 方法。
客户端回调:
上述讲解了服务端如何进行Watcher触发、最终服务端通过使用ServerCnxn对应的TCP链接来向客户端发送一个WatcherEvent事件,下面看看客户端是如何处理这个事件的。
SendThread接收事件通知:
void readResponse(ByteBuffer incomingBuffer) 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
......
return;
}
if (replyHdr.getXid() == -4) {
// -4 is the xid for AuthPacket
......
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("/");
elseif (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);
if (LOG.isDebugEnabled()) {
LOG.debug("Got " + we + " for sessionid 0x"
+ Long.toHexString(sessionId));
}
eventThread.queueEvent( we );
return;
}
...............
} finally {
finishPacket(packet);
}
}
客户端收到消息后,会调用 ClientCnxn 的 SendThread.readResponse 方法来进行统一处理,如清单所示。如果响应头 replyHdr 中标识的 Xid 为 02,表示是 ping,如果为-4,表示是验证包,如果是-1,表示这是一个通知类型的响应,
1、然后进行反序列化、
2、处理 chrootPath、
需要对服务端传过来的完整路径,处理成客户端的一个相对路径。例如服务端响应的路径是:/app/test1,客户端设置的chrootPath=/app 经过处理后的相对路径则是/test1.
3、还原 WatchedEvent
需要将WatcherEvent对象转换成WatchedEvent。
4、 回调 Watcher 等步骤,其中回调 Watcher 步骤将 WacthedEvent 对象交给 EventThread 线程,在下一个轮询周期中进行 Watcher 回调。
EventThread处理事件通知:
SendThread 接收到服务端的通知事件后,会通过调用 EventThread 类的 queueEvent 方法将事件传给 EventThread 线程,queueEvent 方法根据该通知事件,从 ZKWatchManager 中取出所有相关的 Watcher,如果获取到相应的Watcher,就会让Watcher移除失效。
publicvoidqueueEvent(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);
}
/* (non-Javadoc)
* @see org.apache.zookeeper.ClientWatchManager#materialize(Event.KeeperState,
* Event.EventType, java.lang.String)
*/
@Override
public Set<Watcher> materialize(Watcher.Event.KeeperState state,
Watcher.Event.EventType type,
String clientPath)
{
Set<Watcher> result = new HashSet<Watcher>();
switch (type) {
。。。。
caseNodeDataChanged:
caseNodeCreated:
synchronized (dataWatches) {
addTo(dataWatches.remove(clientPath), result);
}
synchronized (existWatches) {
addTo(existWatches.remove(clientPath), result);
}
break;
caseNodeChildrenChanged:
synchronized (childWatches) {
addTo(childWatches.remove(clientPath), result);
}
break;
caseNodeDeleted:
synchronized (dataWatches) {
addTo(dataWatches.remove(clientPath), result);
}
// XXX This shouldn't be needed, but just in case
synchronized (existWatches) {
Set<Watcher> list = existWatches.remove(clientPath);
if (list != null) {
addTo(existWatches.remove(clientPath), result);
LOG.warn("We are triggering an exists watch for delete! Shouldn't happen!");
}
}
synchronized (childWatches) {
addTo(childWatches.remove(clientPath), result);
}
break;
。。。
return result;
}
}
客户端在识别出事件类型 EventType 之后,会从相应的 Watcher 存储中删除对应的 Watcher,获取到相关的 Watcher 之后,会将其放入 waitingEvents 队列,该队列从字面上就能理解是一个待处理队列,线程的 run 方法会不断对该该队列进行处理,这就是一种异步处理思维的实现。
EventThread:
//待处理Watcher队列,EventThred的run方法会不断对该队列进行处理:
privatefinal LinkedBlockingQueue<Object> waitingEvents =new LinkedBlockingQueue<Object>();
privatevoid 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);
}
}
} else {
.............................................
}
} catch (Throwable t) {
LOG.error("Caught unexpected throwable", t);
}
}
EventThred线程每次都会从waitingEvents队列中取出一个Watcher,并进行串行处理。processEvent 方法中的Watcher才是之前客户端真正注册的Watcher,调用其process就可以实现Watcher的回调了。
ZooKeeper Watcher 特性总结
1. 注册只能确保一次消费
无论是服务端还是客户端,一旦一个 Watcher 被触发,ZooKeeper 都会将其从相应的存储中移除。因此,开发人员在 Watcher 的使用上要记住的一点是需要反复注册。这样的设计有效地减轻了服务端的压力。如果注册一个 Watcher 之后一直有效,那么针对那些更新非常频繁的节点,服务端会不断地向客户端发送事件通知,这无论对于网络还是服务端性能的影响都非常大。
2. 客户端串行执行
客户端 Watcher 回调的过程是一个串行同步的过程,这为我们保证了顺序,同时,需要开发人员注意的一点是,千万不要因为一个 Watcher 的处理逻辑影响了整个客户端的 Watcher 回调。
3. 轻量级设计
WatchedEvent 是 ZooKeeper 整个 Watcher 通知机制的最小通知单元,这个数据结构中只包含三部分的内容:通知状态、事件类型和节点路径。也就是说,Watcher 通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。例如针对 NodeDataChanged 事件,ZooKeeper 的 Watcher 只会通知客户指定数据节点的数据内容发生了变更,而对于原始数据以及变更后的新数据都无法从这个事件中直接获取到,而是需要客户端主动重新去获取数据,这也是 ZooKeeper 的 Watcher 机制的一个非常重要的特性。另外,客户端向服务端注册 Watcher 的时候,并不会把客户端真实的 Watcher 对象传递到服务端,仅仅只是在客户端请求中使用 boolean 类型属性进行了标记,同时服务端也仅仅只是保存了当前连接的 ServerCnxn 对象。这样轻量级的 Watcher 机制设计,在网络开销和服务端内存开销上都是非常廉价的。