Raft协议安全性保证
分布式系统中主要的问题就是如何保持节点状态的一致性,不论发生任何failure,只要集群中大部分的节点可以正常工作,则这些节点具有相同的状态,保持一致,在client看来相当于一台机器。
一致性问题本质就是replicated state machines,即所有结点都从同一个state出发,都经过同样的一些操作序列(log),最后到达同样的state。其中保证各个节点执行相同的操作序列就是raft算法所要实现的。在raft算法中有一个Leader的角色,client与之进行交互,并且Leader协调Follower,保障所有的Follower具有相同的操作序列,最后提交这些操作,使状态机达成一个新的一致的stat。
整个raft算法分为Leader选举,日志分发,日志压缩(快照),集群成员变更。其中的Leader选举是算法的核心部分。算法保证任何时候最多只有一个Leader,但是可能没有Leader(比如正在选举过程中或者集群成员多数不可用时)。在Leader确立之后,就可以进行日志分发,算法保证日志会被安全的分发到集群中并且应用到状态机的日志和自己相同。快照是为了减少日志量,去除中间过程。集群成员变更是为了在不停服务的情况下安全使用新的集群配置。
Raft在非拜占庭错误情况下,包括网络延迟、分区、丢包、冗余和乱序等错误都可以保证正确,不会返回错误结果,这就是安全性保证。实际上就是保证所有成员状态机都以同样的顺序,执行同样的命令。下面分析几种典型的场景下,raft是如何保证安全性的。
1. Leader选举之后,如果Follower与Leader日志有冲突该如何处理?
Raft规定Follower中的冲突日志会被Leader中的日志覆盖,使得Follower中的日志总是与Leader的日志保持一致。Leader必须找到Follower日志中最后两者达成一致的地方,然后删除从那个点之后的所有日志条目,发送自己的日志给Follower。所有的这些操作都在进行日志复制RPC的一致性检查时完成: Leader针对每一个Follower维护了一个 nextIndex,表示下一个需要发送给Follower的日志条目的index。当一个Leader刚获得权力的时候,他初始化所有的 nextIndex 值为自己的最后一条日志的index加1。Leader每次发送日志复制RPC的时候会包含两个值:prevLogIndex和prevLogTerm,分别对应紧随新日志条目之前日志的索引值(index)和任期号(term),即prevLogIndex = newIndex - 1,如果Follower在它的日志中找不到包含Leader发送过来的index和term的条目,那么他就会拒绝接收新的日志条目。也就是此时Follower的日志和Leader不一致,日志复制RPC 时的一致性检查就会失败。在被Follower拒绝之后,Leader就会减小 nextIndex 值并进行重试。最终 nextIndex 会在某个位置使得Leader和Follower的日志达成一致。当这种情况发生,日志复制 RPC 就会成功,这时就会把Follower冲突的日志条目全部删除并且加上Leader的日志。一旦日志复制 RPC 成功,那么Follower的日志就会和Leader保持一致,并且在接下来的任期里一直继续保持。通过上述步骤,Raft算法保证了日志是顺序复制的。就是说,假如有一条旧的日志还未复制给FollowerA,那么更新的日志就不能复制。日志的顺序复制,很大程度上简化了Raft算法。比如查看各成员日志的新旧,只要比较最后一条日志即可。
2. 如果在一个Follower宕机的时候Leader提交了若干的日志条目,然后这个Follower上线后可能会被选举为Leader并且覆盖这些日志条目,如此就会产生不一致。
Raft通过对Leader的选举进行限制,来保证所新选出的任何Leader对于给定的任期号,都拥有了之前任期的所有被提交的日志条目,限制规则为:candidate发送出去的投票请求消息必须带上其最后一条日志条目的Index与Term;接收者需要判断该Index与Term至少与本地日志的最后一条日志条目一样新,否则不给投票。因为 前一个Leader提交日志条目的条件是日志复制给集群中的多数成员,Candidate选举为Leader的条件也是需要多数成员的投票。那么这两个大多数中成员必定有一个交叉,即有一个成员具有该日志,并且投票给了新Leader,也就意味着新Leader的日志至少不比该成员旧,那么新Leader也具有该日志。这样就得到证明了,后续的Leader一定具有前面Leader提交的日志。
3. 即使保证上述选举规则,也不能保证一致性,也就是说会出现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可以复制前面任期的日志,但是不会主动提交前面任期的日志。而是通过提交当前任期的日志,而间接地提交前面任期的日志。
4.配置变更的时候,需要保证任何时候只能有一个Leader,直接从旧的配置转化到新配置的方案是不安全的,如下图所示:
转换过程中,server1,server2通过旧配置选出一个Leader(三台中的两台支持),server3,server4,server5通过新配置选出一个Leader(五台中的三台支持),这样在同一个term中就有两台Leader,造成不一致,为了保证安全性,配置更改必须使用两阶段方法。在 Raft 中,集群先切换到一个过渡配置,我们称之为共同一致;一旦共同一致已经被提交了,那么系统就切换到新的配置上。
过渡配置保证不会在old与new两个配置上同时产生Leader :
过渡配置是指由 old配置和 new复合成的一个新配置(old+new)。
Leader会将该过渡配置日志先应用到本地,然后复制给集群中的所有成员。所有收到配置的成员,会直接将配置应用到本地。
处于过渡配置的成员,在Leader选举与提交日志时规则发生了变化,要求分别得到old与new两个配置下的多数成员同意才行。比如:
old:1、2、3
new:2、3、4、5、6 (删除机器1,增加机器4、5、6)
old+new:1、2、3、4、5、6(机器是old+new所有机器)
那么处于过渡配置的成员在Leader选举与提交日志时,需要得到 old(1、2、3)三个成员中的多数支持,以及new(2、3、4、5、6)五个成员中的多数支持(而不是old+new中六个成员的多数)。
那么上述过度配置如何保证不同时间段只产生一个Leader:
1)从old -> old+new配置提交之前
此时还未产生new配置,因此不可能在new配置下产生Leader。
那么是否可能在old与old+new下分别产生Leader哪? old下要产生Leader需要old中的多数投票,old+new下要产生Leader也需要old中的多数投票,而要在old与old+new下分别产生Leader,则old中至少有一个成员同时投票给old与old+new中的Leader。根据投票规则,在一个term下只能投票一次,因此就不可能在old与old+new配置下在同一个term上分别产生Leader。
而在不同term下产生则不违反规则,因为新的term下产生的Leader,发送心跳给旧Leader,旧Leader就会马上变成Follower。
2)old+new提交之后
只要old+new这个配置提交,则意味着该配置已经复制给了old中的多数成员,那么在old配置中就不可能再产生一个Leader出来了。只能在old+new与new配置下产生。而在old+new与new中也不可能在同一个term下分别选举出Leader,这个证明与old与old+new配置是一样的。
若每次增加或者删除一个成员不需要过渡配置:
就是说,每次配置变更只能增加或者删除一个成员。若满足则可以直接从old配置转换到new配置。
证明:旧配置有N个成员,新配置有N+1个成员,在切换过程中不会在旧配置与新配置下分别产生Leader。
1)若旧配置下选举出Leader,那么需要N/2+1个成员投票。
2)新配置下,从成员数是N+1去掉以及投票的N/2+1个成员只剩下N/2个成员。
3)而新配置下要选举出Leader需要(N+1)/2+1=N/2+3/2个成员投票,但是只剩下N/2个成员可以投票,因此新配置下不可能再选举出Leader。
4)反过来也一样。
参考: http://www.blogjava.net/jinfeng_wang/archive/2017/02/03/432287.html