前情提要
上篇我们说到leader
的处理器链的逻辑,当leader
发送了一个事务的提议以后需要等待follower
的ack
响应才可以继续走下去,否则就会在CommitProcessor
这里阻塞住,无法提交这个事务。所以本篇就接着上一篇博客继续探究,当follower
端收到这个提议proposal
以后,会走什么样的流程。本篇也会被收录到【Zookeeper 源码解读系列目录】中。
Follower接收Proposal
SyncRequestProcessor & SendAckRequestProcessor
我们在【集群模式三】的最后说follower
会在自己的线程里用一个while (this.isRunning())
不断地接收来自leader的数据,当时笔者贴的代码如下:
syncWithLeader(newEpochZxid);//同步数据
QuorumPacket qp = new QuorumPacket();
while (this.isRunning()) {
readPacket(qp);//监听leader给我的信息
processPacket(qp);//处理这个信息
}
其实,上一篇所说的发送给follower
的提议就是在这里被接收的。那么这一段代码在哪大家还记得吗?是在Follower.followLeader()
里面,记不得的同学可以回去看一下。那么我们要去的方法就一定是processPacket(qp)
这个里面了:
protected void processPacket(QuorumPacket qp) throws IOException{
switch (qp.getType()) {
case Leader.PING:
ping(qp);
break;
case Leader.PROPOSAL: //接收提议
TxnHeader hdr = new TxnHeader();
Record txn = SerializeUtils.deserializeTxn(qp.getData(), hdr);//取出数据
if (hdr.getZxid() != lastQueued + 1) {
/**Log4j**/
}
lastQueued = hdr.getZxid();
fzk.logRequest(hdr, txn); //重点
break;
case Leader.COMMIT:
fzk.commit(qp.getZxid());
break;
case Leader.UPTODATE:
LOG.error("Received an UPTODATE message after Follower started");
break;
case Leader.REVALIDATE:
revalidate(qp);
break;
case Leader.SYNC:
fzk.sync();
break;
default:
LOG.error("Invalid packet type: {} received by Observer", qp.getType());
}
}
我们找到case Leader.PROPOSAL
这个分支,首先反序列化取出数据txn
,然后调用fzk.logRequest(hdr, txn);
这个方法处理数据,所以进入这个方法:
public void logRequest(TxnHeader hdr, Record txn) {
Request request = new Request(null, hdr.getClientId(), hdr.getCxid(),
hdr.getType(), null, null);
request.hdr = hdr;
request.txn = txn;
request.zxid = hdr.getZxid();
if ((request.zxid & 0xffffffffL) != 0) {
pendingTxns.add(request);
}
syncProcessor.processRequest(request);//也是调用了持久化
}
进入以后,这里也没有做什么内容,先把txn
这个事务转变成了一个request
,然后直接调用持久化的处理器SyncRequestProcessor
。我们上一篇说过,在follower
端如果SyncRequestProcessor
调用成功,也就是持久化操作没问题,接下来的处理器就是SendAckRequestProcessor
。沿着链条我们找到这个发送ack
的方法,即SendAckRequestProcessor.processRequest(request)
public void processRequest(Request si) {
if(si.type != OpCode.sync){
QuorumPacket qp = new QuorumPacket(Leader.ACK, si.hdr.getZxid(), null, null);
try {
learner.writePacket(qp, false);
} catch (IOException e) {
LOG.warn("Closing connection to leader, exception during packet send", e);
try {
if (!learner.sock.isClosed()) {
learner.sock.close();
}
} catch (IOException e1) {
LOG.debug("Ignoring error closing the connection", e1);
}
}
}
}
进入以后,果然首先构造一个QuorumPacket qp
,但是这个qp
的操作码是Leader.ACK
,然后在调用socket
方法learner.writePacket(qp, false);
发送出去。这个处理器做的任务可以说的非常简单,就是发送一个ack
给leader
端。
那么这里是什么意思呢,总结上面所讲,其实就是在说明一件事:首先leader
接收到一个事务,然后leader
进行持久化,leader
持久化以后发送一个提议给follower
。follower
怎么判断这个提议能不能成功呢?也是持久化,follower
接到这个提议以后,直接就去持久化。如果follower
的持久化成功了,就会调用发送ack
的process
,进入SendAckRequestProcessor.processRequest(request);
发送一个ack
告诉leader
:"我"
持久化没问题可以做这个事务,"我"
同意这个提议。
Leader接收提议的ACK
那么到了leader
这里,谁去处理的这个消息呢?到这里我们就又得返回LearnerHandle.run()
方法里找到while(true)
里的ACK
分支,要记住我们线程一直再跑。在follower
端的while (this.isRunning())
和leader
端的while(true)
是一直监听互相的消息的,我们以前说过是配对用的。那么我们去看:
public void run() {
try {
/**已经讲过,略**/
queuedPackets.add(new QuorumPacket(Leader.UPTODATE, -1, null, null));//数据同步结束
while (true) {
/**拿数据,略**/
switch (qp.getType()) {
case Leader.ACK:
if (this.learnerType == LearnerType.OBSERVER) {
/**Log4j**/
}
syncLimitCheck.updateAck(qp.getZxid());
leader.processAck(this.sid, qp.getZxid(), sock.getLocalSocketAddress());
break;
case Leader.PING:
/**不相关,略**/
break;
case Leader.REVALIDATE:
/**不相关,略**/
break;
case Leader.REQUEST: //接收Follower转发
/**不相关,略**/
break;
default:
LOG.warn("unexpected quorum packet, type: {}", packetToString(qp));
break;
}
}
} catch (***Exception e) {
/**Exceptions**/
} finally {
/**Log4j**/
shutdown();
}
}
那么我们找到case Leader.ACK:
,拿到ack
以后就会处理leader.processAck(this.sid, qp.getZxid(), sock.getLocalSocketAddress())
。继续进入看:
synchronized public void processAck(long sid, long zxid, SocketAddress followerAddr) {
/**不相关,略**/
Proposal p = outstandingProposals.get(zxid); //构造提议
/**不相关,略**/
p.ackSet.add(sid);//添加合法的sid
/**Log4j**/
if (self.getQuorumVerifier().containsQuorum(p.ackSet)){ //验证过半
if (zxid != lastCommitted+1) {
/**Log4j**/
}
outstandingProposals.remove(zxid);
if (p.request != null) {
toBeApplied.add(p);
}
if (p.request == null) {
LOG.warn("Going to commmit null request for proposal: {}", p);
}
commit(zxid);//通知follower提交
inform(p);//通知observer
zk.commitProcessor.commit(p.request);
if(pendingSyncs.containsKey(zxid)){
for(LearnerSyncRequest r: pendingSyncs.remove(zxid)) {
sendSync(r);
}
}
}
}
进入以后,略去一些检查的代码。先把事务zxid
对应的提议从提议的队列里面初始化出来Proposal p = outstandingProposals.get(zxid);
。然后再把follower
的服务器id
加入进来p.ackSet.add(sid);
。也就是说ackSet
就是在存储发送ack
过来的follower
的sid
的,记住这里。我们说过,凡是follower发送ack到leader端都会走到这里,也就是说ackSet会不断的把成功的follower加入进来,为什么要加入呢?当然是为了通过过半机制。我们之前说过提议就是要通过过半验证才能提交。果然紧接着就又发现了过半验证的判断if (self.getQuorumVerifier().containsQuorum(p.ackSet))
,验证是不是有过半的follower
发回了ack
,一旦验证过半通过了就进入if
,然后提交commit(zxid)
这个事务。
public void commit(long zxid) {
synchronized(this){
lastCommitted = zxid;
}
QuorumPacket qp = new QuorumPacket(Leader.COMMIT, zxid, null, null); //提交COMMIT
sendPacket(qp); //发给follower
}
进入,里面就只有创建打包为一个packet
,然后sendPacket(qp)
给所有的follower
。往下走还有一个方法inform(p);
这里就是发送一个INFORM
的操作码给对应的observer
。
public void inform(Proposal proposal) {
QuorumPacket qp = new QuorumPacket(Leader.INFORM, proposal.request.zxid,
proposal.packet.getData(), null); //通知INFORM
sendObserverPacket(qp); //发给Observer,这里调用的方法都是不同的
}
发送以后,我们的follower
自然就可以在while(isrunning)
这个循环中监控到,那么再次提醒下这个循环的位置Follower.followerLeader().while(isrunning).processPacket(qp)
:
protected void processPacket(QuorumPacket qp) throws IOException{
switch (qp.getType()) {
case Leader.PING:
ping(qp);
break;
case Leader.PROPOSAL: //接收提议
/**已经讲过,略**/
break;
case Leader.COMMIT: //提交提议
fzk.commit(qp.getZxid());
break;
case Leader.UPTODATE:
LOG.error("Received an UPTODATE message after Follower started");
break;
case Leader.REVALIDATE:
revalidate(qp);
break;
case Leader.SYNC:
fzk.sync();
break;
default:
LOG.error("Invalid packet type: {} received by Observer", qp.getType());
}
}
那么我们找到case Leader.COMMIT:
这个分支里,处理这个事务的方法fzk.commit(qp.getZxid())
:
public void commit(long zxid) {
if (pendingTxns.size() == 0) {
/**Log4j**/
return;
}
long firstElementZxid = pendingTxns.element().zxid;
if (firstElementZxid != zxid) {
/**Log4j**/
System.exit(12);
}
Request request = pendingTxns.remove();
commitProcessor.commit(request); //提交
}
进入后,发现这个提交用的就是commitProcessor.commit(request)
,这里是不是有点醒悟了,我们当初可是在CommitProcessor
里面阻塞了对不对,那么这里是不是要唤醒了呢?我们进入看:
synchronized public void commit(Request request) {
if (!finished) {
if (request == null) {
/**Log4j**/
return;
}
/**Log4j**/
committedRequests.add(request);//添加request
notifyAll();//唤醒
}
}
在这里面,我们就发现了两句话有用committedRequests.add(request)
添加请求到队列里,以及唤醒notifyAll()
。这时候在CommitProcessor
里面就能够直接走到finalProcessor
里面了,并且刚才阻塞的事务可以被提交了。所以CommitProcessor
做了这样的逻辑:首先,不能提交的就先阻塞住等待ack
,可以提交的就直接调用下一个process
去提交更新内存。这里要特别说明一点,leader
和follower
本质上是一样的,所以大家都有CommitProcessor
这个类在执行。leader
端的CommitProcessor
也会在收到ack
之后执行这个notifyAll();
,这一点是一摸一样的。这里大家一定要想清楚,不然很容易就懵了。
总结一下到目前为止,Zookeeper都做了什么:
首先Client
发了一个写请求给leader
,leader
首先会发送一个提议给followers
,followers
会返回ack
给leader
,然后leader
会进行一个过半验证,验证过了会给followers
发送提交认证,followers
接收到以后就会直接调用finalprocessor
更新内存,因为持久化已经在发送ack
之前就做完了。所以leader
的持久化已经做好了,提议也通过了,接下来leader
就也走到了finalprocessor
更新内存。
Follower转发写请求
那么本章我们就还剩下一个内容,如果client
发给follower
一个写请求会有什么不同:
假设一个客户端个follower
发送了一个写请求,那么根据我们上面的分析,第一个接到写请求的应该是FollowerRequestProcessor
,因为这里的逻辑也是放到一个队列里面,然后有一个线程去取出来。所以就有到了FollowerRequestProcessor.run()
里面。
public void run() {
try {
while (!finished) {
Request request = queuedRequests.take();
if (LOG.isTraceEnabled()) {
ZooTrace.logRequest(LOG, ZooTrace.CLIENT_REQUEST_TRACE_MASK,
'F', request, "");
}
if (request == Request.requestOfDeath) {
break;
}
nextProcessor.processRequest(request); //转发,不符合要求的就阻塞
switch (request.type) {
case OpCode.sync:
zks.pendingSyncs.add(request);
zks.getFollower().request(request);
break;
case OpCode.create:
case OpCode.delete:
case OpCode.setData:
case OpCode.setACL:
case OpCode.createSession:
case OpCode.closeSession:
case OpCode.multi:
zks.getFollower().request(request);
break;
}
}
} catch (Exception e) {
handleException(this.getName(), e);
}
LOG.info("FollowerRequestProcessor exited loop!");
}
到这里面以后,直接转交给下一个处理器执行了nextProcessor.processRequest(request)
,也就是用到了CommitProcessor
这个处理器。我们刚刚说过,这个处理器里面,如果发现不合标准的请求会直接阻塞,不多说,我们接着走到下面的switch
里去。这里只要不是OpCode.sync
的操作码的case
,都会用zks.getFollower().request(request);
这个方法去处理。自然我们要去看下里面写了什么:
void request(Request request) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream oa = new DataOutputStream(baos);
oa.writeLong(request.sessionId);
oa.writeInt(request.cxid);
oa.writeInt(request.type);
if (request.request != null) {
request.request.rewind();
int len = request.request.remaining();
byte b[] = new byte[len];
request.request.get(b);
request.request.rewind();
oa.write(b);
}
oa.close();
QuorumPacket qp = new QuorumPacket(Leader.REQUEST, -1, baos
.toByteArray(), request.authInfo); //包装Leader.REQUEST请求码和数据
writePacket(qp, true); //写入socket
}
进入看这个方法就是初始化了一些参数,然后直接包装成一个QuorumPacket qp
写到socket
里。所以这里就是follower
往leader
转发的过程,我们看这里封装的就是Leader.REQUEST
。这个就是之前我们提到的在LearnerHandler.run()
里面的Leader.REQUEST
部分的逻辑处理,所以我们转过去:
queuedPackets.add(new QuorumPacket(Leader.UPTODATE, -1, null, null)); //同步数据完成
while (true) { //这里还是配对监听的while循环
/**略**/
switch (qp.getType()) {
/**略**/
case Leader.REQUEST: //接收Follower转发
bb = ByteBuffer.wrap(qp.getData());
sessionId = bb.getLong();
cxid = bb.getInt();
type = bb.getInt();
bb = bb.slice();
Request si;
if(type == OpCode.sync){
si = new LearnerSyncRequest(this, sessionId, cxid, type, bb, qp.getAuthinfo());
} else {
si = new Request(null, sessionId, cxid, type, bb, qp.getAuthinfo());
}
si.setOwner(this);
leader.zk.submitRequest(si);
break;
default:
LOG.warn("unexpected quorum packet, type: {}", packetToString(qp));
break;
}
}
还是先拿到数据,然后构造了请求si
,再往下就发现了请求提交leader.zk.submitRequest(si)
的方法,继续进入:
public void submitRequest(Request si) {
if (firstProcessor == null) {
synchronized (this) {
try {
while (state == State.INITIAL) {
wait(1000);
}
} catch (InterruptedException e) {
LOG.warn("Unexpected interruption", e);
}
if (firstProcessor == null || state != State.RUNNING) {
throw new RuntimeException("Not started");
}
}
}
try {//到这里
touch(si.cnxn);
boolean validpacket = Request.isValid(si.type);
if (validpacket) {
firstProcessor.processRequest(si);
if (si.cnxn != null) {
incInProcess();
}
} else {
LOG.warn("Received packet at server of unknown type " + si.type);
new UnimplementedRequestProcessor().processRequest(si);
}
} catch (MissingSessionException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Dropping request: " + e.getMessage());
}
} catch (RequestProcessorException e) {
LOG.error("Unable to process request:" + e.getMessage(), e);
}
}
不知道这里大家还有没有印象,这里就是我们启动处理器链的地方对不对。但是我们要注意外面的zk
是LeaderZookeeperServer
的实例对象,所以这个方法里的firstProcessor
就是我们leader
处理链中的PrepRequestProcessor
。这样follower
接收到的事物,就转发到了Leader
上面,Observer
也是同理,这样Zookeeper的一致性保证就结束了。
总结
那么我们用了两篇博客把Zookeeper的初始化也讲解完了。在这两篇里面,我们详细的介绍了集群模式的处理器链是怎么工作的,以及follower和leader的交互又是怎么监听的等等内容。到这里Zookeeper的启动等等内容基本上就告一段落了。但是我们还遗留了一个大坑,那就是Zookeeper的选举机制的源码讲解。这部分作为Zookeeper的精华只会多不会少,所以笔者预计将会用四到五篇博客去处理这部分。也可以看到这部分有多么的复杂。同样留几张图供大家更好的理解这几篇的内容: