ZAB协议恢复模式-数据同步

964 阅读12分钟

本文讨论一下Leader选举以后的事情,并了解zookeeper的集群管理原理。
提前说明:

  • 本文主题虽然是讲述崩溃恢复模式,不过也会对广播模式的内容进行简单的描述。
  • 为了在文中描述不至于太过啰嗦,所以对超过半数省略掉了一个限定范围。例如当出现类似于“超过半数follower与leader同步”,“收到超过半数follower的回复”这种描述时,这种描述不正确,因为这个半数计算的时候是包含leader的。即如果文中讲述“超过半数”其实指的是leader、follower在一起,超过半数,或者所有服务器(不包括observer)超过半数。另外,Leader一定与自己是同步的。

本文主要讨论内容如下:

  • lead流程。
  • followLeader流程。
  • 主从数据如何同步。
  • 如何保证集群数据一致性。
  • 异常场景分析。

1 主从同步简化流程

1.1 模型

在这里插入图片描述

以上为主从同步的简化模型,即:

  • follower连接leader,将last zxid告诉leader;
  • leader根据last zxid确定同步点,然后将已经提交的协议发送给follower;
  • follower同步完成后同通知leader。

1.2 分析

此模型可以说它正确,也可以说它不正确,说它对是因为大体上确实是如此的;但如果剖根问底,同步的每一步具体是怎样的,这个图就显得不那么正确了。Zookeeper主从同步过程中有非常多消息,每种消息分别处理不同业务,并非上面简单的三步。
如果你只想知道个大概,那么就按照这个图理解就可以了,如果想了解更加详细的内容,搞清楚具体原理请继续阅读后续内容。

1.3 问题

因为这个模型太过于简单,它无法回答以下几个问题:

  • 如果follower的zxid大于leader的zxid将会如何?
    这种情况是可能存在的,例如:之前leader是S1服务器,leader宕机后过了一段时间才重新加入到集群中来,加进来是已经选好了新的leader(例如S2服务器),而此时S2的zxid小于S1,此时会如何?
  • Follower连不上leader,或者连接上了以后leader一直没有反应,集群将会如何?
    例如:集群选举时leader正常,选举完成以后,在数据同步完成之前或数据同步过程中leader不可用了,那么集群将会如何?
  • 数据同步时,如果有部分Follower同步的很慢,集群一直无法完成崩溃恢复这一过程会如何?
  • Zookeeper集群是如何保证follower和leader同步以后才能接受请求的?
  • Leader、follower的通信时有哪些类型,如何保证消息不错乱,通信过程安全可靠的?

接下来会从两个角度分析ZAB协议恢复模式中数据同步逻辑。

  • 从leader、follower流程上分析。
  • 从leader、follwer数据交互上分析。

2 流程分析

此部分从leader、follower的流程初步探讨zookeeper集群工作原理,因为存在大量的主从交互,所以从流程上不一定很好理解,所以这一部分从整体上介绍一下工作原理,更加详细的部分放在“主从交互详解”中介绍。

2.1 Leader

2.1.1 流程图

leader的主流程如下:
在这里插入图片描述

因为上图中的多个子流程基本上都是在LearnerHandler中实现的,所以我将这几个这里其他的几个子流程放在LearnerHandler中描述。

2.1.2 分析

2.1.2.1 流程分析

  • Leader每接受一个follower的链接,均会创建一个LearnerHandler对象,以后leader与此follower的交互均在LearnerHandler类中完成。
  • 接着就是确定新的epoch,必须从半数以上follower处获取epoch信息以后,才能生成新的epoch,即这里会会导致leader等待。见Leader#getEpochToPropose方法。
  • 然后是通知所有follower新的epoch,这个过程需要确保集群半数以上服务器达成一致,所以会等待。见leader#waitForEpochAck方法。
  • 然后等待集群完成崩溃恢复中数据同步逻辑,当集群中超过半数以上服务器发送ACK信息给leader以后,leader认为同步完成,恢复模式结束,接着进入广播模式。见Leader#waitForNewLeaderAck方法。
  • ZAB协议中恢复模式完成以后,集群进入广播模式,与之对应的是Leader会进入一个无限循环之中,在此循环中每隔一段时间统计一次同步的follower信息,并校验是否还有超过半数的服务器与leader同步,如果少于一半,则退出lead流程。

