分布式系统中主要的问题就是如何保持节点状态的一致性,不论发生任何failure,只要集群中大部分的节点能够正常工做,则这些节点具备相同的状态,保持一致,在client看来至关于一台机器。html
一致性问题本质就是replicated state machines,即全部结点都从同一个state出发,都通过一样的一些操做序列(log),最后到达一样的state。其中保证各个节点执行相同的操做序列就是raft算法所要实现的。在raft算法中有一个Leader的角色,client与之进行交互,而且Leader协调Follower,保障全部的Follower具备相同的操做序列,最后提交这些操做,使状态机达成一个新的一致的stat。java
整个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提交的日志。spa
3. 即便保证上述选举规则,也不能保证一致性,也就是说会出现Leader提交了前面任期的日志条目以后,该条目还有可能被后来的Leader覆盖而产生不一致。以下图所示:.net
若是上述状况(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:一、二、3
new:二、三、四、五、6 (删除机器1,增长机器四、五、6)
old+new:一、二、三、四、五、6(机器是old+new全部机器)
那么处于过渡配置的成员在Leader选举与提交日志时,须要获得 old(一、二、3)三个成员中的多数支持,以及new(二、三、四、五、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