zookeeper源码阅读之八(数据同步)

zookeeper在经过选举之后,则是会进入恢复阶段,这个阶段leader会从候选的leader变成真正的leader,follower则会根据leader的数据完成自身的事务日志的同步,而同步的相关的代码则主要是zookeeper中的leader,follower和learnerHandler。zookeeper中内部的大部分的协议类型都是在这个阶段进行的。

重要概念

在具体介绍这个阶段之前,先介绍一下两个比较重要的概念:

  • acceptedEpoch: 表示的是NEWEPOCH消息接收的epoch
  • currentEpoch: 表示的是NEWLEADER消息接收的epoch

对于这两个概念的介绍https://issues.apache.org/jira/browse/ZOOKEEPER-3608中有详细的介绍。zookeeper设计这两个概念主要的原因是对leader选举的提交,原来的epoch是直接从zxid中获取,并且在leder选举出来以后会将新的leader的zxid设置为(newEpoch<<32),这就可能会有造成同一个follower接收到两个相同的zxid的不同事务的proposal消息。

简单举个例子,加入有5个节点A,B,C,D,E 首先选举,此时它们的epoch都相同为1,然后重新选举A,B,C选举C成为leader,C成为leader后将zxid设置为(2<<32),然后开始接收事务请求,此时这个事务的事务id为(2<<32)+1,然后C将消息同步为B,然后B,C宕机,此时由于新的(2<<32)+1还没同步到A,A的epoch还是原来的epoch,此时A,D,E选举E为leader,leader此时也将zxid设置为(2<<32),然后再次开始处理事务,而此时的事务id还是(2<<32)+1,然后B,C又启动了,此时它们与A,D,E在同一个事务id下则就会又不同的事务内容。

zookeeper设计的这两个epoch也还是会有其他的问题,其没有对超出的accpetedEpoch进行回滚,从而会产生异常,这个bug我已经在zookeeper上提出了,不过可能需要在3.5.9之后才有可能修复https://issues.apache.org/jira/browse/ZOOKEEPER-4040

同步时间轴

下图展示了zookeeper在进行同步时其对应的leader,learnerHandler,learner的时间轴其中红色的表示leader的相关的时间轴,绿色表示的时learnerHandler的时间轴,青色则表示的是Follower的时间轴,首先leader加载db以及启动连接端口,然后等待follower的连接,而follower在从选举状态出来切换为Following状态后则会连接其选定的leader,并发送FollowerInfo消息给服务端,这个FollowerInfo包含了这个follower节点的accptedEpoch。而对于leader节点,其为每一个Follower都分配了一个LearnerHandler线程来处理与Follower的交互。

而对于Leader节点来说,其Leader线程以及LearnerHandler线程都会阻塞在getEpochToPropose这个方法下,leader节点在收到过半节点的followerInfo后会从这个方法中出来,并且将当前节点的accptedEpoch赋值为其接收的过半节点的FollowerInfo的最大的accptedEpoch+1,并且将这个新的epoch以LeaderInfo的形式发送给Follower,Follower在接收到这个LeaderInfo时会将本地的accptedEpoch更新为LeaderInfo中接收的消息,并且会发送一个EpochAck消息给leader。

Leader节点中的leader线程和LearnerHandler线程此时则是在等待过半的节点的epochAck消息,在接收到了过半节点的epochAck消息之后,leader节点会将本地的currentEpoch更新为这个epoch,并且根据epochAck发送来的消息来决定同步方式时Tunc,Diff还是Snap,并将需要同步的消息加入到其对应的同步队列中,并且将这个Follower对应的LearnerHandler加入到leader的就绪列表中,以接收leader接收客户端请求广播的后续的消息。

再然后learnerHandler开始发送同步消息,而follower开始接收同步消息,learnerHandler在发送完同步数据后会发送一个newLeader消息,而Follower在接收到newLeader消息后则会将当前的currentEpoch更改为最新的epoch,并且对于snap形式的同步,更新epoch之前会进行一次快照操作,将快照的数据持久化。然后Follower会对这个newLeader发送一个Ack的确认消息。

Leader和LearnerHandler会在waitForNewLeaderAck方法中等待过半的节点的确认,在已经等到了过半的节点的确认之后,leader则会这是启动zookeeper,对外提供工作,而learnerHandler则在等待zookeeper启动成功后向follower发送一个uptodate的消息,而Follower在等待到了这个uptodate消息之后对这个消息发送一个确认消息,并且启动zookeeper,开始对外提供工作。