2.1.2.2 广播模式示例代码

boolean tickSkip = true;
while (true) {
    //休眠
    Thread.sleep(self.tickTime / 2);
    if (!tickSkip) {
        self.tick++;
    }
    HashSet<Long> syncedSet = new HashSet<Long>();
    
    // lock on the followers when we use it.
    syncedSet.add(self.getId());
    
    for (LearnerHandler f : getLearners()) {
        // Synced set is used to check we have a supporting quorum,
        // so only
        // PARTICIPANT, not OBSERVER, learners should be used
        if (f.synced() && f.getLearnerType() == LearnerType.PARTICIPANT) {
            //更新同步集合列表
            syncedSet.add(f.getSid());
        }
        //心跳维持
        f.ping();
    }
    
    // check leader running status
    if (!this.isRunning()) {
        shutdown("Unexpected internal error");
        return;
    }
    
    if (!tickSkip && !self.getQuorumVerifier().containsQuorum(syncedSet)) {
        //超过半数以上服务器与leader不同步时,退出lead流程
        // Lost quorum, shutdown
        shutdown("Not sufficient followers synced, only synced with sids: [ " + getSidSetString(syncedSet)
                + " ]");
        // make sure the order is the same!
        // the leader goes to looking
        return;
    }
    tickSkip = !tickSkip;
}

2.2 LearnerHandler

2.2.1 流程图

  • LearnerHandler主流程
    在这里插入图片描述
  • 计算集群epoch在这里插入图片描述
  • 集群更新epoch
    在这里插入图片描述
  • 集群数据同步
    在这里插入图片描述
  • 广播模式
    在这里插入图片描述

2.2.2 分析

LearnerHandler是leader中最为核心的一个类,他和Leader一起合作保证集群正常稳定的运行。LearnerHandler继承自ZooKeeperThread,由此可以看出它是一个线程,此线程负责和此follower的所有通信,具体来说需要负责以下几件事情:

  • 取follower得epoch,以便于leader计算出集群新的epoch。此过程需要确保超过半数以上follower告知leader他们的acceptedEpoch。
  • 通知集群服务器新的epoch。
  • Follower收到epoch后会回复一个消息(条件是protocolVersion>=0x10000,不满足此条件的流程略有差别),等待leader收到超过半数的follower回复此消息。此过程也是需要等待,确保整个集群半数以上服务器epoch已经一致了。
  • 根据follower的last zxid决定同步的起点,然后将需要同步的数据加入到一个队列中,新启动一个线程将这些数据发送给follower。此过程需要等待半数以上服务器完成与leader的同步。此过程结束则集群的崩溃恢复过程技术。
  • 将进入无限循环,读取数据包、处理数据包,广播给follower,即进入广播模式。

2.3 Follower

2.3.1 流程图

  • Follwer主流程
    在这里插入图片描述
  • Epoch同步流程在这里插入图片描述
  • 主从数据同步流程
    在这里插入图片描述
  • 广播模式
    在这里插入图片描述

2.3.2 分析

Follower的逻辑较为简单,总体来说负责以下内容:

  • 告知leader自己的acceptedEpoch,以便于leader计算出集群新的epoch。
  • 接收leader新epoch信息,以便于和集群保持一致的epoch。
  • 同步之前已经committed的数据,以便于和leader同步。
  • 通知leader自己已经同步完成,以便于集群退出崩溃恢复模式。
  • 数据同步完成以后,将进入无限循环,读取数据包、处理数据包,即进入广播模式,处理提议信息。

