前言

  本章讲ZooKeeper重要的机制,Watcher特性。ZooKeeper允许客户端向服务端注册Watcher监听,当服务端一些指定事件触发了这个Watcher,那么就会向指定客户端发送一个事件通知客户端执行回调逻辑

一.Watcher机制

  ZooKeeper允许客户端向服务端注册感兴趣的Watcher监听,当服务端触发了这个Watcher,那么就会向客户端发送一个时间来实现分布式的通知功能。真正的Watcher回调与业务逻辑执行都在客户端

  那么需要注意一下,给客户端的通知里只会告诉你通知状态(KeeperState),事件类型(EventType)和路径(Path)。不会告诉你原始数据和更新过后的数据!

  Watcher机制包括三部分:注册、存储、通知

  1. 注册:注册Watcher
  2. 存储:讲Watcher对象存在客户端的WatcherManager中
  3. 通知:服务端触发Watcher事件,通知客户端,客户端从WatcherManager中取出对应的Watcher对象执行回调

  那么接下来,我们就分这3步来分析:

注册

  我们可以通过以下方式向服务端注册Watcher,主要是构造参数、getData、getChildren和exists方法:

public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)
public byte[] getData(String path, Watcher watcher, Stat stat)
public List<String> getChildren(String path, Watcher watcher)
public Stat exists(String path, Watcher watcher)

  我们就看getData方法,从源码角度看看如何注册的,可以看到首先封装了一个WatchRegistration对象,保存了节点的路径和Watcher对象的关系,然后在请求的request设置了是否有watcher这么一个boolean的成员变量:

public byte[] getData(String path, Watcher watcher, Stat stat) throws KeeperException, InterruptedException {
PathUtils.validatePath(path); // 封装一个WatcherRegistration的对象,保存节点路径和Watcher的对应关系
ZooKeeper.WatchRegistration wcb = null;
if (watcher != null) {
wcb = new ZooKeeper.DataWatchRegistration(watcher, path);
}
String serverPath = this.prependChroot(path);
RequestHeader h = new RequestHeader();
h.setType(4);
GetDataRequest request = new GetDataRequest();
request.setPath(serverPath); // 标记是否有watcher
request.setWatch(watcher != null);
GetDataResponse response = new GetDataResponse();
ReplyHeader r = this.cnxn.submitRequest(h, request, response, wcb);
if (r.getErr() != 0) {
throw KeeperException.create(Code.get(r.getErr()), path);
} else {
if (stat != null) {
DataTree.copyStat(response.getStat(), stat);
}
return response.getData();
}
} class DataWatchRegistration extends ZooKeeper.WatchRegistration {
// 保存节点路径和Watcher的关系
public DataWatchRegistration(Watcher watcher, String clientPath) {
super(watcher, clientPath);
} ...
} abstract class WatchRegistration {
private Watcher watcher;
private String clientPath; public WatchRegistration(Watcher watcher, String clientPath) {
this.watcher = watcher;
this.clientPath = clientPath;
}
...
}

  然后我们继续接着看这个wcb变量做了什么(已经用紫色标注该变量),简单来说就是这个变量被封装在了packet对象里,packet可以看成一个最小的通信协议单元,传输信息。最后将packet对象放到了发送队列里SendThread里

public ReplyHeader submitRequest(RequestHeader h, Record request, Record response, WatchRegistration watchRegistration) throws InterruptedException {
ReplyHeader r = new ReplyHeader();
// 客户端与服务端的网络传输
ClientCnxn.Packet packet = this.queuePacket(h, r, request, response, (AsyncCallback)null, (String)null, (String)null, (Object)null, watchRegistration);
synchronized(packet) {
while(!packet.finished) {
packet.wait();
}
return r;
}
} ClientCnxn.Packet queuePacket(RequestHeader h, ReplyHeader r, Record request, Record response, AsyncCallback cb, String clientPath, String serverPath, Object ctx, WatchRegistration watchRegistration) {
ClientCnxn.Packet packet = null;
LinkedList var11 = this.outgoingQueue;
synchronized(this.outgoingQueue) {
// 任何传输的对象都包装成Packet对象
packet = new ClientCnxn.Packet(h, r, request, response, watchRegistration);
packet.cb = cb;
packet.ctx = ctx;
packet.clientPath = clientPath;
packet.serverPath = serverPath;
if (this.state.isAlive() && !this.closing) {
if (h.getType() == -11) {
this.closing = true;
} // 放入发送队列中,等待发送
this.outgoingQueue.add(packet);
} else {
this.conLossPacket(packet);
}
} this.sendThread.getClientCnxnSocket().wakeupCnxn();
return packet;
}

  然后我们看org.apache.zookeeper.ClientCnxnSocketNIO#doIO这个方法,关键代码已经用红色标注出来了,从要发送的队列outgoingQueue中取出packet然后序列化到底层数组,注意了,这里没有序列化前面说的WatchRegistration对象,只序列化了requestHeader和request两个属性,也就是说,服务端并不会接收到阶段路径和watcher对象的关系,回调的业务逻辑代码也不会给服务端!

void doIO(List<Packet> pendingQueue, LinkedList<Packet> outgoingQueue, ClientCnxn cnxn) throws InterruptedException, IOException {
SocketChannel sock = (SocketChannel)this.sockKey.channel();
if (sock == null) {
throw new IOException("Socket is null!");
} else {
// 是否可读
if (this.sockKey.isReadable()) {
...
} if (this.sockKey.isWritable()) {
synchronized(outgoingQueue) {
Packet p = this.findSendablePacket(outgoingQueue, cnxn.sendThread.clientTunneledAuthenticationInProgress());
if (p != null) {
this.updateLastSend();
if (p.bb == null) {
if (p.requestHeader != null && p.requestHeader.getType() != 11 && p.requestHeader.getType() != 100) {
p.requestHeader.setXid(cnxn.getXid());
} // 序列化
p.createBB();
} sock.write(p.bb);
...
} ...
}
}
}
} public void createBB() {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BinaryOutputArchive boa = BinaryOutputArchive.getArchive(baos);
boa.writeInt(-1, "len"); // 序列化header
if (this.requestHeader != null) {
this.requestHeader.serialize(boa, "header");
}
if (this.request instanceof ConnectRequest) {
this.request.serialize(boa, "connect");
boa.writeBool(this.readOnly, "readOnly"); // 序列化request
} else if (this.request != null) {
this.request.serialize(boa, "request");
}
baos.close();
this.bb = ByteBuffer.wrap(baos.toByteArray());
this.bb.putInt(this.bb.capacity() - 4);
this.bb.rewind();
} catch (IOException var3) {
ClientCnxn.LOG.warn("Ignoring unexpected exception", var3);
}
}

存储

  上面都是客户端发起请求的过程,那么接下来我们看服务端接收到请求会做些什么,ZooKeeper的服务端对于客户端的请求,采用了典型的责任链模式,也就是说客户端的每个请求都由几个不同的处理器来依次进行处理,我们这里就看这个方法:org.apache.zookeeper.server.FinalRequestProcessor#processRequest

public void processRequest(Request request) {
...
PrepRequestProcessor.checkACL(this.zks, this.zks.getZKDatabase().convertLong(aclG), 1, request.authInfo);
Stat stat = new Stat();
// 这里根据客户端设置的是否有watch变量来传入watcher对象
// 如果true则将当前的ServerCnxn传入(ServerCnxn代表客户端和服务端的连接)
byte[] b = this.zks.getZKDatabase().getData(getDataRequest.getPath(), stat, getDataRequest.getWatch() ? cnxn : null);
rsp = new GetDataResponse(b, stat);
...
} public byte[] getData(String path, Stat stat, Watcher watcher) throws NoNodeException {
return this.dataTree.getData(path, stat, watcher);
}

  紧接着,将数据节点路径和ServerCnxn对象存储在WatcherManager的watchTable和watch2Paths中。前者是路径维度,后者是Watcher维度

public byte[] getData(String path, Stat stat, Watcher watcher) throws NoNodeException {
DataNode n = (DataNode)this.nodes.get(path);
if (n == null) {
throw new NoNodeException();
} else {
synchronized(n) {
n.copyStat(stat);
if (watcher != null) {
// 添加watcher
this.dataWatches.addWatch(path, watcher);
} return n.data;
}
}
} public synchronized void addWatch(String path, Watcher watcher) {
HashSet<Watcher> list = (HashSet)this.watchTable.get(path);
if (list == null) {
list = new HashSet(4);
this.watchTable.put(path, list);
} list.add(watcher);
HashSet<String> paths = (HashSet)this.watch2Paths.get(watcher);
if (paths == null) {
paths = new HashSet();
this.watch2Paths.put(watcher, paths);
} paths.add(path);
} // 路径维度
private final HashMap<String, HashSet<Watcher>> watchTable = new HashMap();
// Watcher维度
private final HashMap<Watcher, HashSet<String>> watch2Paths = new HashMap();

  当服务端处理完毕之后,客户端的SendThread线程负责接收服务端的响应,finishPacket方法会从packet中取出WatchRegistration并注册到ZKWatchManager中:

private void finishPacket(ClientCnxn.Packet p) {
// 客户端注册wathcer
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;
this.eventThread.queuePacket(p);
} } public void register(int rc) {
if (this.shouldAddWatch(rc)) {
Map<String, Set<Watcher>> watches = this.getWatches(rc);
synchronized(watches) {
// 根据路径拿到
Set<Watcher> watchers = (Set)watches.get(this.clientPath);
if (watchers == null) {
watchers = new HashSet();
watches.put(this.clientPath, watchers);
} ((Set)watchers).add(this.watcher);
}
} }

通知

  当服务端对应的数据节点内容发生改变,那么会触发watcher,对应的代码在org.apache.zookeeper.server.DataTree#setData

public Stat setData(String path, byte[] data, int version, long zxid, long time) throws NoNodeException {
Stat s = new Stat();
DataNode n = (DataNode)this.nodes.get(path);
if (n == null) {
throw new NoNodeException();
} else {
byte[] lastdata = null;
byte[] lastdata;
// 赋值node
synchronized(n) {
lastdata = n.data;
n.data = data;
n.stat.setMtime(time);
n.stat.setMzxid(zxid);
n.stat.setVersion(version);
n.copyStat(s);
} String lastPrefix;
if ((lastPrefix = this.getMaxPrefixWithQuota(path)) != null) {
this.updateBytes(lastPrefix, (long)((data == null ? 0 : data.length) - (lastdata == null ? 0 : lastdata.length)));
} // 触发watcher
this.dataWatches.triggerWatch(path, EventType.NodeDataChanged);
return s;
}
}

  触发watcher,从watchTable和watch2Paths中移除该路径的watcher。这里可以看出,Watcher在服务端是一次性的,触发一次就失效了

public Set<Watcher> triggerWatch(String path, EventType type) {
return this.triggerWatch(path, type, (Set)null);
} public Set<Watcher> triggerWatch(String path, EventType type, Set<Watcher> supress) {
WatchedEvent e = new WatchedEvent(type, KeeperState.SyncConnected, path);
HashSet watchers; // 这个同步代码块主要做的就是从watchTable和watch2Paths中移除该路径的watcher
synchronized(this) {
watchers = (HashSet)this.watchTable.remove(path);
if (watchers == null || watchers.isEmpty()) {
if (LOG.isTraceEnabled()) {
ZooTrace.logTraceMessage(LOG, 64L, "No watchers for " + path);
} return null;
} Iterator i$ = watchers.iterator(); while(i$.hasNext()) {
Watcher w = (Watcher)i$.next();
HashSet<String> paths = (HashSet)this.watch2Paths.get(w);
if (paths != null) {
paths.remove(path);
}
}
} Iterator i$ = watchers.iterator(); while(true) {
Watcher w;
do {
if (!i$.hasNext()) {
return watchers;
} w = (Watcher)i$.next();
} while(supress != null && supress.contains(w)); // watcher调用,这里的e对象里只有通知状态(KeeperState)、事件类型(EventType)以及节点路径(Path)
// 没有修改过后的新值也没有老的值
w.process(e);
}
}

  最后看一下process方法里,其实做的事情就是把事件发送给客户端,所以我们可以看出,真正的回调和业务逻辑执行都在客户端org.apache.zookeeper.server.NIOServerCnxn#process:

public synchronized void process(WatchedEvent event) {
  // 请求头标记-1,表明是通知
ReplyHeader h = new ReplyHeader(-1, -1L, 0);
if (LOG.isTraceEnabled()) {
ZooTrace.logTraceMessage(LOG, 64L, "Deliver event " + event + " to 0x" + Long.toHexString(this.sessionId) + " through " + this);
} WatcherEvent e = event.getWrapper();
// 发送通知
this.sendResponse(h, e, "notification");
}

  客户端收到该通知,由org.apache.zookeeper.ClientCnxn.SendThread#readResponse处理,主要做的就是反序列化然后交给EventThread线程

