本文会从源码级别分析Zookeeper的Watch机制的底层原理,尽可能详细说明,如有错误或不清晰的地方,欢迎指正,谢谢!
本文中的ZK单指Zookeeper。
我们知道Zookeeper的临时节点可以用来实现分布式锁,多个客户端分别创建一个节点,创建成功即成功获取到了锁,创建失败的客户端们则会监听这个临时节点,获取锁的客户端释放锁(删除临时节点)或 与ZK服务端断开连接后(ZK会删除临时节点),其他客户端会收到Watch发来的通知,兄弟们,它释放锁了,你们过来抢占锁吧。
上面就是一个最原始的分布式锁(生产环境别用,惊群效应),我们看到这里用到了Watch机制来实现对客户端的通知。
惊群效应:当许多进程等待一个事件,事件发生后这些进程被唤醒,但只有一个进程能获得CPU执行权,其他进程又得被阻塞,这造成了严重的系统上下文切换代价。(来自维基百科)
在一些分布式场景中,我们正常只需要某一台机器参与分布式的工作中(其实就是往ZK服务端创建临时节点,这个过程与上面描述的简易版分布式锁类似,某个机器成功创建一个临时节点后即被选为工作机器,其他节点创建失败后则监听这个节点),当机器异常宕机后,Zookeeper会利用Watch机制通知其他机器继续来创建临时节点,且只有一台机器能创建成功,那么其他机器又会监听这个节点;而创建节点成功的机器则会代替之前宕机的机器对外提供服务。
这就是利用ZK解决分布式场景中单点问题的一个案例。
从前文的描述中,我们不难看到ZK的Watch机制是一个比较核心且应用广泛的功能,那么我们就来分析一下ZK内部是如何实现Watch机制的。
Watch机制是怎么实现的?
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.1</version>
</dependency>
首先需要引入pom依赖(ZK原生的客户端API)
如何使用Watch机制
ZooKeeper 类建立连接
本部分会将ZooKeeper类与ZK服务端建立连接的过程阐述一下,会涉及到Watch对象的初始化,具体的底层原理会在后面章节说明。
ZooKeeper 的客户端可以通过 Watch 机制来订阅当服务器上某一节点的数据或状态发生变化时收到相应的通知
public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher) throws IOException {
this(connectString, sessionTimeout, watcher, false);
}
- connectString : ZK服务端的地址
- sessionTimeout : 连接超时时间
- watcher : 监控事件
这个 Watcher 将作为整个 ZooKeeper 会话期间的上下文 ,一直被保存在客户端 ZKWatchManager 的 defaultWatcher 中。
protected final ZKWatchManager watchManager;
private final ZKClientConfig clientConfig;
protected final HostProvider hostProvider;
protected final ClientCnxn cnxn;
public ZooKeeper(
String connectString,
int sessionTimeout,
Watcher watcher,
boolean canBeReadOnly,
HostProvider aHostProvider,
ZKClientConfig clientConfig) throws IOException {
...
if (clientConfig == null) {
// 1. 提供一下默认配置
clientConfig = new ZKClientConfig();
}
this.clientConfig = clientConfig;
// 2. 根据ZKClientConfig创建一个ZKWatchManager对象
watchManager = defaultWatchManager();
watchManager.defaultWatcher = watcher;
// 3. 创建ConnectStringParser对象
ConnectStringParser connectStringParser = new ConnectStringParser(connectString);
hostProvider = aHostProvider;
// 4. 建立连接
cnxn = createConnection(
connectStringParser.getChrootPath(),
hostProvider,
sessionTimeout,
this,
watchManager,
getClientCnxnSocket(),
canBeReadOnly);
cnxn.start();
}
构建ZKClientConfig
public ZKClientConfig() {
super();
initFromJavaSystemProperties();
}
private final Map<String, String> properties = new HashMap<String, String>();
public static final String ZOOKEEPER_REQUEST_TIMEOUT = "zookeeper.request.timeout";
public static final String ZOOKEEPER_SERVER_PRINCIPAL = "zookeeper.server.principal";
// ZKConfig
protected void handleBackwardCompatibility() {
properties.put(JUTE_MAXBUFFER, System.getProperty(JUTE_MAXBUFFER));
properties.put(KINIT_COMMAND, System.getProperty(KINIT_COMMAND));
properties.put(JGSS_NATIVE, System.getProperty(JGSS_NATIVE));
try (ClientX509Util clientX509Util = new ClientX509Util()) {
putSSLProperties(clientX509Util);
properties.put(clientX509Util.getSslAuthProviderProperty(), System.getProperty(clientX509Util.getSslAuthProviderProperty()));
}
try (X509Util x509Util = new QuorumX509Util()) {
putSSLProperties(x509Util);
}
}
/**
* Initialize all the ZooKeeper client properties which are configurable as
* java system property
*/
private void initFromJavaSystemProperties() {
setProperty(ZOOKEEPER_REQUEST_TIMEOUT, System.getProperty(ZOOKEEPER_REQUEST_TIMEOUT));
setProperty(ZOOKEEPER_SERVER_PRINCIPAL,System.getProperty(ZOOKEEPER_SERVER_PRINCIPAL));
}
public void setProperty(String key, String value) {
if (null == key) {
throw new IllegalArgumentException("property key is null.");
}
String oldValue = properties.put(key, value);
if (null != oldValue && !oldValue.equals(value)) {
LOG.debug("key {}'s value {} is replaced with new value {}", key, oldValue, value);
}
}
构建默认的ZKClientConfig
-
ZKConfig#handleBackwardCompatibility-
private final Map<String, String> properties = new HashMap<String, String>();public static final String JUTE_MAXBUFFER = "jute.maxbuffer"; public static final String KINIT_COMMAND = "zookeeper.kinit"; public static final String JGSS_NATIVE = "sun.security.jgss.native"; ... properties.put(JUTE_MAXBUFFER, System.getProperty(JUTE_MAXBUFFER)); properties.put(KINIT_COMMAND, System.getProperty(KINIT_COMMAND)); properties.put(JGSS_NATIVE, System.getProperty(JGSS_NATIVE)); ...设置属性
-
-
ZKClientConfig#initFromJavaSystemPropertiespublic static final String ZOOKEEPER_REQUEST_TIMEOUT = "zookeeper.request.timeout"; public static final String ZOOKEEPER_SERVER_PRINCIPAL = "zookeeper.server.principal"; setProperty(ZOOKEEPER_REQUEST_TIMEOUT, System.getProperty(ZOOKEEPER_REQUEST_TIMEOUT)); setProperty(ZOOKEEPER_SERVER_PRINCIPAL, System.getProperty(ZOOKEEPER_SERVER_PRINCIPAL));
创建ZKWatchManager
watchManager = defaultWatchManager();
// 设置defaultWatcher
watchManager.defaultWatcher = watcher;
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 final Map<String, Set<Watcher>> persistentWatches = new HashMap<String, Set<Watcher>>();
private final Map<String, Set<Watcher>> persistentRecursiveWatches = new HashMap<String, Set<Watcher>>();
private boolean disableAutoWatchReset;
ZKWatchManager(boolean disableAutoWatchReset) {
this.disableAutoWatchReset = disableAutoWatchReset;
}
protected volatile Watcher defaultWatcher;
...
}
ConnectStringParser
通过拆分客户端
connectString来解析主机和端口,并支持IPv6文字
private static final int DEFAULT_PORT = 2181;
private final String chrootPath;
private final ArrayList<InetSocketAddress> serverAddresses = new ArrayList<InetSocketAddress>();
public ConnectStringParser(String connectString) {
// parse out chroot, if any
int off = connectString.indexOf('/');
if (off >= 0) {
String chrootPath = connectString.substring(off);
// ignore "/" chroot spec, same as null
if (chrootPath.length() == 1) {
this.chrootPath = null;
} else {
PathUtils.validatePath(chrootPath);
this.chrootPath = chrootPath;
}
connectString = connectString.substring(0, off);
} else {
this.chrootPath = null;
}
List<String> hostsList = split(connectString, ",");
for (String host : hostsList) {
int port = DEFAULT_PORT;
try {
String[] hostAndPort = ConfigUtils.getHostAndPort(host);
host = hostAndPort[0];
if (hostAndPort.length == 2) {
port = Integer.parseInt(hostAndPort[1]);
}
} catch (ConfigException e) {
e.printStackTrace();
}
serverAddresses.add(InetSocketAddress.createUnresolved(host, port));
}
}
ClientCnxn
此类管理客户端的套接字I / O。 ClientCnxn维护可以连接到的可用服务器列表,并根据需要“透明地”交换与之连接的服务器。说白了,这个类管理客户端与ZK服务端的连接。
public ClientCnxn(
String chrootPath,
HostProvider hostProvider,
int sessionTimeout,
ZooKeeper zooKeeper,
ClientWatchManager watcher,
ClientCnxnSocket clientCnxnSocket,
long sessionId,
byte[] sessionPasswd,
boolean canBeReadOnly) {
this.zooKeeper = zooKeeper;
this.watcher = watcher;
this.sessionId = sessionId;
this.sessionPasswd = sessionPasswd;
this.sessionTimeout = sessionTimeout;
this.hostProvider = hostProvider;
this.chrootPath = chrootPath;
connectTimeout = sessionTimeout / hostProvider.size();
readTimeout = sessionTimeout * 2 / 3;
readOnly = canBeReadOnly;
sendThread = new SendThread(clientCnxnSocket);
eventThread = new EventThread();
this.clientConfig = zooKeeper.getClientConfig();
initRequestTimeout();
}
初始化一些属性。sendThread/eventThread比较重要
设置两个线程为守护线程
SendThread(ClientCnxnSocket clientCnxnSocket) {
super(makeThreadName("-SendThread()"));
state = States.CONNECTING;
this.clientCnxnSocket = clientCnxnSocket;
setDaemon(true);
}
EventThread() {
super(makeThreadName("-EventThread"));
setDaemon(true);
}
final SendThread sendThread;
final EventThread eventThread;
public void start() {
sendThread.start();
eventThread.start();
}
启动发送线程与事件线程。
我们看到
ZooKeeper类与ZK服务端建立连接实际上是先准备好参数,然后启动两个线程,所以说建立连接是异步的方式。
API的方式
除此之外,ZooKeeper 客户端也可以通过 getData、exists 和 getChildren 三个接口来向 ZooKeeper 服务器注册 Watcher,从而方便地在不同的情况下添加 Watch 事件:
/**
* The asynchronous version of getData.
*
* @see #getData(String, boolean, Stat)
*/
public void getData(String path, boolean watch, DataCallback cb, Object ctx) {
getData(path, watch ? watchManager.defaultWatcher : null, cb, ctx);
}
public Stat exists(String path, boolean watch) throws KeeperException, InterruptedException {
return exists(path, watch ? watchManager.defaultWatcher : null);
}
public List<String> getChildren(String path, boolean watch) throws KeeperException, InterruptedException {
return getChildren(path, watch ? watchManager.defaultWatcher : null);
}
状态和事件
interface Event {
/**
* Enumeration of states the ZooKeeper may be at the event
*/
@InterfaceAudience.Public
enum KeeperState {
@Deprecated
Unknown(-1),
Disconnected(0),
@Deprecated
NoSyncConnected(1),
SyncConnected(3),
AuthFailed(4),
ConnectedReadOnly(5),
SaslAuthenticated(6),
Expired(-112),
Closed(7);
}
...
}
DisconnectedEventType.None
SyncConnectedEventType.NodeCreatedEventType.NodeDeletedEventType.NodeDataChangedEventType.NodeChildrenChanged
AuthFailedEventType.None
ExpiredEventType.None
客户端在不同会话状态下,相应的在服务器节点所能支持的事件类型。例如在客户端连接服务端的时候,可以对数据节点的创建、删除、数据变更、子节点的更新等操作进行监控。
前文是对ZK的Watch机制在应用层(API)上的说明,接下来我们来探究一下Watcher机制的底层原理
Watch机制的底层原理
Watch机制看起来很像设计模式中的“观察者模式”:在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。
我们可以将Watch机制认为是分布式场景下的“观察者模式”。
通常我们在实现观察者模式时,最核心或者说关键的代码就是创建一个列表来存放观察者。
而在 ZooKeeper 中则是在客户端和服务器端分别实现两个存放观察者列表,即:ZKWatchManager 和 WatchManager。
客户端Watch注册实现过程
我们以getData方法为例,理解一下客户端Watch注册的原理
ZooKeeper#getData(String path, Watcher watcher, Stat stat)
当发送一个带有 Watch 事件的请求时,客户端首先会把该会话标记为带有 Watch 监控的事件请求,之后通过
DataWatchRegistration类来保存 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) {
// 创建DataWatchRegistration对象
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();
}
客户端向服务端发送请求时,是将请求封装成了Packet对象,并添加到一个等待发送队列 outgoingQueue
public ReplyHeader submitRequest(
RequestHeader h,
Record request,
Record response,
WatchRegistration watchRegistration,
WatchDeregistration watchDeregistration) throws InterruptedException {
ReplyHeader r = new ReplyHeader();
Packet packet = queuePacket(
h,
r,
request,
response,
null,
null,
null,
null,
watchRegistration,
watchDeregistration);
synchronized (packet) {
if (requestTimeout > 0) {
// Wait for request completion with timeout
waitForPacketFinish(r, packet);
} else {
// Wait for request completion infinitely
while (!packet.finished) {
packet.wait();
}
}
}
if (r.getErr() == Code.REQUESTTIMEOUT.intValue()) {
sendThread.cleanAndNotifyState();
}
return r;
}
ClientCnxn#queuePacket(...)
public Packet queuePacket(...){
Packet packet = null;
packet = new Packet(h, r, request, response, watchRegistration);
...
packet.watchDeregistration = watchDeregistration;
...
outgoingQueue.add(packet);
...
sendThread.getClientCnxnSocket().packetAdded();
}
最后,ZooKeeper 客户端就会向服务器端发送这个请求(Java NIO/Netty),完成请求发送后。调用负责处理服务器响应的 SendThread 线程类中的 readResponse 方法接收服务端的回调,并在最后执行 finishPacket()方法将 Watch 注册到 ZKWatchManager 中。
ZooKeeper客户端底层采用Java原生NIO和Netty两种方式来发送请求(具体发送请求的细节不再阐述,读者可以参考代码)
SendThread#readResponse
// org.apache.zookeeper.ClientCnxn.SendThread#readResponse
void readResponse(ByteBuffer incomingBuffer) throws IOException {
...
finishPacket(packet);
}
ClientCnxn#finishPacket
protected void finishPacket(Packet p) {
int err = p.replyHeader.getErr();
if (p.watchRegistration != null) {
p.watchRegistration.register(err);
}
...
}
ZooKeeper.WatchRegistration#register
public void register(int rc) {
if (shouldAddWatch(rc)) {
// 获取Watcher Map
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);
}
// 注册Watch
watchers.add(watcher);
}
}
}
DataWatchRegistration#getWatches
class DataWatchRegistration extends WatchRegistration {
public DataWatchRegistration(Watcher watcher, String clientPath) {
super(watcher, clientPath);
}
@Override
protected Map<String, Set<Watcher>> getWatches(int rc) {
return watchManager.dataWatches;
}
}
watchManager来自于Zookeeper类
protected final ZKWatchManager watchManager;
我们再来看ZKWatchManager这个类包含的Map属性:
static class ZKWatchManager implements ClientWatchManager {
// 获取的Map
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 final Map<String, Set<Watcher>> persistentWatches = new HashMap<String, Set<Watcher>>();
private final Map<String, Set<Watcher>> persistentRecursiveWatches = new HashMap<String, Set<Watcher>>();
private boolean disableAutoWatchReset;
...
}
这些Map分别包含了不同类别的Watch 信息: dataWatches、existWatches、childWatches等等。
总结
-
客户端发送请求首先会将发送信息封装成
Packet对象 -
发送时会采用Java NIO/Netty的方式,如果注册了Watch,会向服务端发送Watch信息
-
发送完成后,客户端通过回调方法获取
ZKWatchManager中对应的Watch Map ,在Map中注册相关的Watch注意getWatch的过程没有加锁,注册
Watch时通过synchronized关键字来保证原子性。因为只有写操作下才有可能出现线程安全问题。
服务端Watch注册实现过程
-
当ZooKeeper服务端接收到一个客户端请求时,首先会对请求进行解析,判断该请求是否包含Watch事件,如果包含Watch事件,则将Watch事件存储到
WatchManager中在ZooKeeper底层是通过
FinalRequestProcessor#processRequest方法来实现的for (Op readOp : multiReadRecord) { try { Record rec; switch (readOp.getType()) { case OpCode.getChildren: rec = handleGetChildrenRequest(readOp.toRequestRecord(), cnxn, request.authInfo); subResult = new GetChildrenResult(((GetChildrenResponse) rec).getChildren()); break; case OpCode.getData: // 处理请求 rec = handleGetDataRequest(readOp.toRequestRecord(), cnxn, request.authInfo); GetDataResponse gdr = (GetDataResponse) rec; subResult = new GetDataResult(gdr.getData(), gdr.getStat()); break; default: throw new IOException("Invalid type of readOp"); } } catch (KeeperException e) { subResult = new ErrorResult(e.code().intValue()); } ((MultiResponse) rsp).add(subResult); } break; ...
FinalRequestProcessor#handleGetDataRequest
private Record handleGetDataRequest(Record request, ServerCnxn cnxn, List<Id> authInfo) throws KeeperException, IOException {
GetDataRequest getDataRequest = (GetDataRequest) request;
// 获取path
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();
byte[] b = zks.getZKDatabase().getData(path, stat, getDataRequest.getWatch() ? cnxn : null);
return new GetDataResponse(b, stat);
}
getDataRequest.getWatch()返回true时,表示该请求需要进行Watch监控注册,并通过zks.getZKDatabase().getData将Watch事件注册到服务端的WatchManager中。我们可以把
ServerCnxn认为是从客户端到服务器的连接,我理解成客户端的Watch事件
public abstract class ServerCnxn implements Stats, Watcher {
ZKDatabase#getData
public byte[] getData(String path, Stat stat, Watcher watcher) throws KeeperException.NoNodeException {
return dataTree.getData(path, stat, watcher);
}
// DataTree#getData
private IWatchManager dataWatches;
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) {
// 注册Watch
dataWatches.addWatch(path, watcher);
}
data = n.data;
}
updateReadStat(path, data == null ? 0 : data.length);
return data;
}
服务端Watch事件的触发时机
服务端执行setData方法对节点数据进行变更后,会调用 WatchManager.triggerWatch 方法触发数据变更事件。
public Stat setData(String path, byte data[], ...){
Stat s = new Stat();
DataNode n = nodes.get(path);
...
dataWatches.triggerWatch(path, EventType.NodeDataChanged);
return s;
}
WatchManager#triggerWatch
首先,封装了一个具有会话状态、事件类型、数据节点 3 种属性的 WatchedEvent 对象。
之后查询该节点注册的 Watch 事件,如果为空说明该节点没有注册过 Watch 事件。如果存在 Watch 事件则添加到定义的 Watchers 集合中,并在 WatchManager 管理中删除。
最后,通过调用 process 方法向客户端发送通知。
public WatcherOrBitSet triggerWatch(String path, EventType type, WatcherOrBitSet supress) {
// 封装WatchedEvent对象 type:事件类型
// KeeperState.SyncConnected: 会话状态 path:数据节点
WatchedEvent e = new WatchedEvent(type, KeeperState.SyncConnected, path);
Set<Watcher> watchers = new HashSet<>();
PathParentIterator pathParentIterator = getPathParentIterator(path);
synchronized (this) {
for (String localPath : pathParentIterator.asIterable()) {
// 获取Watchers Set
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);
if (!watcherMode.isPersistent()) {
iterator.remove();
Set<String> paths = watch2Paths.get(watcher);
if (paths != null) {
paths.remove(localPath);
}
}
}
}
if (thisWatchers.isEmpty()) {
watchTable.remove(localPath);
}
}
}
if (watchers.isEmpty()) {
...
return null;
}
// 遍历添加的Watcher
for (Watcher w : watchers) {
if (supress != null && supress.contains(w)) {
continue;
}
// 向客户端发送通知
w.process(e);
}
...
return new WatcherOrBitSet(watchers);
}
客户端回调的处理过程
客户端使用 SendThread.readResponse() 方法来统一处理服务端的响应。
- 首先反序列化服务器发送请求头信息 replyHdr.deserialize(bbia, "header"),并判断相属性字段 xid 的值为 -1,表示该请求响应为通知类型。在处理通知类型时,首先将己收到的字节流反序列化转换成
WatcherEvent对象。 - 接着判断客户端是否配置了 chrootPath 属性,如果为
true说明客户端配置了 chrootPath 属性。需要对接收到的节点路径进行 chrootPath 处理。 - 最后调用
eventThread.queueEvent()方法将接收到的事件交给EventThread线程进行处理。
if (replyHdr.getXid() == -1) {
...
WatcherEvent event = new WatcherEvent();
// 反序列化服务器发送请求头信
event.deserialize(bbia, "response");
...
if (chrootPath != null) {
String serverPath = event.getPath();
if(serverPath.compareTo(chrootPath)==0)
event.setPath("/");
...
event.setPath(serverPath.substring(chrootPath.length()));
...
}
WatchedEvent we = new WatchedEvent(event);
...
eventThread.queueEvent( we );
}
我们只需要关注
eventThread.queueEvent()方法即可。
EventThread.queueEvent
private void queueEvent(WatchedEvent event, Set<Watcher> materializedWatchers) {
if (event.getType() == EventType.None && sessionState == event.getState()) {
return;
}
sessionState = event.getState();
// 获取注册过的Watcher事件
final Set<Watcher> watchers;
if (materializedWatchers == null) {
// materialize the watchers based on the event
watchers = watcher.materialize(event.getState(), event.getType(), event.getPath());
} else {
watchers = new HashSet<Watcher>();
watchers.addAll(materializedWatchers);
}
WatcherSetEventPair pair = new WatcherSetEventPair(watchers, event);
// queue the pair (watch set & event) for later processing
// 将Watcher事件添加到waitingEvents中
waitingEvents.add(pair);
}
ZooKeeper.ZKWatchManager#materialize
public Set<Watcher> materialize(...)
{
Set<Watcher> result = new HashSet<Watcher>();
...
// type: 事件类型
switch (type) {
...
case NodeDataChanged:
case NodeCreated:
synchronized (dataWatches) {
addTo(dataWatches.remove(clientPath), result);
}
synchronized (existWatches) {
addTo(existWatches.remove(clientPath), result);
}
break;
....
}
return result;
}
第 1 步按照通知的事件类型,从 ZKWatchManager 中查询注册过的客户端 Watch 信息。客户端在查询到对应的 Watch 信息后,会将其从 ZKWatchManager 的管理中删除。
客户端的 Watcher 机制是一次性的,触发后就会被删除。
class EventThread extends ZooKeeperThread {
private final LinkedBlockingQueue<Object> waitingEvents = new LinkedBlockingQueue<Object>();
...
waitingEvents是EventThread中的一个阻塞队列,从ZKWatchManager中查询注册过的客户端 Watch 信息会随后被添加到waitingEvents中。
而EventThread 类中的 run 方法会循环取出在 waitingEvents 队列中等待的 Watcher 事件进行处理。
这里可以理解成在循环中不断的获取要处理的Watcher事件处理
public void run() {
try {
isRunning = true;
// 无限循环
while (true) {
// 取出事件
Object event = waitingEvents.take();
if (event == eventOfDeath) {
wasKilled = true;
} else {
// 处理事件
processEvent(event);
}
if (wasKilled)
synchronized (waitingEvents) {
if (waitingEvents.isEmpty()) {
isRunning = false;
break;
}
}
}
...
}
**processEvent(event) **
private void processEvent(Object event) {
...
if (event instanceof WatcherSetEventPair) {
WatcherSetEventPair pair = (WatcherSetEventPair) event;
for (Watcher watcher : pair.watchers) {
try {
// 回调事件监听的处理
watcher.process(pair.event);
} catch (Throwable t) {
LOG.error("Error while calling watcher ", t);
}
}
}
}
总结
ZooKeeper 实现Watch机制是通过客户端和服务端分别创建有观察者的信息列表。
- 客户端调用 getData、exist 等接口时,首先将对应的 Watch 事件放到本地的
ZKWatchManager中进行管理。- 服务端在接收到客户端的请求后根据请求类型判断是否含有
Watch事件,并将对应事件放到WatchManager中进行管理。
在事件触发的时候服务端通过节点的路径信息查询相应的 Watch 事件通知给客户端,客户端在接收到通知后,首先查询本地的 ZKWatchManager 获得对应的 Watch 信息处理回调操作。
这种设计不但实现了一个分布式环境下的观察者模式,而且通过将客户端和服务端各自处理 Watch 事件所需要的额外信息分别保存在两端,减少彼此通信的内容,大大提升了服务的处理性能。

Watch机制的应用
配置中心
我们可以把诸如数据库配置项这样的信息存储在 ZooKeeper 数据节点中。服务器集群客户端对该节点添加 Watch 事件监控,当集群中的服务启动时,会读取该节点数据获取数据配置信息。而当该节点数据发生变化时,ZooKeeper 服务器会发送 Watch 事件给各个客户端(推),集群中的客户端在接收到该通知后,重新读取节点的数据库配置信息(拉)。
ZooKeeper实现的是推拉结合的机制
注册中心
Dubbo项目中常采用ZooKeeper作为注册中心(CP),原理和配置中心类似,其与Eureka的AP设计形成了鲜明的对比。