3 主从交互详解

接下来从主从交互的角度上详细讨论一下zookeeper工作原理。

3.1 主从交互图

在这里插入图片描述

说明:
以上模型是根据zookeeper-3.4.10画出来的。
在zookeeper不同版本中,逻辑会稍有不同,不过从整体上而言基本相同,数据同步逻辑中这种区别具体体现在:Leader端会根据follower传输过来的protocolVersion,做一些特殊处理。例如: protocolVersion<0x10000时,不会在等待ack前发送LEADERINFO消息;在上面流程如第7步中,如果protocolVersion<0x10000,会先写一个NEWLEADER消息到follower等。
Observer的流程和follower类似,主要区别在于:不参与确认集群状态,不参与提议处理。

3.2 模型详细分析

以下重点对模型中标注数字的模块进行详细说明,阅读时请参考上面模型图。

3.2.1 「1」从磁盘中加载数据

加载磁盘数据,获取lastProcessZxid用于后续生成newEpoch。每次leader诞生以后,都需要升级epoch。

3.2.2 「2」保证有超过半数的follower与leader建立连接

  • 创建LearnerHandler
    leader生成的条件时需要半数以上follower选此服务器;选出leader后,需要保证有超过半数的follower能和leader建立通信。
    Leader会创建LearnerCnxAcceptor线程,专门负责接收follower的连接,将leader与follower建立连接以后,则创建一个LearnerHandler线程。
    LearnerCnxAcceptor#run示例代码如下:
while (!stop) {
    try {
        Socket s = ss.accept();
        // start with the initLimit, once the ack is processed
        // in LearnerHandler switch to the syncLimit
        s.setSoTimeout(self.tickTime * self.initLimit);
        s.setTcpNoDelay(nodelay);

        BufferedInputStream is = new BufferedInputStream(s.getInputStream());
        // 建立处理learner请求的处理器
        LearnerHandler fh = new LearnerHandler(s, is, Leader.this);
        fh.start();
    } catch (SocketException e) {
        if (stop) {
            LOG.info("exception while shutting down acceptor: " + e);

            // When Leader.shutdown() calls ss.close(),
            // the call to accept throws an exception.
            // We catch and set stop to true.
            stop = true;
        } else {
            throw e;
        }
    } catch (SaslException e) {
        LOG.error("Exception while connecting to quorum learner", e);
    }
}
  • 为集群生成新的epoch
    通过Leader#getEpochToPropose方法计算epoch。这个方法会等待一段时间,如果这段时间内有超过半数follower成功与leader建立链接,那么将这些服务器中最大的epoch+1作为新的epoch。此过程就是。
    Leader#getEpochToPropose示例代码如下:
private HashSet<Long> connectingFollowers = new HashSet<Long>();

/**
 * 获取提议的epoch,此方法会等待足够多的follower进来,以确定epoch
 * <li>如果lastAcceptedEpoch>leader.epoch,那么设置
 * {@code leader.epoch = lastAcceptedEpoch+1};
 * <li>如果没有超过半数的follower与leader通信,那么进入等待,直到超时或者足够的follower与leader建立通信。
 * 
 * @param sid
 * @param lastAcceptedEpoch
 *            : 最近接受的epoch,新leader选举出来以后的epoch不小于此值。
 * @return
 * @throws InterruptedException
 * @throws IOException
 */