重要变量

  • zookeeper.hzxid:这是在zookeeper中维护的当前分配的zxid,是leader用来进行事务的zxid的分配的
  • dataTree.lastProcessedZxid:这表示的是当前的处理的事务的最大的zxid(这个值在服务器在稳定的事务处理阶段是和磁盘中记录的值是不同的,因为leader是先写事务日志,此时事务日志中的事务id已经比内存数据中的lastProcessedZxid大了,然后再commit之后这条事务才应用到内存数据库,此时才更新。但是在选举恢复阶段,这个值是和磁盘中的事务最大日志zxid相同的,因为在shutdown的时候会将磁盘中的事务日志都加载进内存中)
  • zkDataBase.minCommittedLog: 这表示在数据库内存缓存中最小事务的id
  • zkDataBase.maxCommittedLog: 这表示的是在数据库内存缓存中的最大的事务id(这个值可能会和lastProcessedZxid不一致,当leader在完成恢复操作后会将本地的dataTree.lastProcessedZxid设置为的(epoch+1)<<32这个新周期的zxid,但是不会修改内存缓存的值)

leader的lastProceeedZxid对应的epoch一定是大于等于follower对应的lastProcessedZxid对应的epoch。如果follower的lastProceesedZxid要大于leader的,那么follower的currentEpoch一定是大于leader的currentEpoch,因为每个节点都是先走恢复阶段再走同步阶段,所以currentEpoch一定是大于等于zxid对应的epoch,但是对于leader的currentEpoch一定是大于等于follower的,故此leader的lastProcessedZxid的epoch一定是大于等于follower的。

同步方式

在leader接收了follower的epochAck后,会根据这个epochAck中的follower的对应的zxid来确定同步方式,而其同步方式主要有三种

  • diff:这种方式是直接发送事务日志给follower的方式
  • truncate:这种方式是follower中存在leader没有的事务id,则leader会要求follower先将本地的事务日志截断到和leader相同的最后的一条,然后follower再接收leader发送的事务日志来进行同步
  • sanp:snap的形式则是leader会将当前的内存数据库的快照直接同步到follower。

下面是具体的确定同步的方式的逻辑,主要是先尝试用diff或者truncate的方式,不行了才使用snap的形式。

boolean syncFollower(long peerLastZxid, LearnerMaster learnerMaster) {  
     //这里表示这个peerLastZxid是新的zxid.
     //可能有这样的情况,leader会在选举成功之后,并且重新开始启动之后,会将本地的lastProcessedZxid变为新的(epoch+1)<<32
     //此时follower再次进行同步收到的zxid则就会为(epoch+1)<<32
     //然后follower再断线又重连,重新进行同步操作,但是此时followeer的peerLastZxid已经为(epoch+1)<<32,会比maxCommittedLog
     //要大
     boolean isPeerNewEpochZxid = (peerLastZxid & 0xffffffffL) == 0;
     long currentZxid = peerLastZxid;
     boolean needSnap = true;
     ZKDatabase db = learnerMaster.getZKDatabase();
     boolean txnLogSyncEnabled = db.isTxnLogSyncEnabled();
     ReentrantReadWriteLock lock = db.getLogLock();
     ReadLock rl = lock.readLock();
     try {
         rl.lock();
         long maxCommittedLog = db.getmaxCommittedLog();//内存中缓存的事务日志的最小的zxid
         long minCommittedLog = db.getminCommittedLog();//内存中缓存的事务日志中最大的zxid
         long lastProcessedZxid = db.getDataTreeLastProcessedZxid();//数据库处理的最大的事务日志id
         if (db.getCommittedLog().isEmpty()) {
             minCommittedLog = lastProcessedZxid;
             maxCommittedLog = lastProcessedZxid;
         }
         if (forceSnapSync) {
             
         } else if (lastProcessedZxid == peerLastZxid) {//leader和learner已经同步了则直接发送一个空的diff给learner
             queueOpPacket(Leader.DIFF, peerLastZxid);
             needOpPacket = false;
             needSnap = false;
         } else if (peerLastZxid > maxCommittedLog && !isPeerNewEpochZxid) {
             //learner的peerLastZxid比当前缓存的maxCommittedLog还大,则需要进程截断操作
             queueOpPacket(Leader.TRUNC, maxCommittedLog);
             currentZxid = maxCommittedLog;
             needOpPacket = false;
             needSnap = false;
         } else if ((maxCommittedLog >= peerLastZxid) && (minCommittedLog <= peerLastZxid)) {
             //这里表示peerLastZxid在内存缓存的日志消息中,则直接从内存中读取日志消息加入到发送队列中
             Iterator<Proposal> itr = db.getCommittedLog().iterator();
             currentZxid = queueCommittedProposals(itr, peerLastZxid, null, maxCommittedLog);
             needSnap = false;
         } else if (peerLastZxid < minCommittedLog && txnLogSyncEnabled) {
             //这里表peerLastZxid比内存缓存的日志要小,则需要从磁盘中读取数据
             long sizeLimit = db.calculateTxnLogSizeLimit();//这里计算的是我们最多能从磁盘读取的最多的日志,多了则通过snap恢复
             Iterator<Proposal> txnLogItr = db.getProposalsFromTxnLog(peerLastZxid, sizeLimit);
             if (txnLogItr.hasNext()) {
                 currentZxid = queueCommittedProposals(txnLogItr, peerLastZxid, minCommittedLog, maxCommittedLog);
                 if (currentZxid < minCommittedLog) {//表示利用磁盘事务日志无法恢复到内存缓存的最小的一条日志,改为snap恢复
                     currentZxid = peerLastZxid;//清理通过磁盘加入的事务日志队列和恢复到的数据   
                     queuedPackets.clear();
                     needOpPacket = true;
                 } else {//剩下的可以通过内存缓存恢复,则利用内存缓存进行恢复
                     Iterator<Proposal> committedLogItr = db.getCommittedLog().iterator();
                     currentZxid = queueCommittedProposals(committedLogItr, currentZxid, null, maxCommittedLog);
                     needSnap = false;
                 }
             }
             if (txnLogItr instanceof TxnLogProposalIterator) {//关闭日志读取迭代器
                 TxnLogProposalIterator txnProposalItr = (TxnLogProposalIterator) txnLogItr;
                 txnProposalItr.close();
             }
         } else {
             //这里可能存在peerLastZxid > maxCommittedLog并且isPeerNewEpochZxid==true的情况
         }
         if (needSnap) {
             currentZxid = db.getDataTreeLastProcessedZxid();
         }
         //将正字proposalRequestProcessor,CommitProcessor这些正在处理的事务消息加入到事务队列中
         leaderLastZxid = learnerMaster.startForwarding(this, currentZxid);
     } finally {
         rl.unlock();
     }
     //到此处还是有需要写入diff或者trunc类型的包操作,而不需要进行snap操作
     //这种情况应该是不会出现的,这里做保险因为后续不会产生diff或者trunc包,所以将其强制转换为snap形式
     if (needOpPacket && !needSnap) {
         needSnap = true;
     }
     return needSnap;
 }


