Raft算法选举流程及情景分析

 阅读约 9 分钟

Raft算法分为两个阶段

领导者选举

三个角色

Follower、Candidate、Leader(顺序有先后)
三个角色不能越级,即Leader只能退化为Follower、Follower只能升级为Candidate、Candidate可以退化为Follwer、也可以当选成为Leader。(怎么转换的?term、timeout会不会变?)

假设ABC三个节点组成的系统启动之初:
1.假设整个系统启动,也就是没有持久化之前的任何数据(持久化哪些数据),那么三个节点的角色都会被初始化为Follwer,随后开始计算超时时间(该时间为150-200ms随机值),当达到超时时间后,节点转变自己的角色为Candidate,发起RPC请求投票,先来看一下请求投票的参数:
term:发起者此时的term(term?在什么情况下会自增?)
candidateId:发起者的ID
lastLogIndex:当前节点的最新日志(logIndex)的上一条的Index(logIndex-1),理解为下标
lastLogTerm:上述日志对应的term

返回的结果:
term:接受者的term,这个term要注意,是处理完请求后的term,比如你的term比我大请求我投票,那么我返回的将是最新的term,这种情况我认为是用来处理,当一个候选者请求某节点投票,但这个节点已经投给了别人,更新了自己的term且比后来的这个候选者term更大
voteGranted:是否投票,为True证明投票给我

当发起投票后,会有三种可能:
1.候选者收到了超过半数者的同意,成功当选,那么候选者升级为leader,重置自己的超时时间。(有哪些重置自己超时时间的机会?)
2.候选者收到了别人的投票请求,且别人的term比自己的大,那么候选者退化为follower。
3.没有消息,比如网络失败,或者两个人同时发起请求,都没有达到半数,那么将会在达到超时后在此重试,rpc调用失败的情况会直接再次对该节点发起请求。

当候选者把请求发给其他人时,其他人做什么操作呢?
1.有两种情况,会直接拒绝投票:
发起者的Term还没有自己的大,说明发起者早就落后了,这种情况直接拒绝。
发起者和自己的Term相同,但是别人到的早,因为一个Term只能给一个人投票,那么我只有直接拒绝。
还有一种情况是,我发现这个term里我已经投给你了,那么也会返回true,因为可能是上一次的回复消息把消息丢掉了。
2.还有几种情况需要进一步判断
Term相同,且没有投票给别人 或者 别人的Term比自己大,不管我之前投没投过,都要状态转换为follower,且reset超时时间。
这两种情况要进一步判断,上一条log和term的消息。
先去拿到发起者那里上一条logindex和他的term,然后找到自己手里这一条term所对应的term。如果我的term比他的大,那么说明我这里有更新的人当选,如果term相等,就比较log的数量,谁的最新就以谁的为准,如果我的term小于他,说明在他那里有更新的leader产生。

理论依据:1.相同的term和相同的index一定存储一样的数据 2.相同term和相同index之前的数据一定也是相同的
raft算法可以保证在同一个term的数据,一定是只有一个leader给出来的,每一条数据只会创建一个logentry且位置不会再改变。
但是这个位置可能被覆盖,如果还没有提交的话,如果有更大term的leader产生的话,有可能会以此比较发现你这里的log无效,进而把这里给替换掉。
这也就是为什么要比较lastLogIndex和lastLogTerm的原因。