public long getEpochToPropose(long sid, long lastAcceptedEpoch) throws InterruptedException, IOException {
    synchronized (connectingFollowers) {
        if (!waitingForNewEpoch) {
            return epoch;
        }
        if (lastAcceptedEpoch >= epoch) {
            epoch = lastAcceptedEpoch + 1;
        }
        connectingFollowers.add(sid);
        QuorumVerifier verifier = self.getQuorumVerifier();
        if (connectingFollowers.contains(self.getId()) && verifier.containsQuorum(connectingFollowers)) {
            waitingForNewEpoch = false;
            self.setAcceptedEpoch(epoch);
            connectingFollowers.notifyAll();
        } else {
            long start = System.currentTimeMillis();
            long cur = start;
            long end = start + self.getInitLimit() * self.getTickTime();
            while (waitingForNewEpoch && cur < end) {
                connectingFollowers.wait(end - cur);
                cur = System.currentTimeMillis();
            }
            if (waitingForNewEpoch) {
                throw new InterruptedException("Timeout while waiting for epoch from quorum");
            }
        }
        return epoch;
    }
}
  • 统一集群epoch
    当protocolVersion>=0x10000时,leader会发送LEADERINFO消息并等待follower返回信息,并且需要保证在指定时间内,有超过半数的follwer回复了此消息。protocolVersion<0x10000时此过程逻辑稍微有一点不同,比较简单,所以暂不讨论。
private HashSet<Long> electingFollowers = new HashSet<Long>();
private boolean electionFinished = false;

/**
 * 等待超过半数的follower ack
 * <li>首先判断当前leader的epoch、lastZxid是否比参数{@link ss}更新,<br>
 * <ol>
 * <li>如果新,那么将follower的sid加入到electingFollowers列表</li>
 * <li>如果ss更新,说明follower的epoch或者lastZxid比leader更大,
 * 那么follower不能加入到集群中来,所以抛出错误信息。</li>
 * </ol>
 * </li>
 * <li>检查electingFollowers集合,判断是否有超过半数的follower回复了leader,如果是,唤醒线程;
 * 如果不是那么等待直到超时或者达到半数这个条件。</li>
 * 
 * @param id
 * @param ss
 * @throws IOException
 * @throws InterruptedException
 */
public void waitForEpochAck(long id, StateSummary ss) throws IOException, InterruptedException {
    synchronized (electingFollowers) {
        if (electionFinished) {
            return;
        }
        if (ss.getCurrentEpoch() != -1) {
            if (ss.isMoreRecentThan(leaderStateSummary)) {
                // follower 比 leader 更新(事物id更大),抛出错误信息
                throw new IOException(
                        "Follower is ahead of the leader, leader summary: " + leaderStateSummary.getCurrentEpoch()
                                + " (current epoch), " + leaderStateSummary.getLastZxid() + " (last zxid)");
            }
            electingFollowers.add(id);
        }
        QuorumVerifier verifier = self.getQuorumVerifier();
        if (electingFollowers.contains(self.getId()) && verifier.containsQuorum(electingFollowers)) {
            electionFinished = true;
            electingFollowers.notifyAll();
        } else {
            long start = System.currentTimeMillis();
            long cur = start;
            long end = start + self.getInitLimit() * self.getTickTime();
            while (!electionFinished && cur < end) {
                electingFollowers.wait(end - cur);
                cur = System.currentTimeMillis();
            }
            if (!electionFinished) {
                throw new InterruptedException("Timeout while waiting for epoch to be acked by quorum");
            }
        }
    }
}

当同时满足epoch验证和ack验证是,认为有超过半数的follower与leader建立连接了,不满足条件时会抛出异常,退出lead流程。

3.2.3 「3」等待同步完成的消息

leader与follower、observer建立链接以后,需要进行数据同步,同样的,当在指定时间内和leader同步的follower少于一半时,也会抛出异常,退出lead流程。
单个learner与leader是否同步的判断标准是:同步完成后follower会发送ACK消息给leader,leader收到此消息,认为这个follower和leader同步了。
Leader#waitForNewLeaderAck示例:

/**
 * LeaderHandler在收到ACK消息时调用此方法,用于告知leader同步已经完成。
 * <li>此方法需要保证超过一半的服务器与leader保持一致。
 *
 * @param sid
 * @param learnerType
 * @throws InterruptedException
 */
