AcceptEpoch和CurrentEpoch区别

研究AcceptEpoch和CurrentEpoch区别,并重新理解Zab和Raft的选举流程区别

最近在看Raft算法,在看到一些文档说Zab和Raft区别时总觉得我看源码了解到的不同,但也不知道是不是版本的问题。保险起见,我去读了一下Zab论文。在论文最后一部分,有提到ZOOKEEPER-335,让我想起了当时看源码时选举之后那多个"countdown机制"的waitfor方法,当时也觉得云里雾里,于是借着论文理解一下这个issue,顺带再理清一下Zab。

AcceptEpoch和currentEpoch是个啥?

As mentioned, the implementation up to version 3.3.3 has not included epoch vari-ables acceptedEpoch and currentEpoch. This omission has generated problems [5] (issue ZOOKEEPER-335 in Apache’s issue tracking system) in a production version and was noticed by many ZooKeeper clients. The origin of this problem is at the beginning of Recovery Phase (Algorithm 4 line 2), when the leader increments its epoch (contained in lastZxid) even before acquiring a quorum of successfully connected followers (such leader is called false leader). Since a follower goes back to FLE if its epoch is larger than the leader’s epoch (line 25), when a false leader drops leadership and becomes a follower of a leader from a previous epoch, it finds a smaller epoch (line 25 )and goes back to FLE. This behavior can loop, switching from Recovery Phase to FLE.

原文的大致意思是,当一个集群出现一个假Leader(单是选举阶段得到了票数并Epoch+1但是后面没有和自己的Follower建立连接),在假Leader执行到Recovery(崩溃恢复)阶段时失败会重回FLE(快速选举阶段),然后作为真Leader的一个Follower,但此时由于假Leader的Epoch要更高,会再次回到FLE,如此循环。

模拟过程

首先,我们通过一个场景模拟并理解一下里面的假Leader,在这种场景中Epoch是从Zxid获取的。

假如集群现在是ABC三个节点,A节点是Leader,BC是Follower(且C的事务ID比B大),假设此时的Epoch是1。

此时BC因为网络问题重启了会尝试再次选举,假设此时BC的投票信息要快于A节点的正常消息(不然收到A的消息就加入A成为Follower了),那么C获得两票成为新的Leader,此时C的Epoch为2,A的Epoch为1。

接下来正常的流程应该是C和B建立连接作为大多数活下去,A由于联系不上其他节点超时之后重新进入选举流程。

但是在B和C建立连接之前,B收到了来自A的消息,回归了A的怀抱,C无法和B建立连接了,因为B已经不是Looking状态了。C只能放弃这个Leader身份(超时异常),然后再次进入FLE阶段并接受到了来自A的消息,然后发现自己的Epoch(2)大于Leader的Epoch(1),于是又进入了FLE阶段,如此循环。


在A的角度来看,自己并没有挂掉,不存在让出Leader的可能,在C的角度来看,自己明明是靠大多数节点的选择当选的Leader且自己的Epoch就是更大,但是现在就是联系不上其他节点,且还有一个存活的Leader把持着大部分节点。

如果想要破局,只有A承认C和C承认A这两种情况,相比之下第一种更合理,因为此时A占有大多数节点的连接(投票),是大多数认可的Leader,C只有口头投票,没有持有他们的连接。总结发现,只有拥有大多数投票,且持有大多数连接的才是真正的Leader,AcceptEpoch和CurrentEpoch就是这样一种方案。

CurrentEpoch还是Zxid里的那个概念,主要是AcceptEpoch。

本来两个节点比较的是由zxid取到的Epoch,可以理解为是当前节点自己所认为的周期。但是这个无法证明谁是持有了大多数,因此多增加了一个AcceptEpoch,表示的是大多数节点共识的周期,它的赋值是在Leader和Follwer建立连接之后。

流程是口头投票(预备Leader)–和其他节点建立连接—商讨并确认最新的Epoch。

新的Follower节点加入的时候要比的就是这个AcceptEpoch,小于等于Leader都是正常,只有大于才是异常,因为大于代表着该节点持有大部分节点的连接,那老的Leader就肯定只有少部分节点甚至挂掉了。

