最近在看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就肯定只有少部分节点甚至挂掉了。
理解过程中的一些问题
会不会出现假Leader的情况?
非常有可能,只要半数以上节点重新发起投票,必然会选出一个Leader
为什么半数节点都重新选举了,原来的Leader没有"挂掉"呢?
因为Leader不会每时每刻都检测,而是tickTime或者tickTime/2的间隔下检查。tickTime默认值是2000ms
在下面的代码中,跳出条件是三个
tickSkip为Fasle
Leader少于半数票
处理一种特殊情况,节点等于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的区别:
Raft的投票流程是比Zab要简单的,其投票只会一进一出,且比较逻辑是周期大&commitId大;Zab的投票则是类似于水波,节点在收到投票后不是单独回复给发送方,而是转发给所有节点。
Raft的选举成功就是真的成功了,Zab的FLE阶段选出的Leader只是一个预备Leader,还要和其他节点同步并确定Epoch之后才算是真的Leader
Raft的Leader收到消息后立马变为Follower,而Zab的Leader只有真正自己挂了或者节点少于半数了才会重新进入选举阶段。
一种有趣的说法
Raft就好像混帮派,只要你枪大粮多小弟多,你就可以成为新老大,旧老大也会承认你的。
Zab更像封建皇权,要么等老皇帝死(挂掉),要么等他失去民心(持有链接少于一半节点),不然其他节点就算竞选也没有机会。
参考文档:
ZOOKEEPER335 为什么要区分acceptedEpoch 和 currentEpoch-腾讯云开发者社区-腾讯云