场景分析:
1.ABCDE,A在term1当选leader,虽后DE发生网络分区,但是DE之间无法选出来主,那么他们的term会比较高,那么重新连接网络后,会当选吗?
不会,因为他们的上一个log对应的term比ABC低。
2.前提相同,后来AB发生分区,CDE重新选主,A接受了很多log,然后A不断重启,自身term很大,此时term也大,log也多,能被选吗?不能,原因同上。他的所有log都是在term1接受到的,不可能在高term接到,因为他既然是高term,就不可能是主,非主不接log
3.
raft-do-not-commit-entries-from-previous-terms.png
ABCDE,A为主,接受了logIndex1,term1,并且同步给其他人,随后crash,但是A再次为主,此时term2,并且又接受了一个logindex2,但是同步给AB后又crash了。
后E为主,在term3当选,为什么是term3,因为BCD里给A投过第二次票,肯定知道目前最新是term2。接到了一个index2,但是没同步,就crash了。为什么能当选,首先term为3,lastLoginIndex是1,在其他人那里term比自己大,index1对应的term也是1,term相同,E的logIndex相等,所以CDE会投票,B不会投,因为log不如她。
然后A又当主了,然后A继续复制logindex2到其他人,此时ABC都复制到了,那么此时index2能标记提交了吗?
不能。因为如果A又crash了,那么E再次当选,为什么再次当选?
因为E的新term为4,且logIndex为2,首先term大,且上一条logIndex对应的term为为#,所以BCDE都会选。
这个时候,logindex2 会覆盖他们,如果之前的term2log被commit
,就会导致数据不一致。
怎么办,不主动去commit,如果crash,没提交就覆盖不会有问题。
如果crash了,但是A当选,term4接了更新的log,在这次同步的时候,会把上一次commit的提交过去,这个时候因为ABC的logIndex更新,所以E不会当选。

  1. 如果在一个Follower宕机的时候Leader提交了若干的日志条目,然后这个Follower上线后可能会被选举为Leader并且覆盖这些日志条目,如此就会产生不一致。

  Raft通过对Leader的选举进行限制,来保证所新选出的任何Leader对于给定的任期号,都拥有了之前任期的所有被提交的日志条目,限制规则为:candidate发送出去的投票请求消息必须带上其最后一条日志条目的Index与Term;接收者需要判断该Index与Term至少与本地日志的最后一条日志条目一样新,否则不给投票。因为 前一个Leader提交日志条目的条件是日志复制给集群中的多数成员,Candidate选举为Leader的条件也是需要多数成员的投票。那么这两个大多数中成员必定有一个交叉,即有一个成员具有该日志,并且投票给了新Leader,也就意味着新Leader的日志至少不比该成员旧,那么新Leader也具有该日志。这样就得到证明了,后续的Leader一定具有前面Leader提交的日志。

  1. 即使保证上述选举规则,也不能保证一致性,也就是说会出现Leader提交了前面任期的日志条目之后,该条目还有可能被后来的Leader覆盖而产生不一致。如下图所示:

(a) S1是Leader,并且部分地复制了index-2;
(b) S1宕机,S5得到S3、S4、S5的投票当选为新的Leader(S2不会选择S5,因为S2的日志较S5新),并且在index-2写入到一个新的条目,此时是term=3(注:之所以是term=3,是因为在term-2的选举中,S3、S4、S5至少有一个参与投票,也就是至少有一个知道term-2,虽然他们没有term-2的日志);
(c) S5宕机,S1恢复并被选举为Leader,并且开始继续复制日志(也就是将来自term-2的index-2复制给了S3),此时term-2,index-2已经复制给了多数的服务器,但是还没有提交;
(d) S1再次宕机,并且S5恢复又被选举为Leader(通过S2、S3、S4投票,因为S2、S3、S4的term=4<5,且日志条目(为term=2,index=2)并没有S5的日志条目新,所以可以选举成功),然后覆盖Follower中的index-2为来自term-3的index-2;(注:此时出现了,term-2中的index-2已经复制到三台服务器,还是被覆盖掉);
(e) 然而,如果S1在宕机之前已经将其当前任期(term-4)的条目都复制出去,然后该条目被提交(那么S5将不能赢得选举,因为S1、S2、S3的日志term=4比S5都新)。此时所有在前的条目都会被很好地提交。
  如果上述情况(c)中,term=2,index=2的日志条目被复制到大多数后,如果此时当选的S1提交了该日志条目,则后续产生的term=3,index=2会覆盖它,此时就可能会在同一个index位置先后提交一个不同的日志,这就违反了状态机安全性,产生不一致。也就是说当一个新Leader当选时,由于所有成员的日志进度不同,很可能需要继续复制前面term的日志条目,就算复制到多数服务器并且提交,还是可能被覆盖,因为前面term对应的日志条目较旧,容易使的没有这些条目的其他服务器当选Leader,此时就会覆盖这些日志条目。

  为了消除上述场景就规定Leader可以复制前面任期的日志,但是不会主动提交前面任期的日志。而是通过提交当前任期的日志,而间接地提交前面任期的日志。

阅读 162更新于 11月11日
推荐阅读
目录