//将peerLastZxid到maxZxid的事务日志加入到发送队列中
protected long queueCommittedProposals(Iterator<Proposal> itr, long peerLastZxid, Long maxZxid, Long lastCommittedZxid) {
     boolean isPeerNewEpochZxid = (peerLastZxid & 0xffffffffL) == 0;
     long queuedZxid = peerLastZxid;//表示的是加入到消息发送队列的最后的一条日志的zxid
     long prevProposalZxid = -1;//从迭代器中获取的当前的事务日志的前一个事务日志的id
     while (itr.hasNext()) {
         Proposal propose = itr.next();
         long packetZxid = propose.packet.getZxid();
         if ((maxZxid != null) && (packetZxid > maxZxid)) {//已经同步maxZxid则跳出循环
             break;
         }
         if (packetZxid < peerLastZxid) {//跳过比peerLastZxid小的事务日志
             prevProposalZxid = packetZxid;
             continue;
         }
         if (needOpPacket) {//还没确定是diff还是trunc形式的同步方式
             if (packetZxid == peerLastZxid) {//这个peerLastZxid是在本地事务日志中,则发送diff形式的同步
                 queueOpPacket(Leader.DIFF, lastCommittedZxid);
                 needOpPacket = false;
                 continue;
             }
             if (isPeerNewEpochZxid) { //这里的isPeerNewEpochZxid是新的epoch,则直接发送Diff而不发送trunc
                 // Send diff and fall through if zxid is of a new-epoch
                 queueOpPacket(Leader.DIFF, lastCommittedZxid);
                 needOpPacket = false;
             } else if (packetZxid > peerLastZxid) {
                 //follower的事务日志超过leader多个周期,不能跨周期的trunc,因为follower的数据可能已经有损坏
                 //所以进行snap同步
                 if (ZxidUtils.getEpochFromZxid(packetZxid) != ZxidUtils.getEpochFromZxid(peerLastZxid)) {
                     return queuedZxid;
                 }
                 //peerLastZxid不再本地的事务日志中,则需要发送trunc将其先回滚到其达到这个peerLastZxid之前的一个事务中
                 queueOpPacket(Leader.TRUNC, prevProposalZxid);
                 needOpPacket = false;
             }
         }
         //读取的日志数据比加入到队列中的zxid要小,可能是事务日志中存在重复的日志
         if (packetZxid <= queuedZxid) {            
             continue;
         }
         queuePacket(propose.packet);//加入proposal消息到发送队列中
         queueOpPacket(Leader.COMMIT, packetZxid);//加入commit消息到发送队列中
         queuedZxid = packetZxid;
     }
     if (needOpPacket && isPeerNewEpochZxid) {
         queueOpPacket(Leader.DIFF, lastCommittedZxid);
         needOpPacket = false;
     }
     return queuedZxid;
 }

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值