Zookeeper 源码解读系列,集群模式(三)

前情提要

上一篇我们讲了leaderlearner同步数据的方式和内容,以及Zxid这个重要的概念。而止步于leader发送数据给learner,所以这一篇博客就要重点讲一下,learner拿到这些数据和操作码以后会做什么。这里笔者要提醒各位,我们现在一直都说的是服务端集群的机器互相之间的数据传输,和客户端已经没有关系了。客户端的源码不区分集群模式和单机模式,客户端的源码已然在单机模式系列讲完了,说到数据交互的时候千万不要搞混了,集群这部分源码都是服务端的内容。本篇也会被收录到【Zookeeper 源码解读系列目录】中。

Learner的数据处理

我们去看下follower是怎么处理SNAP,DIFF,TRUNC,COMMIT的,大家还记得我们的入口是在哪里吗?提醒大家一下,这里看源码的时候一定要直到自己从哪里跳进来的,又要跳到哪里去,否则很容易就懵了。我们的入口就是QuorumPeer.run()对不对,在这里面有一个while(running)循环,循环里面有switch,我们要找到的地方就是case FOLLOWING:这个里面的follower.followLeader();方法:

void followLeader() throws InterruptedException {
    /**略**/
    try {
        QuorumServer leaderServer = findLeader(); //找到leader
        try {
            connectToLeader(leaderServer.addr, leaderServer.hostname);//connectToLeader
            long newEpochZxid = registerWithLeader(Leader.FOLLOWERINFO);//发送数据给leader
            long newEpoch = ZxidUtils.getEpochFromZxid(newEpochZxid);//更新Epoch
            if (newEpoch < self.getAcceptedEpoch()) {
               /**Exceptions**/
            }
            syncWithLeader(newEpochZxid);//同步数据
            QuorumPacket qp = new QuorumPacket();
            while (this.isRunning()) {
                readPacket(qp);//监听leader给的信息
                processPacket(qp);//处理这个信息
            }
        } catch (Exception e) {
            /**Exceptions**/
        }
    } finally {
        zk.unregisterJMX((Learner)this);
    }
}

回到这个方法里以后,刨去已经讲过的内容。很容易就找到syncWithLeader(newEpochZxid);这个方法,看名字也能判断出来这里就是接收到leader发给"我"的同步方式和需要同步的数据。那就进去看下,收到数据后怎么处理,方法比较长我们还是分段处理,首先我们先看直接经过socket发送的三个操作码SNAP,DIFF,TRUNC这个我们上一篇已经说过,但是到这里其实从代码层面也能体会出来,因为接收的方式都是不一样的:

protected void syncWithLeader(long newLeaderZxid) throws IOException, InterruptedException{
    QuorumPacket ack = new QuorumPacket(Leader.ACK, 0, null, null);
    QuorumPacket qp = new QuorumPacket(); //构造QuorumPacket
    long newEpoch = ZxidUtils.getEpochFromZxid(newLeaderZxid);
    boolean snapshotNeeded = true;
    readPacket(qp); //接收数据
    LinkedList<Long> packetsCommitted = new LinkedList<Long>();
    LinkedList<PacketInFlight> packetsNotCommitted = new LinkedList<PacketInFlight>();
    synchronized (zk) {
        if (qp.getType() == Leader.DIFF) {//Diff好像什么都没有做
            LOG.info("Getting a diff from the leader 0x{}", Long.toHexString(qp.getZxid()));
            snapshotNeeded = false;
        }
        else if (qp.getType() == Leader.SNAP) {//Snap
            LOG.info("Getting a snapshot from leader 0x" + Long.toHexString(qp.getZxid()));
            zk.getZKDatabase().clear();//把当前数据库清空
            zk.getZKDatabase().deserializeSnapshot(leaderIs);//反序列化leader发回来的数据
            String signature = leaderIs.readString("signature");
            if (!signature.equals("BenWasHere")) {
                /**Exceptions**/
            }
            zk.getZKDatabase().setlastProcessedZxid(qp.getZxid());
        } else if (qp.getType() == Leader.TRUNC) {//如果是TRUNC
            /**Log4j**/
            boolean truncated=zk.getZKDatabase().truncateLog(qp.getZxid());//删数据
            if (!truncated) {
                /**Exceptions**/
            }
            zk.getZKDatabase().setlastProcessedZxid(qp.getZxid());
        }
        else {
            /**Exceptions**/
        }
        /**还有很长,暂时略**/
    }
}