void readResponse(ByteBuffer incomingBuffer) throws IOException {
...
// 如果是通知
} else if (replyHdr.getXid() == -1) {
if (ClientCnxn.LOG.isDebugEnabled()) {
ClientCnxn.LOG.debug("Got notification sessionid:0x" + Long.toHexString(ClientCnxn.this.sessionId));
} // 反序列化
WatcherEvent event = new WatcherEvent();
event.deserialize(bbia, "response");
if (ClientCnxn.this.chrootPath != null) {
String serverPath = event.getPath();
if (serverPath.compareTo(ClientCnxn.this.chrootPath) == 0) {
event.setPath("/");
} else if (serverPath.length() > ClientCnxn.this.chrootPath.length()) {
event.setPath(serverPath.substring(ClientCnxn.this.chrootPath.length()));
} else {
ClientCnxn.LOG.warn("Got server path " + event.getPath() + " which is too short for chroot path " + ClientCnxn.this.chrootPath);
}
} WatchedEvent we = new WatchedEvent(event);
if (ClientCnxn.LOG.isDebugEnabled()) {
ClientCnxn.LOG.debug("Got " + we + " for sessionid 0x" + Long.toHexString(ClientCnxn.this.sessionId));
} // 交给EventThread线程处理
ClientCnxn.this.eventThread.queueEvent(we);
}
...
}

  然后从之前注册的ZKWatcherManager中获取到所有该路径的watcher,注意了,客户端的Watcher机制也是一次性的!

public void queueEvent(WatchedEvent event) {
if (event.getType() != EventType.None || this.sessionState != event.getState()) {
this.sessionState = event.getState();
ClientCnxn.WatcherSetEventPair pair = new ClientCnxn.WatcherSetEventPair(ClientCnxn.this.watcher.materialize(event.getState(), event.getType(), event.getPath()), event);
this.waitingEvents.add(pair);
}
} public Set<Watcher> materialize(KeeperState state, EventType type, String clientPath) {
...
// 把该路径下的所有Watcher都拿出来
// remove方法,所以客户端也是一次性的,一旦触发,watcher就失效了
case NodeDataChanged:
case NodeCreated:
var6 = this.dataWatches;
synchronized(this.dataWatches) {
this.addTo((Set)this.dataWatches.remove(clientPath), result);
} var6 = this.existWatches;
synchronized(this.existWatches) {
this.addTo((Set)this.existWatches.remove(clientPath), result);
break;
}
...
}

  最后EventThread会从waitingEvents队列中取出Watcher并执行串行化同步处理。看一下这个方法:org.apache.zookeeper.ClientCnxn.EventThread#processEvent