public void waitForNewLeaderAck(long sid, long zxid, LearnerType learnerType) throws InterruptedException {

    synchronized (newLeaderProposal.ackSet) {

        if (quorumFormed) {
            return;
        }

        long currentZxid = newLeaderProposal.packet.getZxid();
        if (zxid != currentZxid) {
            LOG.error("NEWLEADER ACK from sid: " + sid + " is from a different epoch - current 0x"
                    + Long.toHexString(currentZxid) + " receieved 0x" + Long.toHexString(zxid));
            return;
        }

        if (learnerType == LearnerType.PARTICIPANT) {
            newLeaderProposal.ackSet.add(sid);
        }

        if (self.getQuorumVerifier().containsQuorum(newLeaderProposal.ackSet)) {
            quorumFormed = true;
            newLeaderProposal.ackSet.notifyAll();
        } else {
            long start = System.currentTimeMillis();
            long cur = start;
            long end = start + self.getInitLimit() * self.getTickTime();
            while (!quorumFormed && cur < end) {
                newLeaderProposal.ackSet.wait(end - cur);
                cur = System.currentTimeMillis();
            }
            if (!quorumFormed) {
                throw new InterruptedException("Timeout while waiting for NEWLEADER to be acked by quorum");
            }
        }
    }
}

3.2.4 「4」进入广播模式

leader进入while无限循环,每次循环先休眠一段时间,然后检验是否有超过一半的follower与leader保持同步,是否被设置为停止(running=false),如果满足这两个条件之一,则退出lead流程。

3.2.5 「5」确认leader、follower确认开始

在Leader、LearnerHandler中我们多次提到主从需要计算出新的epoch,那么他是如何计算的呢?这里将详细讲述这个过程。
选主完成后,follower去链接leader,链接好以后需要完成以下几件事情。

  • follower发送FOLLOWERINFO消息(带上AcceptedEpoch、protocolVersion= 0x10000)。
  • leader等待超过半数的follower发送FOLLOWERINFO信息给leader,然后计算出epoch(如有疑问,见2步)。接着回复LEADERINFO消息(带上epoch、protocolVersion= 0x10000)。这里和leader#getEpochToPropose相对应。
  • Follower收到消息以后回复ACKEPOCH消息(带上epoch、lastLoggedZxid)。
    过程有点绕,我们解释一下这么做的原因:
  • 集群已超过半数为基本条件,这意味着epoch计算时,leader需要通过超过半数follower的epoch来计算出一个新的epoch,所以就必须等待这个条件。
  • Leader需要将计算出的新epoch告诉follower。这也比较好理解:一届leader选举出以后,以后所有的数据应该都发生在此届之中,所以应该统一集群的epoch。
  • Follower再次回复信息,一来是做个ack,告知已经让epoch和leader的epoch保持一致了;二来是为后续确定同步点做准备。

3.2.6 「6」确保超过半数的follower ack leader

5中的最后一步指的是一个follower回复ACKEPOCH消息,但是集群需要确保超过半数的follower ack,所以这里会有一个等待确认的过程。这里和leader中等待ack过程相对应。
等待leader收到半数以上的ack,是为了确保整个集群多数派已经更新了epoch。

3.2.7 「7」确定同步点

3.2.7.1 leader中确定同步节点的逻辑是:
  • 首先设置type=SNAP
  • 如果follower.lastZxid = leader.lastProcessedZxid,那么设置type=DIFF。
  • 如果commitedLog为空,那么设置type=DIFF。
  • 如果follower.lastZxid > leader.maxCommittedLog, 那么设置type= TRUNC。
  • 如果leader.maxCommittedLog >= follower.lastZxid并且 leader.minCommittedLog <= follower.lastZxid,确定同步起始位置,并且将此后的所有提议消息(提议+commit提议两个消息)加入queuedPackets中。
  • 将type、zxid发送个follower
    示例代码如下:
ReentrantReadWriteLock lock = leader.zk.getZKDatabase().getLogLock();
ReadLock rl = lock.readLock();
try {
    rl.lock();
    final long maxCommittedLog = leader.zk.getZKDatabase().getmaxCommittedLog();
    final long minCommittedLog = leader.zk.getZKDatabase().getminCommittedLog();
    LOG.info("Synchronizing with Follower sid: " + sid + " maxCommittedLog=0x"
            + Long.toHexString(maxCommittedLog) + " minCommittedLog=0x" + Long.toHexString(minCommittedLog)
            + " peerLastZxid=0x" + Long.toHexString(peerLastZxid));

    LinkedList<Proposal> proposals = leader.zk.getZKDatabase().getCommittedLog();

    if (peerLastZxid == leader.zk.getZKDatabase().getDataTreeLastProcessedZxid()) {
        // 同伴(follower)的事务id和leader的事务id相同,说明follower和leader是同步的。
        LOG.info("leader and follower are in sync, zxid=0x{}", Long.toHexString(peerLastZxid));
        packetToSend = Leader.DIFF;
        zxidToSend = peerLastZxid;
    } else if (proposals.size() != 0) {
        LOG.debug("proposal size is {}", proposals.size());
        if ((maxCommittedLog >= peerLastZxid) && (minCommittedLog <= peerLastZxid)) {
            // follower有数据需要从leader处同步
            LOG.debug("Sending proposals to follower");

            // as we look through proposals, this variable keeps
            // track of previous
            // proposal Id.
            long prevProposalZxid = minCommittedLog;

            // Keep track of whether we are about to send the first
            // packet.
            // Before sending the first packet, we have to tell the
            // learner
            // whether to expect a trunc or a diff
            boolean firstPacket = true;

            // If we are here, we can use committedLog to sync with
            // follower. Then we only need to decide whether to
            // send trunc or not
            packetToSend = Leader.DIFF;
            zxidToSend = maxCommittedLog;

            for (Proposal propose : proposals) {
                // skip the proposals the peer already has
                if (propose.packet.getZxid() <= peerLastZxid) {
                    prevProposalZxid = propose.packet.getZxid();
                    continue;
                } else {
                    // If we are sending the first packet, figure
                    // out whether to trunc in case the follower has
                    // some proposals that the leader doesn't
                    if (firstPacket) {
                        firstPacket = false;
                        // Does the peer have some proposals that
                        // the leader hasn't seen yet
                        if (prevProposalZxid < peerLastZxid) {
                            // send a trunc message before sending
                            // the diff
                            packetToSend = Leader.TRUNC;
                            zxidToSend = prevProposalZxid;
                            updates = zxidToSend;
                        }
                    }
                    queuePacket(propose.packet);
                    QuorumPacket qcommit = new QuorumPacket(Leader.COMMIT, propose.packet.getZxid(), null,
                            null);
                    queuePacket(qcommit);
                }
            }
        } else if (peerLastZxid > maxCommittedLog) {
            LOG.debug("Sending TRUNC to follower zxidToSend=0x{} updates=0x{}",
                    Long.toHexString(maxCommittedLog), Long.toHexString(updates));

            packetToSend = Leader.TRUNC;
            zxidToSend = maxCommittedLog;
            updates = zxidToSend;
        } else {
            LOG.warn("Unhandled proposal scenario");
        }
    } else {
        // just let the state transfer happen
        LOG.debug("proposals is empty");
    }

    LOG.info("Sending " + Leader.getPacketType(packetToSend));
    leaderLastZxid = leader.startForwarding(this, updates);

} finally {
    rl.unlock();
}
3.2.7.2 Follower收到消息后进行一下处理:
  • 如果type= DIFF,说明主从有差异,直接接收数据就好。
  • 如果type= SNAP,那么说明主从差异太多了,直接从主下载snapshot数据。
  • 如果type= TRUNC,说明从的zxid比主大,需要将follower中大的这部分提议清理掉。
  • 如果type为其他值,那说明异常了,退出服务。