进入后看到构造了两个QuorumPacket一个是ack另一个是qpack的先放一放暂时用不着。第一个判断的就是DIFF的逻辑if (qp.getType() == Leader.DIFF) ,但是好像并没有做什么事情只是重置了一个标志位snapshotNeeded = false;。然后是第二个SNAP操作的逻辑if (qp.getType() == Leader.SNAP),首先把当前的数据内容清空zk.getZKDatabase().clear();,然后反序列化leader发回来的leaderIs数据zk.getZKDatabase().deserializeSnapshot(leaderIs);,其实这两步就是先清空再赋值,不多说。接着说第三个操作SNAP的逻辑if (qp.getType() == Leader.TRUNC),没有别的操作直接就调用方法进行删除zk.getZKDatabase().truncateLog(qp.getZxid());,其传入的参数qp.getZxid()就是超出leader端的事务id,一直点进去会到FileTxnLog.truncate()里面循环调用itr.logFile.delete()删除,当然truncateLog(***)以后也会加载一下内存loadDataBase();,岔开的太远了,大家自己点点看吧,这里就不细说了。

protected void syncWithLeader(long newLeaderZxid) throws IOException, InterruptedException{
    /**以上讲过,略**/
    synchronized (zk) {
        /**以上讲过,略**/
        boolean writeToTxnLog = !snapshotNeeded;
        outerLoop: //Jump语句,记住这里很重要。
        while (self.isRunning()) {
            readPacket(qp);
            switch(qp.getType()) {
            case Leader.PROPOSAL:
                /**暂时略,以后讲**/ break;
            case Leader.COMMIT:
                if (!writeToTxnLog) {//判断需不需要写log
                    pif = packetsNotCommitted.peekFirst();
                    if (pif.hdr.getZxid() != qp.getZxid()) {
                        /**Log4j**/
                    } else {
                        zk.processTxn(pif.hdr, pif.rec); //更新内存
                        packetsNotCommitted.remove();
                    }
                } else {//需要写log
                    packetsCommitted.add(qp.getZxid());//把zxid加到队列
                }
                break;
            case Leader.INFORM:
                /**暂时略,以后讲**/ break;
            case Leader.UPTODATE:
                if (isPreZAB1_0) {
                    zk.takeSnapshot();
                    self.setCurrentEpoch(newEpoch);
                }
                self.cnxnFactory.setZooKeeperServer(zk);                
                break outerLoop;
            case Leader.NEWLEADER: 
                /**暂时略,以后讲**/ break;
            }
        }
    }
    ack.setZxid(ZxidUtils.makeZxid(newEpoch, 0));
    writePacket(ack, true);
    sock.setSoTimeout(self.tickTime * self.syncLimit);
    zk.startup();  //注意这里调用的服务器启动,是在数据同步以后做的
    /**还有很长,暂时略**/
}

那么我们往下着看,碰到了DIFF那里修改的标记writeToTxnLog,这个标记一会儿会用到,记住这里取的是非值writeToTxnLog = !snapshotNeeded;。下面又是一个while循环,只要服务器运行self.isRunning()就永远是true,那么我们首先读取readPacket(qp);,这里的循环和上一篇讲的匿名线程里的循环是相互印证的,匿名线程用来发,这里就是用来接收数据的。直接找到case Leader.COMMIT:这里的逻辑块,首先判断要不要写事务日志if (!writeToTxnLog),如果不需要写日志,就直接更新内存,调用zk.processTxn(pif.hdr, pif.rec);更新内存;如果需要写日志,就把获取到的zxid加入到一个队列packetsCommitted.add(qp.getZxid());中,然后break继续循环。

UPTODATE:更新数据完毕的标志

