微服务系列(二)(4) ZooKeeper源码分析-part-3
本文将继续探究以下内容:Zookeeper Client发送命令链路追踪,Zookeeper的事务请求原理,Watcher监听原理
使用ZooKeeper Client的小伙伴应该熟悉这个类org.apache.zookeeper.ZooKeeper,可能有人使用了ZkClient或curatorFramework,而这两种方便好用的客户端均是封装了Apache ZooKeeper 原生client API来实现的。
以创建节点为例来追踪处理链路
入口在org.apache.zookeeper.ZooKeeper#create(java.lang.String, byte[], java.util.List, org.apache.zookeeper.CreateMode)
发现有这样一句,显而易见,这是一个请求发送并获取响应头的操作
ReplyHeader r = cnxn.submitRequest(h, request, response, null);
进入submitRequest
,进入queuePacket
public Packet queuePacket(RequestHeader h, ReplyHeader r, Record request,
Record response, AsyncCallback cb, String clientPath,
String serverPath, Object ctx, WatchRegistration watchRegistration,
WatchDeregistration watchDeregistration) {
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.
packet = new Packet(h, r, request, response, watchRegistration);
packet.cb = cb;
packet.ctx = ctx;
packet.clientPath = clientPath;
packet.serverPath = serverPath;
packet.watchDeregistration = watchDeregistration;
// The synchronized block here is for two purpose:
// 1. synchronize with the final cleanup() in SendThread.run() to avoid race
// 2. synchronized against each packet. So if a closeSession packet is added,
// later packet will be notified.
synchronized (state) {
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().packetAdded();
return packet;
}
那么就开始有一些难理解了,简单看这段逻辑,它构造了一个Packet(网络数据包),然后判断连接的state值,如果失效则丢弃它,如果有效则加入到outgoingQueue
队列,最终还调用了一次sendThread.getClientCnxnSocket().packetAdded();
,最后这一句进入到最后,调用了selector.wakeup();
(默认认为是NIO的实现),实际上是通知取消线程的阻塞来处理新加入的数据包。
那么可以猜测到,这个新加入的数据包被加入outgoingQueue
后,由一个守护线程循环取Packet和处理,这个逻辑也完全符合NIO的处理模型,由一个线程不断的轮询selector,以根据不同的key做不同的处理。
另一个猜测则是在ZooKeeper client启动时建立了socket连接。
首先验证第二个猜测,从ZooKeeper的构造函数找到cnxn.start();
,继续进入sendThread.start();
,最终到达org.apache.zookeeper.ClientCnxn.SendThread#run
里面有这样的一句startConnect(serverAddress);
最终调用到org.apache.zookeeper.ClientCnxnSocketNIO#connect
可见,ZooKeeper client端的通讯抽象类是ClientCnxnSocket
,其有ClientCnxnSocketNIO
和ClientCnxnSocketNetty
两种实现类
那么第一个猜测就应该在ClientCnxnSocketNIO
类找到答案,其中有这样一个方法:
org.apache.zookeeper.ClientCnxnSocketNIO#doTransport
代码过长,学习过Java NIO API 的会很熟悉这样的编程模式,这里粘贴几个重要的部分:
-
selector.select(waitTimeOut);
for (SelectionKey k : selected) {
SocketChannel sc = ((SocketChannel) k.channel());
if ((k.readyOps() & SelectionKey.OP_CONNECT) != 0) {
if (sc.finishConnect()) {
updateLastSendAndHeard();
updateSocketAddresses();
sendThread.primeConnection();
}
} else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) {
doIO(pendingQueue, cnxn);
}
}
-
(
doIO
方法内逻辑)SocketChannel sock = (SocketChannel) sockKey.channel();
-
if (sockKey.isReadable()) { //doSomething}
-
if (sockKey.isWritable()) { //doSomething}
对照NIO思考…
- 不断轮询seletor获取key
- 根据key的值来做不同的处理,如写操作、读操作
并在其中找到了一个联系前文的方法org.apache.zookeeper.ClientCnxnSocketNIO#findSendablePacket
这里处理了前文说到的outgoingQueue,这其中不仅是弹出outgoingQueue的第一个Packet,而是额外处理了一个逻辑,将primeConnection()
排队的空标头数据包优先处理,是为了让sasl身份验证优先处理。
最终调用了sock.write(p.bb)
将packet的ByteBuf写入Channel。
到这里,client端的数据包写入channel,则由server端的channel接受了数据。
上一篇文章有跟踪到server端session信息的管理及socket的接受,进入那个熟悉的类
org.apache.zookeeper.server.NIOServerCnxnFactory
server端由org.apache.zookeeper.server.NIOServerCnxnFactory.AcceptThread
负责socket的accept
由org.apache.zookeeper.server.NIOServerCnxnFactory.SelectorThread
负责请求的分发
最终在SelectorThread
分发到处理器org.apache.zookeeper.server.WorkerService
来执行IO
然后进入到org.apache.zookeeper.server.NIOServerCnxn#doIO
逻辑
由client发送的数据包,最终由server处理,进入到org.apache.zookeeper.server.NIOServerCnxn#readPayload
逻辑
最终走到熟悉的逻辑org.apache.zookeeper.server.ZooKeeperServer#processPacket
这里会分别处理认证和其他请求,而我们这里追踪的是create请求,会进入到org.apache.zookeeper.server.ZooKeeperServer#submitRequest
,验证packet的有效性(能否识别出packet.type)后,交给firstProcessor
处理器执行processRequest()
,这里的firstProcessor
有多种实现类(如CommitProcessor、AckRequestProcessor、FinalRequestProcessor、FollowerRequestProcessor、LeaderRequestProcessor等),那么这里真正使用的processor
就需要根据本机server的角色来决定进入哪个处理器,这些处理器在ZooKeeperServer初始化的时候进行安装。
假设进入leader server的情况,那么情况会是这样:
protected void setupRequestProcessors() {
RequestProcessor finalProcessor = new FinalRequestProcessor(this);
RequestProcessor toBeAppliedProcessor = new Leader.ToBeAppliedRequestProcessor(finalProcessor, getLeader());
commitProcessor = new CommitProcessor(toBeAppliedProcessor,
Long.toString(getServerId()), false,
getZooKeeperServerListener());
commitProcessor.start();
ProposalRequestProcessor proposalProcessor = new ProposalRequestProcessor(this,
commitProcessor);
proposalProcessor.initialize();
prepRequestProcessor = new PrepRequestProcessor(this, proposalProcessor);
prepRequestProcessor.start();
firstProcessor = new LeaderRequestProcessor(this, prepRequestProcessor);
setupContainerManager();
}
处理器链路为:LeaderRequestProcessor
->PrepRequestProcessor
->ProposalRequestProcessor
->CommitProcessor
->Leader.ToBeAppliedRequestProcessor
->FinalRequestProcessor
首先再解释一下ZooKeeper Server的processor是如何实现的,一般来说,分两种processor,一种是静态处理逻辑,由调用者直接执行处理逻辑并转发给下一个processor,另一种则是在每个processor维护一个队列,用于接收请求,而内部作为一个任务由一个独立的线程不断的循环处理队列中的请求,并在处理请求后将请求转发给下个processor处理。
看起来这段链路非常长,不过不要担心,挑重要的逻辑来分析:
LeaderRequestProcessor
:Check if this is a local session and we are trying to create an ephemeral node, in which case we upgrade the session(检查这是否是本地会话,我们正在尝试创建一个临时节点,在这种情况下我们升级会话),看起来它处理内容并不是我们关注的内容
PrepRequestProcessor
:根据OpCode(create/setData等)来做预处理,这里假设是create请求,会先校验create请求的有效性,最终封装成ChangeRecord加入outstandingChanges
队列(供数据同步链路的处理器使用)
ProposalRequestProcessor
:如果是leader的请求,发起同步的提案,由其他成员同步该消息,转发到ToBeAppliedRequestProcessor
,并转发给SyncRequestProcessor
->AckRequestProcessor
->CommitProcessor
(完成集群所有成员增量数据同步的功能)处理,如果不是来自leader的请求,转发给leader来做处理。(这里会很晕,先看下一个处理器工作内容)
ToBeAppliedRequestProcessor
:验证ProposalRequestProcessor
中发起的提案是否得到集群的认可(即learner是否均成功同步该消息,同步请求会将提案发送给每个参与者,当参与者同步完成会返回确认同步的消息)
FinalRequestProcessor
:如果该请求被其他所有成员同步成功,则此时进行leader上的写操作,将数据应用到本地DataTree,并封装响应对象写入outgoingBuffers
队列,由org.apache.zookeeper.server.NIOServerCnxnFactory.SelectorThread
调度写回响应数据包,此时client可感知数据变化。
这样就完成了一次create命令,从client到server的链路追踪,了解了其processor的工作原理。
那么对于SyncRequestProcessor
->AckRequestProcessor
->CommitProcessor
这条链路,实际上就是实现事务消息的核心,下面就进入这条链路来分析。
SyncRequestProcessor
拥有这样的职责:
* 1. Leader - Sync request to disk and forward it to AckRequestProcessor which * send ack back to itself. * 2. Follower - Sync request to disk and forward request to * SendAckRequestProcessor which send the packets to leader. * SendAckRequestProcessor is flushable which allow us to force * push packets to leader. * 3. Observer - Sync committed request to disk (received as INFORM packet). * It never send ack back to the leader, so the nextProcessor will * be null. This change the semantic of txnlog on the observer * since it only contains committed txns.
- 作为leader的处理器时,其同步请求到磁盘后将请求转发到
AckRequestProcessor
- 作为follower的处理器时,其同步请求到磁盘后转发请求到
SendAckRequestProcessor
并将数据包发送给leader,SendAckRequestProcessor
允许强制推送数据包到leader - 作为observer的处理器时,将已提交的请求同步到磁盘,它永远不会将ack发送回领导者,因此nextProcessor将为null。
以create为例,且本机为leader的话,这里会将请求同步到磁盘,然后转发到AckRequestProcessor
。
接着看AckRequestProcessor
的职责:
* This is a very simple RequestProcessor that simply forwards a request from a * previous stage to the leader as an ACK.
追踪链路org.apache.zookeeper.server.quorum.AckRequestProcessor#processRequest
-> org.apache.zookeeper.server.quorum.Leader#processAck
->org.apache.zookeeper.server.quorum.Leader#tryToCommit
最终定位到:
commit(zxid);
inform(p);
zk.commitProcessor.commit(p.request);
第一句是创建commit packet并发送给其他所有成员(让其他所有成员执行commit):
/**
* Create a commit packet and send it to all the members of the quorum
*
* @param zxid
*/
public void commit(long zxid) {
synchronized(this){
lastCommitted = zxid;
}
QuorumPacket qp = new QuorumPacket(Leader.COMMIT, zxid, null, null);
sendPacket(qp);
ServerMetrics.COMMIT_COUNT.add(1);
}
第二句则是创建info packet并发送给所有observer(让observer同步已持久化到磁盘的消息):
/**
* Create an inform packet and send it to all observers.
*/
public void inform(Proposal proposal) {
QuorumPacket qp = new QuorumPacket(Leader.INFORM, proposal.request.zxid,
proposal.packet.getData(), null);
sendObserverPacket(qp);
}
第三句最终定位到org.apache.zookeeper.server.quorum.CommitProcessor#run
:
修正本地的queuedRequests
、committedRequests
和pendingRequests
,即已完成的请求需要被移除、未完成的请求需要继续在队列中等待被处理。
可以看到,核心的处理逻辑在SyncRequestProcessor
和AckRequestProcessor
,CommitProcessor
仅仅是完成了一些收尾工作,SyncRequestProcessor
完成二阶段提交中的同步的工作(leader的同步会先本地持久化后发送同步请求给其他参与者,而learner的同步会执行本地持久化后返回ack消息给leader),而AckRequestProcessor
则是完成了二阶段提交中的提交的工作(提交的工作仅由leader完成,当learner执行AckRequestProcessor
处理时会将请求转发给leader,最终由leader完成提交的工作)。
前面以create命令为例做了链路的跟踪,为了探索watcher机制,去跟踪一个需要传递Watcher对象的命令方法:
org.apache.zookeeper.ZooKeeper#getData(java.lang.String, org.apache.zookeeper.Watcher, Stat)
首先,我们知道我们在使用zk时,只需要实现org.apache.zookeeper.Watcher
接口,并在使用getData等命令时传递进去,就可以达到监听的效果。
在getData
时做了这样的包裹
WatchRegistration wcb = null;
if (watcher != null) {
wcb = new DataWatchRegistration(watcher, clientPath);
}
可以发现,zk有三个继承了org.apache.zookeeper.ZooKeeper.WatchRegistration
的监听器类:
org.apache.zookeeper.ZooKeeper.ExistsWatchRegistration
org.apache.zookeeper.ZooKeeper.DataWatchRegistration
org.apache.zookeeper.ZooKeeper.ChildWatchRegistration
这里为什么要提这个,因为它用了设计模式,是值得学习的,它用了模板模式,通过抽象类封装了逻辑,只需要继承抽象类并实现org.apache.zookeeper.ZooKeeper.WatchRegistration#getWatches
逻辑即可,可以看到扩展起来是非常方便的。
那么继续,现在已知Watcher
对象被做了一层包裹,现在的Watcher对象是DataWatchRegistration(Watcher)
Watcher对象最终被传递到Packet的构造器中:
org.apache.zookeeper.ClientCnxn.Packet
而在org.apache.zookeeper.ZooKeeper.WatchRegistration
类中,可以发现,当调用register()
方法时,它将Watcher添加到org.apache.zookeeper.ZooKeeper.ZKWatchManager#dataWatches
之中。
那么合理的猜测是,当触发Watcher监听事件时,需要通过该容器来获取到Watcher对象,并触发Watcher对象的监听方法。
最后通过反向的追踪,没有找到完整的链路,那么重新思考,从client发送packet,由server接收packet作为起点继续追踪。
通过前文可以知道,由server接收packet的入口在org.apache.zookeeper.server.ZooKeeperServer#processPacket
最终定位到org.apache.zookeeper.server.ZooKeeperServer#submitRequest
执行firstProcessor.processRequest(si);
到这里,可以知道,无论是server是leader还是learner均会有FinalRequestProcessor
作为链路最后一个处理器,这里就不描述链路跟踪的过程了,总之,最后在FinalRequestProcessor
找到了Watcher相关的部分。
case OpCode.getData: {
lastOp = "GETD";
GetDataRequest getDataRequest = new GetDataRequest();
ByteBufferInputStream.byteBuffer2Record(request.request,
getDataRequest);
path = getDataRequest.getPath();
DataNode n = zks.getZKDatabase().getNode(path);
if (n == null) {
throw new KeeperException.NoNodeException();
}
PrepRequestProcessor.checkACL(zks, request.cnxn, zks.getZKDatabase().aclForNode(n),
ZooDefs.Perms.READ,
request.authInfo, path, null);
Stat stat = new Stat();
byte b[] = zks.getZKDatabase().getData(path, stat,
getDataRequest.getWatch() ? cnxn : null);
rsp = new GetDataResponse(b, stat);
break;
}
在这里,server接收并处理了OpCode.getData
的请求,执行本地方法
zks.getZKDatabase().getData(path, stat,
getDataRequest.getWatch() ? cnxn : null);
从请求中解析并封装了GetDataRequest
对象,并从中获取Watcher对象,本地方法调用作为参数传递Watcher对象,执行org.apache.zookeeper.server.ZKDatabase#getData
-> org.apache.zookeeper.server.DataTree#getData
dataWatches.addWatch(path, watcher);
org.apache.zookeeper.server.watch.IWatchManager#addWatch
这里发现是一个接口IWatchManager
,有两个实现类WatchManager
和WatchManagerOptimized
,用户可以自定义指定实现类,通过系统配置zookeeper.watchManagerName=WatchManagerOptimized/WatchManager
这里默认使用WatchManager
org.apache.zookeeper.server.watch.WatchManager#addWatch
,这里的逻辑实际上就是将Watcher对象放入容器,由manager管理。
那么可以推测,触发Watcher也是由manager来进行:
确实存在这样的方法org.apache.zookeeper.server.watch.WatchManager#triggerWatch
来触发同一监听事件的所有Watcher
for (Watcher w : watchers) {
if (supress != null && supress.contains(w)) {
continue;
}
w.process(e);
}
通过反向追踪,在DataTree中执行了manager.triggerWatch()
的逻辑
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 = getMaxPrefixWithQuota(path);
if(lastPrefix != null) {
long dataBytes = data == null ? 0 : data.length;
this.updateCountBytes(lastPrefix, dataBytes
- (lastdata == null ? 0 : lastdata.length), 0);
}
nodeDataSize.addAndGet(getNodeSize(path, data) - getNodeSize(path, lastdata));
dataWatches.triggerWatch(path, EventType.NodeDataChanged);
return s;
}
最终反向定位到org.apache.zookeeper.server.DataTree#processTxn(TxnHeader, org.apache.jute.Record, boolean)
一个熟悉的方法…
在前文【微服务系列(二)(3) ZooKeeper源码分析-part-2】追踪到的一个处理事务消息的方法,它是将请求根据不同的类型更新到本地内存,即ZkDataBase
。
那么在client端又是在哪里接收server端Watcher发送来的数据包并触发client端事件的呢?
这里就不做详细的解析了,关注这个类org.apache.zookeeper.ClientCnxn.EventThread
client端在ClientCnxn
维护了一个org.apache.zookeeper.ClientCnxn.EventThread
对象
作为一个独立的线程,不断的处理org.apache.zookeeper.ClientCnxn.EventThread#waitingEvents
等待事件队列中的事件,然后执行事件的触发。
而waitingEvents
队列由org.apache.zookeeper.ClientCnxn.EventThread#queuePacket
入队
最终定位到org.apache.zookeeper.ClientCnxn#finishPacket
,在每次client端向发送请求后,由org.apache.zookeeper.ClientCnxn.SendThread
这个线程来处理server端的响应信息,并执行queuePacket()
逻辑
而由上文的链路追踪,我们很容易推测到完整链路:
当client端发送setData命令到server,server解析数据包并转发到指定处理链路,通过一系列的数据同步将数据持久化到本地磁盘并将数据写入到本地内存(ZkDatabase
),此时调用到ZkDatabase的setData()
方法,触发NodeDataChanged
事件,触发server端监听器事件,server端监听到事件并发送请求到client端,client端接收到数据包并解析后发布指定事件类型的事件,最终触发client端的Watcher。
可以发现,ZooKeeper的实现运用了大量的异步处理,队列和守护线程(生产者/消费者模式)使用非常频繁,其处理器的实现也应用了该方式,且加入了next指针来完成长链路的处理,有一个明显的好处就是可以自由的切换异步/同步处理逻辑,另外,其网络编程的封装模式也是值得学习的,屏蔽内部实现,不同的类实现不同的功能,互不干扰。