目录
假期闲来无事,记录下前段时间阅读 Zookeeper 源码的一些笔记,主要参考了《从Paxos到Zookeeper》这本书和一些博客,从高层看了一些代码,详细阅读以后有时间会继续学习总结。
1. 系统模型
1. 数据模型
- Znode是Zookeeper中数据的最小单元。
- ZK的数据结构模型是基于ZNode的树状模型。在ZK内部通过类似内存数据库的方式保存了整棵树的内容,并定时写入磁盘。
- ZK的内存数据放在DataTree中,它是ZK内存数据存储的核心,也是一个树形结构。
2. 节点特性
- 持久节点,是指该数据节点被创建后,会一直存在Zookeeper服务器上,直到有删除操作来主动清除这个节点
- 临时节点,是指节点的生命周期和客户端的会话绑定在一起,即如果客户端会话失效,那么这个节点就会被自动清理掉
- 顺序节点,是指创建节点时维护一份顺序,用于记录下每个节点创建的先后顺序,Zookeeper会自动为该节点名加上一个数字后缀,作为一个新的,完整的节点名。这个数字后缀的上限是整型的最大值
每个数据节点除了存储了数据内容之外,还存储了数据节点本身的一些状态信息。
3. 版本-保证分布式数据原子性操作
Zookeeper中的版本概念表示的是对数据节点的数据内容,子节点列表,或是节点ACL信息的修改次数。
4. Watcher-数据变更的通知
在Zookeeper中,引入了Watcher机制来实现这种分布式的通知功能。
- Watcher,接口类型,其定义了process方法,需子类实现。
- Event,接口类型,Watcher的内部类,无任何方法。
- KeeperState,枚举类型,Event的内部类,表示Zookeeper所处的状态。
- EventType,枚举类型,Event的内部类,表示Zookeeper中发生的事件类型。
- WatchedEvent,WatchedEvent类包含了三个属性,分别代表事件发生时Zookeeper的状态、事件类型和发生事件所涉及的节点路径。
- ClientWatchManager,接口类型,表示客户端的Watcher管理者,其定义了materialized方法,需子类实现。
- ZKWatchManager,Zookeeper的内部类,继承ClientWatchManager。
- MyWatcher,ZooKeeperMain的内部类,继承Watcher。
- ServerCnxn,接口类型,继承Watcher,表示客户端与服务端的一个连接。
- WatchManager,管理Watcher。
ZooKeeper的Watcher机制主要包括客户端线程、客户端WatchManager和ZooKeeper 服务器三部分。在具体工作流程上,简单地讲,客户端在
向ZooKeeper服务器注册Watcher的同时,会将Watcher对象存储在客户端的WatchManager中。当ZooKeeper服务器端触发Watcher事件后,会向客户端发送通知,客户端线程从WatchManager中取出对应的Watcher对象来执行回调逻辑。
Watcher接口
接口类Watcher用于表示一个标准的事件处理器,包含KeeperState和EventType两个枚举类,分别代表了通知状态和事件类型。
回调方法process()
- 当Zookeeper向客户端发送一个Watcher事件通知时,客户端就会对相应的process方法进行回调。
- Zookeeper使用WatchedEvent对象来封装服务端事件并传递给Watcher。
- WatchedEvent是一个逻辑事件,用于服务端和客户端程序执行过程中所需的逻辑对象,而WatcherEvent因为实现了序列化接口,因此可以用于网络传输。
无论是WatchedEvent还是WatcherEvent,其对Zookeeper服务端事件封装都是极其简单的。
客户端无法直接从该事件中获取到对应数据节点的原始数据内容以及变更后的新数据内容,而是需要客户端再次主动去重新获取数据。
工作机制
三个过程:客户端注册Watcher,服务端处理Watcher和客户端回调Watcher。
客户端注册Watcher
向Zookeeper中注册Watcher的接口大概有如下几个:
- 建立ZK连接时传入的Watcher:必须参数。
- 这个Watcher将作为整个Zookeeper会话期间的默认Watcher,会一直保存在客户端ZKWatcherManager的defaultWatcher中。
- 通过getData, exist, getChildren来设置Watcher,而它们又各有同步和异步两种形式。
public byte[] getData(final String path, Watcher watcher, Stat stat)
public byte[] getData(String path, boolean watch, Stat stat)
boolean参数来标记是否使用上文中提到的默认Watcher来进行注册。
封装一个Watcher的注册信息WatchRegistration对象,用于暂时保存数据节点的路径和Watcher的对应关系。
public byte[] getData(final String path, Watcher watcher, Stat stat) throws KeeperException, InterruptedException {
// the watch contains the un-chroot path
WatchRegistration wcb = null;
if (watcher != null) {
// 生成datawatch的对象
wcb = new DataWatchRegistration(watcher, clientPath);
}
// chroot是zk启动时配置的默认前缀
final String serverPath = prependChroot(clientPath);
// 生成request的相关类
RequestHeader h = new RequestHeader();
//设置请求类型
h.setType(ZooDefs.OpCode.getData);
// 生成可序列化的getdatarequest
GetDataRequest request = new GetDataRequest();
// 设置path和watcher
request.setPath(serverPath);
request.setWatch(watcher != null);
// 生成可序列化的getdataresponse
GetDataResponse response = new GetDataResponse();
// 发送请求
ReplyHeader r = cnxn.submitRequest(h, request, response, wcb);
return response.getData();
}
public ReplyHeader submitRequest(RequestHeader h, Record request, Record response, WatchRegistration watchRegistration) {
ReplyHeader r = new ReplyHeader();
// 把传入的参数传入queuePacket
Packet packet = queuePacket(h, r, request, response, null, null, null, null, watchRegistration);
synchronized (packet) {
// 判断packet是否处理完,没有就wait
while (!packet.finished) {
packet.wait();
}
}
return r;
}
在ZooKeeper中,Packet 可以被看作一个最小的通信协议单元,用于进行客户端与服务端之间的网络传输,任何需要传输的对象都需要包装成一个Packet对象。
因此,在ClientCnxn中WatchRegistration又会被封装到Packet中去,然后放入发送队列中等待客户端发送。
Packet queuePacket(RequestHeader h, ReplyHeader r, Record request, Record response, AsyncCallback cb, String clientPath,
String serverPath, Object ctx, WatchRegistration watchRegistration) {
Packet packet = null;
// 在这里并没有为packet生成xid,它是在稍后通过ClientCnxnSocket::doIO()的实现在发送时生成的
synchronized (outgoingQueue) {
// 初始化packet
packet = new Packet(h, r, request, response, watchRegistration);
// 属性赋值,注册watcher时回调为空
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;
}
/**
* 把packet放入队列中(生产者)
* 问题来了,每次在队列中添加了一个watch的registration之后是谁消费的呢?
*
* 在ClientCnxn类中有一个sendThread线程的run方法里,clientCnxnSocket.doTransport(to,
* pendingQueue, outgoingQueue, ClientCnxn.this);这个方法就是负责消费队列里的registration的
*
* 其中,ClientCnxnSocketNIO是clientCnxnSocket的默认实现类
*
* 在ClientCnxnSocketNIO的doTransport方法中调用了doIO(pendingQueue, outgoingQueue, cnxn);方法,
* 这里是负责具体的序列化及IO的工作。其中p.createBB();方法是负责具体的序列化的工作
*/
outgoingQueue.add(packet);
}
}
sendThread.getClientCnxnSocket().wakeupCnxn();
return packet;
}
完成请求发送后,会由客户端SendThread线程的readResponse方法负责接收来自服务端的响应。finishPacket方法会从Packet中取出对应的Watcher并注册到ZKWatcherManager中。
private void finishPacket(Packet p) {
// registration不为空
if (p.watchRegistration != null) {
// 根据返回码注册
// 这里的register就是之前说过的三种watchregistration里的register方法了。这样就把watcher注册到了zk客户端中,
// 同时服务端也获取到了每个znode是否被watch
p.watchRegistration.register(p.replyHeader.getErr());
}
}
现在就需要从WatcherRegistration这个封装对象中再次提取出Watcher来。
public void register(int rc) {
// 如果rc即response code为0则添加
if (shouldAddWatch(rc)) {
// 获取所有已经注册的path和watcher的map关系
Map<String, Set<Watcher>> watches = getWatches(rc);
synchronized (watches) {
// 找到此次注册的watcher的znode的path
Set<Watcher> watchers = watches.get(clientPath);
// 若之前没有watcher,则新建watcher的set
if (watchers == null) {
watchers = new HashSet<Watcher>();
watches.put(clientPath, watchers);
}
// 把watcher添加到全局对应关系中
watchers.add(watcher);
}
}
}
在register方法中,客户端会将之前暂时保存的Watcher对象转交给ZKWatcherManager,并最终保存到dataWatches中去。
在底层实际的网络传输序列化过程中,并没有将WatcherRegistration对象完全的序列化到底层字节数组中去。
public void createBB() {
try {
// stream和archive的初始化
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BinaryOutputArchive boa = BinaryOutputArchive.getArchive(baos);
boa.writeInt(-1, "len"); // We'll fill this in later
if (requestHeader != null) {
// 序列化header,内容包含znode的路径和是否有watcher,这里很重要,server解析这里知道某个znode
// 是否被watch
requestHeader.serialize(boa, "header");
}
if (request instanceof ConnectRequest) {
// 序列化connect
request.serialize(boa, "connect");
// append "am-I-allowed-to-be-readonly" flag
boa.writeBool(readOnly, "readOnly");
} else if (request != null) {
//序列化request
request.serialize(boa, "request");
}
baos.close();
this.bb = ByteBuffer.wrap(baos.toByteArray());
this.bb.putInt(this.bb.capacity() - 4);
this.bb.rewind();
}
}
在Packet.createBB()方法中,Zookeeper只会将requestHeader和request两个属性进行序列化。
服务端处理Watcher
在FinalRequestProcessor.processRequest()中会判断当前请求是否需要注册Watcher。
case OpCode.getData: {
byte b[] = zks.getZKDatabase().getData(getDataRequest.getPath(), stat, getDataRequest.getWatch() ? cnxn : null);
// 包装response
rsp = new GetDataResponse(b, stat);
break;
}
getDataRequest.getWatch() ? cnxn : null,如果为true,Zookeeper就认为当前客户端请求需要进行Watcher注册,于是将ServerCnxn对象和数据节点路径传入getData方法中去。ServerCnxn代表了一个服务端与客户端的连接。
ServerCnxn接口的默认实现是NIOServerCnxn,实现了Watcher的process接口,我们可以把ServerCnxn看作是一个Watcher对象。
Warcher触发
Watcher注册的请求,Zookeeper会将其对应的ServerCnxn存储到WatchManager中
服务端是如何触发Watcher的?DataTree中:
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);
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;
}
public Set<Watcher> triggerWatch(String path, EventType type, Set<Watcher> supress) {
// 根据事件类型、连接状态、节点路径创建WatchedEvent
WatchedEvent e = new WatchedEvent(type, KeeperState.SyncConnected, path);
// watcher集合
HashSet<Watcher> watchers;
// 同步块
synchronized (this) {
// 从watcher表中移除path,并返回其对应的watcher集合
watchers = watchTable.remove(path);
// 遍历watcher集合
for (Watcher w : watchers) {
// 根据watcher从watcher表中取出路径集合
HashSet<String> paths = watch2Paths.get(w);
if (paths != null) {
// 路径集合不为空,则移除路径
paths.remove(path);
}
}
}
// 遍历watcher集合
for (Watcher w : watchers) {
/**
* 为啥这里需要一个过滤的操作呢,可以通过下面datatree中deletenode里的代码可以了解
* 可以看到,每个节点对应的watch会存到datawatches里,且如果一个节点是另一个节点的子节点,那么在server获取
* getchildren指令的时候会把children相关的的watch加入到datatree的childwatches里去。这时如果节点本身已经
* 触发过了那么childwatches里的节点的watches便不用被触发了(因为节点都要被delete了,不存在子节点)
*/
if (supress != null && supress.contains(w)) {
continue;
}
// 进行处理
w.process(e);
}
return watchers;
}
Watcher触发逻辑:
- 封装WatchedEvent
- 查询Watcher
- 调用process方法来触发Watcher
synchronized public void process(WatchedEvent event) {
// 包装header
ReplyHeader h = new ReplyHeader(-1, -1L, 0);
// Convert WatchedEvent to a type that can be sent over the wire
// 把event包装(连接和事件)
WatcherEvent e = event.getWrapper();
/**
* serverCnxn的回调是为了告诉客户端去调用哪些watcher里的process
* 而为什么会有这个回调呢,比如现在已经在server注册了一个watcher,现在通过setdata把znode的值改掉,
* 这时就会触发watcher
*
* 客户端在ClientCnxn的readResponse方法中处理接收到的消息
*/
sendResponse(h, e, "notification");
}
- 在请求头中标记“1”,表明当前是一个通知
- 将WatchedEvent包装成WatcherEvent,以便进行网络传输序列化
- 向客户端发送该通知
本质上是借助当前客户端连接的ServerCnxn对象来实现对客户端的WatchedEvent传递,真正的客户端Watcher回调与业务逻辑执行都在客户端。
客户端回调Watcher
void readResponse(ByteBuffer incomingBuffer) throws IOException {
// 通知为watcherEvent
if (replyHdr.getXid() == -1) {
// 路径、连接状态和事件
WatcherEvent event = new WatcherEvent();
event.deserialize(bbia, "response");
// 把server端的path转换成client端的path
// 从server的path->客户端定义的真实path,因为可能有预定义的chrootPath存在
// 判断是否有预定义的chrootpath
if (chrootPath != null) {
String serverPath = event.getPath();
// 把server端地址为chrootPath作为根节点
if (serverPath.compareTo(chrootPath) == 0)
event.setPath("/");
else if (serverPath.length() > chrootPath.length())
// 获取地址
event.setPath(serverPath.substring(chrootPath.length()));
}
// WatcherEvent生成WatchedEvent,WatcherEvent实现序列化接口
WatchedEvent we = new WatchedEvent(event);
// 加入waitingEvents队列
eventThread.queueEvent(we);
return;
}
对于一个来自服务端的响应,客户端都是由SendThread.readResponse方法来统一进行处理的,如果响应头中表示了XID为-1,表明这是一个通知类型的响应。
- 反序列化
- 处理chrootPath
- 还原WatchedEvent
- 回调Watcher:将WatchedEvent对象交给EventThread线程,在下一个轮询周期中进行Watcher回调。
EventThread处理事件通知
EventThread线程是Zookeeper客户端中专门用来处理服务端通知事件的线程。
public void queueEvent(WatchedEvent event) {
// 根据事件类型和状态来判断,如果事件类型为None且session状态没有变化就不加入队列中
if (event.getType() == EventType.None && sessionState == event.getState()) {
return;
}
// 获取session状态
sessionState = event.getState();
// 构建路径和事件(连接状态和event状态)的关系
WatcherSetEventPair pair = new WatcherSetEventPair(watcher.materialize(event.getState(), event.getType(),
event.getPath()), event); // 根据事件类型做对应的处理
// 加入队列,等待处理
waitingEvents.add(pair);
}
queueEvent方法首先会根据该通知事件,从ZKWatcherManager中取出所有相关的Watcher。
public Set<Watcher> materialize(Watcher.Event.KeeperState state,
在该方法中,首先会根据EventType类型确定相应的事件类型,针对NodeDataChanged事件而言,其会从dataWatches和existWatches中删除ClientPath对应的Watcher
获取到相关的所有Watcher之后,会将其放入waitingEvents队列中去。waitingEvents是一个待处理的Watcher的队列,EventThread的run方法会不断对该队列进行处理。
@Override
public void run() {
try {
isRunning = true;
while (true) {
// 取出队列第一个元素
Object event = waitingEvents.take();
// eventOfDeath表示eventthread需要被kill
if (event == eventOfDeath) {
// 设置标志,但是这里并没有被真正kill,表示要被kill
wasKilled = true;
} else {
// 不是death标志就处理
processEvent(event);
}
}
private void processEvent(Object event) {
try {
// watcher类型
if (event instanceof WatcherSetEventPair) {
// each watcher will process the event
WatcherSetEventPair pair = (WatcherSetEventPair) event;
for (Watcher watcher : pair.watchers) {
try {
// 执行watcher的回调
watcher.process(pair.event);
}
}
processEvent方法中的Watcher才是之前客户端真正注册的Watcher,调用其process方法就可以实现Watcher的回调了。
特性:
- 一次性
- 客户端串行执行
- 轻量
- WatchedEvent是Zookeeper整个Watcher通知机制的最小通知单元,这个数据结构中只包含三部分内容:通知状态,事件类型和节点路径,需要客户端主动重新去获取数据。
- 客户端向服务端注册Watcher时,仅仅只是在客户端请求中使用Boolean类型属性进行了标记,同时服务端也仅仅是只保存了当前连接的ServerCnxn对象。
5. ACL
- ACL,即访问控制列表,是一种相对来说比较新颖且更细粒度的权限管理方式,可以针对任意用户和组进行细粒度的权限控制。
- 可以从三个方面理解Zookeeper的权限机制,分别是:
- 权限模式Scheme
- 授权对象ID
- 权限Permission
- 通常使用scheme:id:permission来标识一个有效的ACL信息
权限模式:Scheme
- IP
- Digest
- World
- Super
授权对象:ID
授权对象指的是权限赋予的用户或一个指定实体,例如IP地址或是机器等。
权限:Permission
权限就是指那些通过权限检查后可以被允许执行的操作。
2. 序列化与协议
https://www.cnblogs.com/leesf456/p/6278853.html
1. 使用Jute进行序列化
所有需要序列化的类都必须实现Record接口。
在serialize和deserialize方法中,OutputArchive/InputArchive类是Jute底层真正用来做序列化和反序列化的类,并且它们可以为多个对象进行序列化/反序列化操作,这也是方法中tag存在的作用,用来标识对象。
上面这个代码片段演示了如何使用Jute 来对MockReqHeader对象进行序列化和反序列化,总的来说,大体可以分为4步。
- 实体类需要实现Record接口的serialize和deserialize方法。
- 构建一个序列化器Binary0utputArchive。
- 序列化:调用实体类的serialize方法,将对象序列化到指定tag中去。例如在本例中就将MockReqHeader对象序列化到header中去。
- 反序列化:调用实体类的deserialize,从指定的tag中反序列化出数据内容。
2. 通信协议
Zookeeper通信协议整体上的设计非常简单,对于请求,主要包含请求头和请求体,而对于响应,则主要包含响应头和响应体。
请求头包含了最基本的信息,包括xid和type。请求头中xid是记录客户端请求发起的先后序号,用来标识单个客户端请求的先后顺序,type代表请求的操作类型。
协议的请求体部分是指请求的主体内容部分,包含了请求的所有操作内容。
响应头中包含了每一个响应最基本的信息,包括xid,zxid和err。
xid和上文中提到的请求头中的xid是一致的,响应中只是将请求中的xid原值返回。zxid代表ZooKeeper服务器上当前最新的事务ID。err则是一个错误码,当请求处理过程中出现异常情况时,会在这个错误码中标识出来,常见的包括处理成功(Code.OK:0)。节点不存在(Code.NONODE: 101) 和没有权限(Code.NOAUTH: 102) 等。
协议的响应体部分是指响应的主题内容部分,包含了响应的所有返回数据。
最后
大家可以关注我的微信公众号一起学习进步。