说到这里不知道大家有没有想过,我们说过self.isRunning()不调用shutdown()或者不挂掉的话永远,都是true,不可能就在这里一直循环,不然后面的代码就没有办法执行了,所以什么时候跳出来呢?我们找一找有没有跳出循环的语句,果然在这个switch里找到了,在case Leader.UPTODATE:逻辑里面,执行到最后的时候有一句话break outerLoop;,跟着这句话的引导,就直接跳到了循环外面,那么就是说只有拿到这个UPTODATE才会跳出来,所以就再提一个问题UPTODATE是什么时候传递进去的呢?首先肯定是LearnerHandler发送这个操作。那么为了解决这个问题,我们必须回到LearnerHandler.run(),继续看rl.lock();这个锁解开了以后做了什么。那么就定位到rl.unlock();后面:

QuorumPacket newLeaderQP = new QuorumPacket(Leader.NEWLEADER, 
												ZxidUtils.makeZxid(newEpoch, 0), null, null);
if (getVersion() < 0x10000) {
    oa.writeRecord(newLeaderQP, "packet");
} else {
    queuedPackets.add(newLeaderQP);
}
bufferedOutput.flush();

大家看这里做了什么,发现这里又给Learner发了一个Leader.NEWLEADER的标记,明明leader已经选举出来了,为什么要还发送这个NewLeader呢?那么我们去syncWithLeader(***)看一下就明白了:

case Leader.NEWLEADER: 
    File updating = new File(self.getTxnFactory().getSnapDir(),
                        QuorumPeer.UPDATING_EPOCH_FILENAME);
    if (!updating.exists() && !updating.createNewFile()) {
        throw new IOException("Failed to create " + updating.toString());
    }
    if (snapshotNeeded) { //如果是不是diff
        zk.takeSnapshot(); //读取快照内容
    }
    self.setCurrentEpoch(newEpoch);  //设置新的epoch
    if (!updating.delete()) {
        throw new IOException("Failed to delete " + updating.toString());
    }
    writeToTxnLog = true; 
    isPreZAB1_0 = false;
    writePacket(new QuorumPacket(Leader.ACK, newLeaderZxid, null, null), true); //发送ack
    break;

大家看如果收到了Leader.NEWLEADER首先就是要拿快照文件,然后读取快照内容,更新自己的届到最新的Epoch,最后发送ack回去。那这两块Leader.NEWLEADER这是在干嘛呢?这里其实就是要告诉新加的learner,我就是新的leader,而且里面做的什么也很清楚,就是在把自己的epoch更新成leaderEpoch。就像新领导发送一个NEWLEADER说我就是新领导,其他的learner更新epoch表示同意,然后发送ack告诉新领导:我们都支持你。就是这么个意思。那么我们返回LearnerHandler.run(),找到接收ack的地方:

qp = new QuorumPacket();
ia.readRecord(qp, "packet");
if(qp.getType() != Leader.ACK){
    LOG.error("Next packet was supposed to be an ACK");
    return;
}
LOG.info("Received NEWLEADER-ACK message from " + getSid());
leader.waitForNewLeaderAck(getSid(), qp.getZxid());  //等待newleader的ack返回
syncLimitCheck.start();

这里就是等待newleaderack返回,接着往下终于看到了我们想要找的操作码

queuedPackets.add(new QuorumPacket(Leader.UPTODATE, -1, null, null));

而且被添加到了queuedPackets队列里。其实到了这儿也就是说leader已经把所有的数据同步都告诉了follower,剩下的就是follower要同步数据了,而后learner端碰到UPTODATE退出循环,这些同步的数据在之前都存到了packetsCommitted这里。

if (zk instanceof FollowerZooKeeperServer) {//如果是follower就会去提交
    FollowerZooKeeperServer fzk = (FollowerZooKeeperServer)zk;
    for(PacketInFlight p: packetsNotCommitted) {
        fzk.logRequest(p.hdr, p.rec);
    }
    for(Long zxid: packetsCommitted) {
        fzk.commit(zxid);
    }
} else if (zk instanceof ObserverZooKeeperServer) {//如果是Observer就不会提交
    ObserverZooKeeperServer ozk = (ObserverZooKeeperServer) zk;
    for (PacketInFlight p : packetsNotCommitted) {
        Long zxid = packetsCommitted.peekFirst();
        if (p.hdr.getZxid() != zxid) {
            LOG.warn("Committing " + Long.toHexString(zxid)
                    + ", but next proposal is "
                    + Long.toHexString(p.hdr.getZxid()));
            continue;
        }
        packetsCommitted.remove();
        Request request = new Request(null, p.hdr.getClientId(),
                p.hdr.getCxid(), p.hdr.getType(), null, null);
        request.txn = p.rec;
        request.hdr = p.hdr;
        ozk.commitRequest(request);
    }
} else {
    throw new UnsupportedOperationException("Unknown server type");
}

