SOFAStack ( Scalable Open Financial Architecture Stack)是蚂蚁金服自主研发的金融级分布式架构,包含了构建金融级云原生架构所需的各个组件,是在金融场景里锤炼出来的最佳实践。
本文为《剖析 | SOFAJRaft 实现原理》第四篇,本篇作者力鲲,来自蚂蚁金服《剖析 | SOFAJRaft 实现原理》系列由 SOFA 团队和源码爱好者们出品,项目代号:<SOFA:JRaftLab/>,目前领取已经完成,感谢大家的参与。
SOFAJRaft 是一个基于 Raft 一致性算法的生产级高性能 Java 实现,支持 MULTI-RAFT-GROUP,适用于高负载低延迟的场景。
SOFAJRaft :
https://github.com/sofastack/sofa-jraft
前言
在 Raft 算法中,选举是很重要的一部分,所谓选举也就是在多个节点中选出一个 Leader 节点,由他来对外提供写服务 (以及默认情况下的读服务)。
在剖析源码时,对选举机制的理解经常会遇到两极分化的情况,对于了解 Raft 算法基本原理的同学,阅读源码就是品味实现之巧妙的过程,而对初体验的同学,却会陷入丈二和尚的窘境,仿佛坠入云里雾里。
为了提升文章的可读性,我还是希望花一部分篇幅讲清楚选举机制的基本原理,以便后面集中注意力于代码实现。下面是一段图文比喻,帮助理解的同时也让整篇文章不至于过早陷入细节的讨论。
问题1:选举要解决什么
一个分布式集群可以看成是由多条战船组成的一支舰队,各船之间通过旗语来保持信息交流。这样的一支舰队中,各船既不会互相完全隔离,但也没法像陆地上那样保持非常密切的联系,天气、海况、船距、船只战损情况导致船舰之间的联系存在但不可靠。
舰队作为一个统一的作战集群,需要有统一的共识、步调一致的命令,这些都要依赖于旗舰指挥。各舰船要服从于旗舰发出的指令,当旗舰不能继续工作后,需要有别的战舰接替旗舰的角色。
图1 - 所有船有责任随时准备接替旗舰
如何在舰队中,选出一艘得到大家认可的旗舰,这就是 SOFAJRaft 中选举要解决的问题。
问题2:何时可以发起选举
何时可以发起选举?换句话说,触发选举的标准是什么?这个标准必须对所有战舰一致,这样就能够在标准得到满足时,所有舰船公平的参与竞选。在 SOFAJRaft 中,触发标准就是通信超时,当旗舰在规定的一段时间内没有与 Follower 舰船进行通信时,Follower 就可以认为旗舰已经不能正常担任旗舰的职责,则 Follower 可以去尝试接替旗舰的角色。这段通信超时被称为 Election Timeout (简称 ET), Follower 接替旗舰的尝试也就是发起选举请求。
图2 - ET 触发其他船竞选旗舰
问题3:何时真正发起选举
在选举中,只有当舰队中超过一半的船都同意,发起选举的船才能够成为旗舰,否则就只能开始一轮新的选举。所以如果 Follower 采取尽快发起选举的策略,试图尽早为舰队选出可用的旗舰,就可能引发一个潜在的风险:可能多艘船几乎同时发起选举,结果其中任何一支船都没能获得超过半数选票,导致这一轮选举无果,然后失败的 Follower 们再一次几乎同时发起选举,又一次失败,再选举 again,再失败 again ···
图3 - 同时发起选举,选票被瓜分
为避免这种情况,我们采用随机的选举触发时间,当 Follower 发现旗舰失联之后,会选择等待一段随机的时间 Random(0, ET) ,如果等待期间没有选出旗舰,则 Follower 再发起选举。
图4 - 随机等待时间
问题4:哪些候选者值得选票
SOFAJRaft 的选举中包含了对两个属性的判断:LogIndex 和 Term,这是整个选举算法的核心部分,也是容易让人产生困惑的地方,因此我们做一下解释:
Term
我们会对舰队中旗舰的历史进行编号,比如舰队的第1任旗舰、第2任旗舰,这个数字我们就用 Term 来表示。由于舰队中同时最多只能有一艘舰船担任旗舰,所以每一个 Term 只归属于一艘舰船,显然 Term 是单调递增的。LogIndex
每任旗舰在职期间都会发布一些指令(称其为“旗舰令”,类比“总统令”),这些旗舰令当然也是要编号归档的,这个编号我们用 Term 和 LogIndex 两个维度来标识,表示“第 Term 任旗舰发布的第 LogIndex 号旗舰令”。不同于现实中的总统令,我们的旗舰令中的 LogIndex 是一直递增的,不会因为旗舰的更迭而从头开始计算。
图5 - 总统令 Vs 旗舰令,LogIndex 稍有区别
所有的舰船都尽可能保存了过去从旗舰接收到的旗舰令,所以我们选举的标准就是哪艘船保存了最完整的旗舰令,那他就最有资格接任旗舰。具体来说,参与投票的船 V 不会对下面两种候选者 C 投票:一种是 lastTermC < lastTermV;另一种是 (lastTermV == lastTermC) && (lastLogIndexV > lastLogIndexC)。
稍作解释,第一种情况说明候选者 C 最后一次通信过的旗舰已经不是最新的旗舰了,至少比 V 更滞后,所以它所知道的旗舰令也不可能比 V 更完整。第二种情况说明,虽然 C 和 V 都与同一个旗舰有过通信,但是候选者 C 从旗舰处获得的旗舰令不如 V 完整 (lastLogIndexV > lastLogIndexC),所以 V 不会投票给它。
图6 - Follower 船 b 拒绝了船 c 而投票给船 a,船 a 旗舰令有一个空白框表示“第 Term 任旗舰”没有发布过任何旗舰令
问题5:如何避免不够格的候选者“捣乱”
如上一小节所说,SOFAJRaft 将 LogIndex 和 Term 作为选举的评选标准,所以当一艘船发起选举之前,会自增 Term 然后填到选举请求里发给其他船只 (可能是一段很复杂的旗语),表示自己竞选“第 Term + 1 任”旗舰。
这里要先说明一个机制,它被用来保证各船只的 Term 同步递增:当参与投票的 Follower 船收到这个投票请求后,如果发现自己的 Term 比投票请求里的小,就会自觉更新自己的 Term 向候选者看齐,这样能够很方便的将 Term 递增的信息同步到整个舰队中。
图7 - Follower 船根据投票请求更新自己的 Term
但是这种机制也带来一个麻烦,如果一艘船因为自己的原因没有看到旗舰发出的旗语,他就会自以为是的试图竞选成为新的旗舰,虽然不断发起选举且一直未能当选(因为旗舰和其他船都正常通信),但是它却通过自己的投票请求实际抬升了全局的 Term,这在 SOFAJRaft 算法中会迫使旗舰 stepdown (从旗舰的位置上退下来)。
图8 - 自以为是的捣乱者,迫使旗舰 stepdown
所以我们需要一种机制阻止这种“捣乱”,这就是预投票 (pre-vote) 环节。候选者在发起投票之前,先发起预投票,如果没有得到半数以上节点的反馈,则候选者就会识趣的放弃参选,也就不会抬升全局的 Term。
图9 - Pre-vote 预投票
选举剖析
在上面的比喻中,我们可以看到整个选举操作的主线任务就是:
Candidate 被 ET 触发
Candidate 开始尝试发起 pre-vote 预投票
Follower 判断是否认可该 pre-vote request
Candidate 根据 pre-vote response 来决定是否发起 RequestVoteRequest
Follower 判断是否认可该 RequestVoteRequest
Candidate 根据 response 来判断自己是否当选
这个过程可用下图表示:
图10 - 一次成功的选举
在代码层面,主要是由四个方法来处理这个流程:
com.alipay.sofa.jraft.core.NodeImpl#preVote //预投票
com.alipay.sofa.jraft.core.NodeImpl#electSelf //投票
com.alipay.sofa.jraft.core.NodeImpl#handlePreVoteRequest //处理预投票请求
com.alipay.sofa.jraft.core.NodeImpl#handleRequestVoteRequest //处理投票请求
代码逻辑比较直观,所以我们用流程图来简述各个方法中的处理。
预投票和投票
图11 - 预投票 Vs 投票
图中可见,预投票请求 preVote 和投票请求 electSelf 的流程基本类似,只是有几个细节不太一样:
preVote 是由超时触发;
preVote 在组装 Request 的时候将 term 赋值为 currTerm + 1,而 electSelf 是先将 term ++;
preVote 成功后,进入 electSelf,electSelf 成功后 become Leader。
处理请求
处理预投票和投票请求的逻辑也比较类似,同样用图来表示。
图12 - 处理预投票请求
图13 - 处理投票请求
图中可见,处理两种请求的流程也基本类似,只是处理投票请求的时候,会有 stepdown 机制,强制使 Leader 从其 Leader 的身份退到 Follower。在具体的实现中,Leader 会通过租约的机制来避免一些没有必要的 stepdown,关于租约机制,可以参见之前的系列文章《SOFAJRaft 线性一致读实现剖析 | SOFAJRaft 实现原理》。
总结
我们在本文中采用了类比的方式来剖析源码,主要是为了让读者能更容易的理解如何在分布式环境中达成共识,其实这也是整个 SOFAJRaft 要实现的目标。
行文至此,作者感觉已经把选举说清楚了,如果还有没提到的地方,或者一些流程中的分支任务,欢迎从源码中进一步寻找答案。最后贴出上面提到的四个方法的源码。
图14 - preVote 预投票
图15 - electSelf 投票
图16 - handlePreVoteRequest 处理预投票
图17 - handleRequestVoteRequest 处理投票