服务端有3种运行方式:leader,follower,observer。leader是领导者,一个ZooKeeper集群同一时刻最多只能有一个leader。follower是跟随者,可以有多个跟随者。Observer是观察者,也可以有多个观察者。
集群刚开始的时候没有leader,这时所有的follower会发起选举过程,选举出唯一一个leader。只有选举出了leader之后集群才能正式工作,被选举出的机器的角色则从follower转换成leader,其余的机器则将自己的leader地址更改为新选举出的leader地址。observer不参与选举过程。
正常的集群对外提供统一服务接口,不管leader、follower、还是observer都可以提供对外服务,对于客户端来说他们是没有区别的。
服务端功能可以分成两大类:选举相关的和服务相关的,我们先看服务相关的。
服务相关的专门用于响应ZooKeeper客户端的请求,类名称为ZooKeeperServer。它又分成3个子类,名称为:LeaderZooKeeperServer,FollowerZooKeeperServer,ObserverZooKeeperServer。顾名思义它们分别在Leader,Follower,Observer时使用。
当LeaderZooKeeperServer收到客户端写事务请求时,先将请求交给Leader类来发起整个集群的写事务流程,这时候Leader类给所有Follower发送PROPOSAL,要求Follower将写请求持久化到本地磁盘以防丢失,Follower持久化后回复ACK信息给Leader,当Leader收集到超过半数的ACK回复时,给Follower发送COMMIT包提交事务执行,最终Leader和Follower都会执行该事务。当然最后是由LeaderZooKeeperServer返回结果给客户端的。
当FollowerZooKeeperServer收到客户端写事务请求时,会调用Follower类发送REQUEST消息给Leader。Leader再给所有Follower发送PROPOSAL发起写事务流程,后面的流程和Leader模式下类似。
对于写操作来说,都是要交给leader然后再开始后续流程的。而对于读操作来说,由于集群提供了分布式一致性存储,就由本地服务端提供服务,而不用区别服务端的角色。
1、怎么启动
服务端的启动是从QuorumPeerMain开始的,它又干了什么事呢?
QuorumPeerMain读取配置信息然后启动QuorumPeer,配置信息由QuorumPeerConfig类负责读取。然后在ZooKeeperServerMain方法中启动ZooKeeperServer并等待关闭事件,ZooKeeperServer又启动ServerCnxn负责和客户端的网络通信。
2、ZooKeeperServer
ZooKeeperServer是服务端基类,它使用NettyServerCnxn作为底层Socket通信机制,NettyServerCnxn收到客户端请求之后会自动调用ZooKeeperServer的prorcessPacket方法来执行客户端命令,并发送响应包给客户端。
ZooKeeper服务端定义了3种子类:LeaderZooKeeperServer、FollowerZooKeeperServer和ObserverZooKeeperServer。它们分别用于Leader,Follower和Observer。
ZooKeeper客户端连接服务端时,可能连接的是Leader,也可能连接的是Follower或者Observer。连接到不同类型的服务端它的命令请求流程是不太一样的,特别是写事务请求。
如果连接的是Leader,则Leader会将写请求生成Proposal包广播到Follower,通知Follower有写事务要执行,这时Follower先将写事务请求保存到磁盘,然后给Leader发送ACK确认。Leader在超过半数确认的情况下才会真正开始执行写命令,写命令完成后Leader再给Follower发送COMMIT命令,通知Follower写操作已完成,这时Follower也会接着调用CommitProcessor并且最后调用FinalRequestProcessor执行同样的写命令,全部完成后Leader和Follower仍然保持相同的ZooKeeper事务,保持集群数据一致性。
读操作则不需要这么复杂,客户端不管连接的是Leader还是Follower,它发送的读命令都会直接执行并返回结果给客户端,因此读操作的并发性能要高很多。
ZooKeeper客户端和服务端之间数据交互流程示意图如下:
它们之间采用Netty框架作为底层Socket连接,NettyServerFactory是服务端连接管理池,为每个客户端创建一个NettyServer,NettyServer接收到客户端命令请求之后会调用ZooKeeperServer的processPacket方法处理该客户端请求。
2.1 processPacket
当ZooKeeperServer的底层NettyServerCnxn接收到客户端的Request时,该方法被NettyServerCnzx的主接收线程调用。ZooKeeperServer然后开始处理Request。
该方法的主要代码:
public void processPacket(ServerCnxn cnxn, ByteBuffer incomingBuffer) throws IOException {
// We have the request, now process and setup for next
InputStream bais = new ByteBufferInputStream(incomingBuffer);
BinaryInputArchive bia = BinaryInputArchive.getArchive(bais);
RequestHeader h = new RequestHeader();
h.deserialize(bia, "header");
Request si = new Request(cnxn, cnxn.getSessionId(), h.getXid(),
h.getType(), incomingBuffer, cnxn.getAuthInfo());
si.setOwner(ServerCnxn.me);
setLocalSessionFlag(si);
submitRequest(si);
}
最后一行的submitRequest方法调用firstProcessor开始处理Request,firstProcessor是处理请求包的入口方法,在Leader/Follower等不同的模式下其firstProcessor是不同的。主要代码如下:
public void submitRequest(Request si) {
if (firstProcessor == null) {
touch(si.cnxn);
boolean validpacket = Request.isValid(si.type);
if (validpacket) {
firstProcessor.processRequest(si);
if (si.cnxn != null) {
incInProcess();
}
}
}
}
2.2 processTxn
处理客户端的写操作请求,通过ZKDatabase类实现具体逻辑。
该方法由FinalRequestProcessor调用,客户端读操作不在这里实现,而是放在FinalRequestProcessor中直接代码实现。当然读操作最终也是通过ZkDatabase的相关方法实现具体逻辑的。
处理写操作请求,写操作涉及到集群状态同步,和读操作有很大的区别,写操作提供了统一的入口方法processTxn来处理。
该方法的主要代码如下:
private ProcessTxnResult processTxn(Request request,TxnHeader hdr, Record txn) {
ProcessTxnResult rc;
int opCode = request != null ? request.type : hdr.getType();
long sessionId = request != null ? request.sessionId : hdr.getClientId();
if (hdr != null) {
rc = getZKDatabase().processTxn(hdr, txn);
} else {
rc = new ProcessTxnResult();
}
if (opCode == OpCode.createSession) {
if (hdr != null && txn instanceof CreateSessionTxn) {
CreateSessionTxn cst = (CreateSessionTxn) txn;
sessionTracker.addGlobalSession(sessionId, cst.getTimeOut());
} else if (request != null && request.isLocalSession()) {
request.request.rewind();
int timeout = request.request.getInt();
request.request.rewind();
sessionTracker.addSession(request.sessionId, timeout);
} else {
LOG.warn("*****>>>>> Got " + txn.getClass() + " " + txn.toString());
}
} else if (opCode == OpCode.closeSession) {
sessionTracker.removeSession(sessionId);
}
return rc;
}
2.3 NettyServerCnxn
NettyServerCnxn是ZooKeeper客户端和服务端底层通信的接口,采用Netty组件作为Socket接口。客户端提交的命令会通过NettyServerCnxn到达服务端,然后触发NettyServerCnxn中的receiveMessage方法,该方法再调用ZooKeeperServer的processPacket方法处理接收到的请求包。其中代码片段如下:
public void receiveMessage(ChannelBuffer message) {
if (initialized) {
zks.processPacket(this, bb);
if (zks.shouldThrottle(outstandingCount.incrementAndGet())) {
disableRecvNoWait();
}
} else {
zks.processConnectRequest(this, bb);
initialized = true;
}
}
当客户端第一次连接到服务端时,会调用ZooKeeperServer的processConnectRequest方法处理;其他情况下会调用ZooKeeperServer的processPacket方法处理客户端请求。具体的处理逻辑可参见ZooKeeperServer及其子类(LeaderZooKeeperServer、FollowerZooKeeperServer、ObserverZooKeeperServer)的相关代码。