上面贴的这些代码在syncWithLeader(long newLeaderZxid)这个方法里面的最后,就是learner发送了同意NEWLEADERack以后收到UPTODATE跳出循环while (self.isRunning())以后,继续走的逻辑。如果说服务器对象zkfollower的实例,那就提交事务fzk.commit(zxid);;如果服务器对象zkobserver的实例,就不用提交这些事务。

那么到这里到同步数据正式完成。

同步数据后继续监听消息

还有一点要说一下,这里就不详细解析代码了:在LearnerHandler.run()发送操作码"UPTODATE"后,也就是queuedPackets.add(new QuorumPacket(Leader.UPTODATE, -1, null, null));这句话的后面还有一个while(ture)

queuedPackets.add(new QuorumPacket(Leader.UPTODATE, -1, null, null)); //同步数据完成
while (true) {
    /**略**/
    switch (qp.getType()) {
    case Leader.ACK:
        /**略**/ break;
    case Leader.PING:
        /**略**/ break;
    case Leader.REVALIDATE:
        /**略**/ break;
    case Leader.REQUEST:   //接收Follower转发
        /**略**/ break;
    default:
        LOG.warn("unexpected quorum packet, type: {}", packetToString(qp));
        break;
    }
}

我们知道LearnerHandler是个线程,那么同步完毕数据以后,为什么还要每个线程都开启一个while(True)继续做一些事情呢?这里其实就是在监听learner会不会给leader发送一些指令,比如ACK、Ping这种,但是其中有一个Request比较特殊。我们知道,如果一个客户端连接了follower,并且给这个follower发送了一个写请求,那么这个写请求会被转发到leader上。这个情况就是用的Leader.REQUEST这个分支,这个请求就是在这里发的,我们先不看,因为这个分支我们会在后面的集群请求处理器链里面详细的说这一部分的过程。

那么我们现在转去Follower.followLeader()这里,当数据同步syncWithLeader(newEpochZxid);方法执行以后,又是做了什么:

syncWithLeader(newEpochZxid);//同步数据
QuorumPacket qp = new QuorumPacket();
while (this.isRunning()) {
    readPacket(qp);//监听leader给我的信息
    processPacket(qp);//处理这个信息
}

大家看又是一个while (this.isRunning()),这里和上面说的while(ture)是配对的,readPacket(qp);监听leader给当前learner的信息,processPacket(qp);就是用来处理这个信息的。

总结

到了这里我们就把服务器启动前的数据同步也分析完毕了,这里的代码跳跃性很大,是因为leader和learner之间交互十分的频繁,需要不断地确定和同步返回。如果按照方法来分割内容,不仅十分难以理解,而且代码的篇幅也会干扰笔者解读源码。所以不得不穿插起来讲解,这也造成了本篇代码十分的散乱,所以希望大家一定跟着源码阅读。那么说好的总结:

Leader:
	1.找到要同步的数据,存到queue里
	2.发送出去
	3.发送一个update的执行,告诉follower数据可以使用了

Follower:
	1.收到数据
	while(true){
		2.不停的加入指令到packetsCommitted队列里
		3.直到收到update,退出while
	}
	4.执行指令

这三篇加在一起,我们已经说完了服务器集群模式下的启动,解读配置文件,同步数据的源码, 那么下一步就要开始讲解服务器初始化的时候代码是什么走的流程。这里面就涉及到了集群模式的处理器链的知识,但是这部分很像单机模式下的流程,所以看过以前博客的同学一定会比较容易接收这些知识。那么感谢大家阅读到此,我们下一篇见。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值