private void processEvent(Object event) {
try {
if (event instanceof ClientCnxn.WatcherSetEventPair) {
ClientCnxn.WatcherSetEventPair pair = (ClientCnxn.WatcherSetEventPair)event;
Iterator i$ = pair.watchers.iterator(); while(i$.hasNext()) { // 这里的watcher就是客户端传入的watcher,里面有真正的回调逻辑代码
Watcher watcher = (Watcher)i$.next(); try {
watcher.process(pair.event);
} catch (Throwable var7) {
ClientCnxn.LOG.error("Error while calling watcher ", var7);
}
}
} else {
...
}
...
}

  嗯,就是这样,走完了,从网上找到一张图,我觉得画的很不错。以上三步骤,注册,存储,通知可以结合这张图来看,最好请打开原图来看:

三.总结

Watcher特性总结

一次性

  无论客户端还是服务端,一旦watcher被触发,都会被移除

客户端串行执行

  从源码也看到了,watcher回调是串行同步化执行过程,注意不要一个watcher中放很多处理逻辑造成影响别的watcher回调

性能轻量

  注册watcher把watcher对象传给服务端,回调的时候并不会告诉节点的具体变化前后的内容。非常轻量  

时效

  发生CONNECTIONLOSS之后,只要在session_timeout之内再次连接上(即不发生SESSIONEXPIRED),那么这个连接注册的watches依然在。

节点通知

  guava to java is Curator to ZooKeeper,开源客户端Curator引入Cache实现对服务端事件的监听,从而大大简化了原生API开发的繁琐过程。

  虽然我们可以通过Curator或者ZKClient避免每次要watcher注册的痛苦,但是我们无法保证在节点更新频率很高的情况下客户端能收到每一次节点变化的通知

  原因在于:当一次数据修改,通知客户端,客户端再次注册watch,在这个过程中,可能数据已经发生了许多次数据修改

参考:

偷来的图:https://blog.csdn.net/huyangyamin/article/details/77743624

搞懂ZooKeeper的Watcher之源码分析及特性总结的更多相关文章

  1. 【Netty之旅四】你一定看得懂的Netty客户端启动源码分析!

    前言 前面小飞已经讲解了NIO和Netty服务端启动,这一讲是Client的启动过程. 源码系列的文章依旧还是遵循大白话+画图的风格来讲解,本文Netty源码及以后的文章版本都基于:4.1.22.Fi ...

  2. storm操作zookeeper源码分析-cluster.clj

    storm操作zookeeper的主要函数都定义在命名空间backtype.storm.cluster中(即cluster.clj文件中).backtype.storm.cluster定义了两个重要p ...

  3. zookeeper服务发现实战及原理--spring-cloud-zookeeper源码分析

    1.为什么要服务发现? 服务实例的网络位置都是动态分配的.由于扩展.失败和升级,服务实例会经常动态改变,因此,客户端代码需要使用更加复杂的服务发现机制. 2.常见的服务发现开源组件 etcd—用于共享 ...

  4. Java集合源码分析(二)Linkedlist

    前言 前面一篇我们分析了ArrayList的源码,这一篇分享的是LinkedList.我们都知道它的底层是由链表实现的,所以我们要明白什么是链表? 一.LinkedList简介 1.1.LinkedL ...

  5. LinkedList 源码分析(JDK 1.8)

    1.概述 LinkedList 是 Java 集合框架中一个重要的实现,其底层采用的双向链表结构.和 ArrayList 一样,LinkedList 也支持空值和重复值.由于 LinkedList 基 ...

  6. 【Zookeeper】源码分析之Watcher机制(一)

    一.前言 前面已经分析了Zookeeper持久话相关的类,下面接着分析Zookeeper中的Watcher机制所涉及到的类. 二.总体框图 对于Watcher机制而言,主要涉及的类主要如下. 说明: ...

  7. 【Zookeeper】源码分析之Watcher机制(二)

    一.前言 前面已经分析了Watcher机制中的第一部分,即在org.apache.zookeeper下的相关类,接着来分析org.apache.zookeeper.server下的WatchManag ...

  8. 【Zookeeper】源码分析之Watcher机制(三)之Zookeeper

    一.前言 前面已经分析了Watcher机制中的大多数类,本篇对于ZKWatchManager的外部类Zookeeper进行分析. 二.Zookeeper源码分析 2.1 类的内部类 Zookeeper ...

  9. 【Zookeeper】源码分析之Watcher机制(二)之WatchManager

    一.前言 前面已经分析了Watcher机制中的第一部分,即在org.apache.zookeeper下的相关类,接着来分析org.apache.zookeeper.server下的WatchManag ...

随机推荐

  1. 洛谷 P2205 [USACO13JAN]画栅栏

    这题其实没什么,但用到的算法都十分有用.做一个不恰当的比喻,这是一只必须用牛刀杀的鸡,但因为我这个蒟蒻杀不死牛,所以只能找只鸡来练练手. 题目描述 Farmer John 想出了一个给牛棚旁的长围墙涂 ...

  2. [转]webservice 采用SSL实现加密传输

    本文转自:http://book.51cto.com/art/200906/129770.htm http://yeweiyun868.blog.163.com/blog/static/5637844 ...

  3. Android百度地图开发 百度地图得到当前位置

    1.申请key 2.复制jar,以及.so .注意要Libs目录右键build path -> use as source folder(这是一个坑) 3. AndroidMainFast.xm ...

  4. django项目在uwsgi+nginx上部署遇到的坑

    本文来自网易云社区 作者:王超 问题背景 django框架提供了一个开发调试使用的WSGIServer, 使用这个服务器可以很方便的开发web应用.但是 正式环境下却不建议使用这个服务器, 其性能.安 ...

  5. 云计算之路-阿里云上:在SLB上部署https遇到的问题及解决方法

    一.问题场景 这个问题只会出现在云服务器操作系统使用Windows Server 2012的场景,如果使用的是Windows Server 2008 R2则不存在这个问题. 二.https部署场景 1 ...

  6. 线段树&树状数组模板

    树状数组: #include <bits/stdc++.h> using namespace std; ; struct binit { int a[MAXN], n; void modi ...

  7. linux常用的日志分析脚本

    linux实用的日志分析脚本 日志分析 随意的tail一个access_log文件,下面是一条经典的访问记录 /Dec/::: +] “GET /query/trendxml/district/tod ...

  8. CSA Round #84 The Sprawl

    题目 Analysis 曼哈顿距离($L1$ metric)最小生成树. Implementation 下面的代码参考了 gispzjz 在比赛中的提交. #include <bits/stdc ...

  9. 321. Create Maximum Number 解题方法详解

    321. Create Maximum Number 题目描述 Given two arrays of length m and n with digits 0-9 representing two ...

  10. vue基础教程

    1.执行npm install 2.安装stylus,(npm install之后node_module已经有了stylus,但还是要再安装一次) npm install --save-dev sty ...