理解过程中的一些问题

  1. 会不会出现假Leader的情况?

    非常有可能,只要半数以上节点重新发起投票,必然会选出一个Leader

  2. 为什么半数节点都重新选举了,原来的Leader没有"挂掉"呢?

    因为Leader不会每时每刻都检测,而是tickTime或者tickTime/2的间隔下检查。tickTime默认值是2000ms

    在下面的代码中,跳出条件是三个

    1. tickSkip为Fasle

    2. Leader少于半数票

    3. 处理一种特殊情况,节点等于2,且主节点发起了提案,但Follower挂掉,这里我们先不关注这种很特殊的情况

    tickSkip是每次循环变动的,因此每两次循环才会检查一次,每次循环还要等tickTime/2的时间,理想的话可能刚好是节点失效的时候是只等待了[0,tickTime/2]时间,且tickSkip刚好是false,最慢的情况是tickSkip为True,那表示要等[tickTime/2,tickTime]时间才能校验。

    boolean tickSkip = true;
    //保持法定人数的统计和ping
    while (true) {
        synchronized(this) {
            long start = Time.currentElapsedTime();
            long cur = start;
            long end = start + self.tickTime / 2;
            //等待tickTime时间
            while (cur < end) {
                wait(end - cur);
                cur = Time.currentElapsedTime();
            }
            if (!tickSkip) {
                self.tick.incrementAndGet();
            }
            syncedAckSet.addAck(self.getMyId());
            //Learner表示Leader和Follower的连接,因此有几个Learner就有几票
            for (LearnerHandler f: getLearners()) {
                if (f.synced()) {
                    syncedAckSet.addAck(f.getSid());
                }
            }
            //tickSkip为false才会进检查,也就是说两次循环只有一次会进
            if (!tickSkip && !syncedAckSet.hasAllQuorums() &&
                !(self.getQuorumVerifier().overrideQuorumDecision(getForwardingFollowers()) &&
                    self.getQuorumVerifier().revalidateOutstandingProp(this, new ArrayList < > (outstandingProposals.values()), lastCommitted))) {
                break;
            }
            //每次循环变动
            tickSkip = !tickSkip;
        }
        for (LearnerHandler f: getLearners()) {
            f.ping();
        }
    }
    }
    

ZOOKEEPER-1154

论文中还提交了一个ISSUE,讲的是Recovery阶段的一个问题。是说假设有ABC三个节点,周期为1,Commit进度都是3,表示为(1,3),其中A是Leader;此时LeaderA发起提案(1,4),但是B和C还没收到提案就重启了(进入FLE阶段),而A也恰好挂掉了。此时B和C经过重新选举定为周期是(2,0),再经过几次提案之后变为(2,n),最小提交进度(所有节点都确定提交的进度)此时为(1,3),此时A恢复成为B的Follwer,在补足数据的时候因为(1,3)处于(1,1)和(2,n)之间,那么会收到Leader的SNAP或者DIFF,并不会对A节点的(1,4)处理,但是(1,4)其实应该丢弃的。

这个问题其实已经修复了,在同步的时候特殊处理

protected long queueCommittedProposals(xxxxx) {
    long prevProposalZxid = -1;
    while (itr.hasNext()) {
        Proposal propose = itr.next();
        long packetZxid = propose.packet.getZxid();
        //跳过已经有的,且将prevProposalZxid指针指向该条,比如这个例子最多指到(1,3)
        if (packetZxid < peerLastZxid) {
            prevProposalZxid = packetZxid;
            continue;
        }
        if (needOpPacket) {
            if (packetZxid == peerLastZxid) {
                queueOpPacket(Leader.DIFF, lastCommittedZxid);
                needOpPacket = false;
                continue;
            }
            //同属于一个周期不存在这种情况
            if (isPeerNewEpochZxid) {
                queueOpPacket(Leader.DIFF, lastCommittedZxid);
                needOpPacket = false;
            //这里应该是论文中说的ZOOKEEPER-1154
            //会在指针首次超过peerLastZxid时,通知Follower丢弃prevProposalZxid到peerLastZxid的数据
            } else if (packetZxid > peerLastZxid) {
                queueOpPacket(Leader.TRUNC, prevProposalZxid);
                needOpPacket = false;
            }
        }
    }
}

Raft算法有没有这种情况呢?

需要考虑Raft会不会有假Leader这种情况,通过官网动画构造这么一种情况,

S4是Leader,在发出第二个提案之后只有S2收到了,旨在让S2的commit进度比较靠前。然后让S2和S1几乎同时(但要让S1先超时)超时触发下一个周期的选举,且让S5投给S1,S3给S2,这样各有两票,观察S4的投票情况。

当S4收到S1的投票提案时,就从Leader转变为了Follower,但是给S1的答复确实拒绝,原因是S1的commit进度要小于S4(这是raft投票规则中的一个条件),当收到S2的投票提案后,给出的答案是同意,因为S2的commit的进度是等于S4的,最终S2成为新的Leader。

因此Raft算法不会出现这个Zab的假Leader情况。

借此,也可以看出几点Raft算法和Zab的区别:

  1. Raft的投票流程是比Zab要简单的,其投票只会一进一出,且比较逻辑是周期大&commitId大;Zab的投票则是类似于水波,节点在收到投票后不是单独回复给发送方,而是转发给所有节点。

  2. Raft的选举成功就是真的成功了,Zab的FLE阶段选出的Leader只是一个预备Leader,还要和其他节点同步并确定Epoch之后才算是真的Leader

  3. Raft的Leader收到消息后立马变为Follower,而Zab的Leader只有真正自己挂了或者节点少于半数了才会重新进入选举阶段。

一种有趣的说法

Raft就好像混帮派,只要你枪大粮多小弟多,你就可以成为新老大,旧老大也会承认你的。

Zab更像封建皇权,要么等老皇帝死(挂掉),要么等他失去民心(持有链接少于一半节点),不然其他节点就算竞选也没有机会。

参考文档:

ZOOKEEPER335 为什么要区分acceptedEpoch 和 currentEpoch-腾讯云开发者社区-腾讯云

ZAB算法

Licensed under CC BY-NC-SA 4.0
最后更新于 2024-10-18