3.2.8 「8」数据同步

3.2.8.1 leader
  • 向queuedPackets队列中加入NEWLEADER消息。
  • 在LearnerHandler线程中启动一个新线程将queuedPackets中的消息发送给follower。
  • 因为最后一个消息是NEWLEADER,Follower收到此消息时,回复一个ACK消息,leader根据此消息确定同步过程结束。
  • 等待指定的时间,确保leader半数以上的follower都已经完成数据同步,指定时间内集群满足这个条件,则集群成形;不满足则抛出异常,退出lead流程。
  • 向queuedPackets队列中加入UPTODATE消息。
3.2.8.2 follower

接收leader发送过来的数据。
当收到NEWLEADER消息时,表示目前所有消息已经全部同步完了,Follower回复ACK消息告知Leader目前最新的zxid。
Leader收到ACK消息后,等待集群成形,即崩溃恢复模式结束,成型后会给follower发送NEWLEADER消息,follower收到NEWLEADER后退出同步逻辑。

3.2.8.3 说明

截止此步骤结束,整个zookeeper集群方退出崩溃恢复模式。到此步骤成功前的任何一个步骤失败或者条件不满足,均退出lead流程、follower流程,即再次进入选举流程。

3.2.9 「9」进入广播模式

这一环节是zookeeper集群接收提议、提交提议、维持心跳等逻辑的环节,即zookeeper集群正常运行的环节。这一阶段的过程大概如下:
Leader收到请求后,将请求封装成提议发送给follower,消息类型为PROPOSAL
Follower收到PROPOSAL消息以后,回复Leader一个ACK消息。
Leader发现一个PROPOSAL有超过半数以上回复,则认为此消息可以commit了,那么发送COMMIT消息给follower。

这一环节还有几种消息:

  • PING:主从心跳维持消息。
  • REVALIDATE:延长session,保证主从会话有效。
  • SYNC:将已提交的数据刷新到committedRequests集合。
  • UPTODATE:这个环节应该受到整个消息,如果收到了留一条日志。

3.3 总结

Zookeeper数据同步核心就是在于上面提到的各种消息。接下来我们总结一下上面的整个流程。

3.3.1 建立链接

选举后Leader、Follower身份将被确立下来;接着Follower向Leader发起连接,Leader创建LearnerHandler。

3.3.2 确定集群epoch

leader等待超过半数follower链接,计算出新的epoch。
protocolVersion>0x10000时,Leader会等待Follower确认epoch。

3.3.3 同步数据

leader根据follower给的zxid信息,计算出需要同步的数据,并将这些数据放到queuedPackets队列中。
在发送已提交的提议数据前,会发送给follower一个消息,告诉follower进行一些预处理。例如:如果follower的zxid太大,那么截取部分消息;如果follower落后太远,那么直接从leader获取snapshot等。
启动线程发送queuedPackets中的提议消息。
提议消息发送完成后,Leader发送发送NEWLEADER消息告知follower同步已结束;follower收到此消息以后会回复ACK消息给Leader,表示知道已经同步结束了。注意,leader会等待至少半数以上的Follower ACK,因为follower同步过程耗时可能差别较大,这里需要等待一段时间,让follower跟上leader。
超过半数Follwer ACK,意味着整个集群大部分服务器已经和leader同步了,那么Zookeeper集群也就“成形”了。leader再给一个UPTODATE消息,告诉follower退出同步逻辑。

3.3.4 进入广播模式

follower、LearnerHandler都将进入无限循环,处理REQUEST、发送提议、提交提议、维持集群心跳。

4 相关文档

ZAB协议恢复模式-数据同步

ZAB协议恢复模式-Leader选举

Zookeeper原理