Raft一致性算法

372 阅读23分钟

1. Introdution

论文: In Search of an Understandable Consensus Algorithm

翻译: Raft论文翻译

算法演示图: thesecretlivesofdata.com/raft

Raft算法是分布式存储比较常用的一致性算法之一,最开始Raft的提出是由于传统Paxos算法晦涩难懂,并且在实际使用时还有较大的修改,Raft算法则是将Paxos算法进行了拆分并减少了状态机的状态种类,这使得Raft更加利于开发者理解并实现。

本文主要还是按照论文顺序来拆分讲解下Raft算法。

2. Basic Conceptions

介绍Raft算法之前,先介绍一些Raft算法中的基础概念。

2.1 Replicated State Machines

复制状态机,是一致性算法的基础。基本上一致性算法的实现都是通过以下方式实现。

每个状态机都有着自己的日志条目(log entries),而这些日志条目按照顺序包含相同的机器指令(状态机可执行),状态机则按照日志条目的顺序执行相同的机器指令,达到状态一致的效果。日志条目只有被提交了,才会被状态机执行。

2.2 Server States

Raft中服务器状态仅有三种,FollowerCandidateLeader。

状态描述
Follower追随者状态,集群启动Server最初的状态,这种状态下的Server只会接收请求,一定时间内接收不到请求即超时(time out) ,就会转变成Candidate,参与Leader选举。
Candidate候选者状态,这种状态下的server会发请求给其他服务器以拉取选票,获得大部分服务器的选票, 转变成Leader, 未当选且有新Leader出现,状态转变成Follwer
Leader领导者状态,负责响应Client追加日志条目,分发日志条目给其他Server,当有新Leader出现,状态转变成Follwer

2.3 Term

Raft把时间切分成了任意大小的时间块,称为任期(term)。任期用连续的整数来表示,选举Leader即为一个新任期的开始。一个任期内最多只有一位Leader,如果选举中出现了选票分割,比如集群中所有Follower转成了Candidate并投票给自己,此时没有Server获得大部分选票,即没有Leader,重新开启新的任期,新的选举。每个Server有自己观测到的当前任期(Current Term),并且在Server与其他服务器交互时会判断自己的当前任期是否需要更新。

2.4 Log Entry Struct

日志结构是Raft算法的重要组成之一,它是由一个一个有序号的条目组成,而日志条目除了包含相应的状态机指令之外还有着本条目创建的任期,log index则标志着当前log entry所在的位置。

2.5 Server Inner State Params

服务器有着一些自身的内部状态参数。列举如下。

内部状态参数解释
持久性状态currentTerm服务器目前观测到的最新任期(初始化为0)
votedFor当前任期内投给的candidateId,可为空即没有投给任何Server
log[]日志条目列表,每个条目包含状态机指令以及创建时的任期(初始索引为1)
易失性状态commitIndex本服务器已提交的最高的日志条目索引(初始化为0)
lastApplied本服务器应用到状态机中最高日志条目的索引(初始化为0)
易失性状态领导人(服务器)nextIndex[]领导者准备发给每个追随者的下一个日志条目索引
matchIndex[]领导者已经复制到每一个追随者的最高日志条目索引

持久性状态是指在响应 RPC 请求之前,Server内这些状态参数已经更新到稳定存储设备了,可以在服务器重启或崩溃后恢复。

易失性状态则是是指Server内这些状态参数仅存储在内存中,在服务器重启或崩溃后需要等待同步。

2.6 Five properties

Raft有五大特性来保证一致性算法的正确性与安全性。

特性解释
选举安全特性(Election Safety)每个任期,最多有一个领导者
领导人仅追加特性(Leader Append-Only)领导者不会覆盖、删除已有日志条目,只会在原有日志条目后追加日志条目
日志匹配特性(Log Matching)如果两个Server某条日志条目的index和term都一致,那么它们之前所有的日志条目的index和term都是一致的
领导人完整特性(Leader Completeness)如果一个领导者在任期内已经提交了某条日志条目,那么后续的领导者的日志条目相同index下一定也是这条日志条目
状态机安全特性(State Machine Safety)如果某一台服务器已经将某index下的日志条目应用到状态机中,那么其他服务器的状态机在相同index下不会应用其他的日志条目

2.7 Three RPC Requests

2.7.1 AppendEntriesRPC

Leader向Follower发送追加日志的RPC请求

输入参数
参数描述作用
termLeader任期Follower会比对任期比自己当前任期低,则返回false比自己当前任期高,更新自己,则返回true
leaderIdLeader的IDClient请求打到FollowerFollower会重定向到Leader
prevLogIndexprevLogTerm新日志条目的前一个日志条目的Index新日志条目的前一个日志条目索引的任期二者一起保证日志匹配特性如果FollowerprevLogIndex位置处的日志条目的term不一致,返回false
entries需要Follower存储的日志条目Follower存储的日志条目
leaderCommitLeader目前已经提交的日志条目的Index更新Follower里可提交的日志条目index
返回参数
参数描述
term请求的Server当前任期
success日志追加成功/失败
请求效果

当Client请求Leader时,Leader会将指令存到自己的日志中,并开始向Follower发送AppendEntriesRPC请求,只有大多数Follower返回了成功,Leader才会认为追加日志成功,然后将日志条目提交到自己的状态机并更新commitIndex。

如果领导者发现自己的任期比Follower返回的任期小,那么领导者会立即转变成Follower,等待election timeout的时间以接收新的RPC请求。

2.7.2 HeartBeatRPC

心跳RPC就是带有空log entries的AppendEntriesRPC请求,是Leader周期性发送给Follwer来确保Leader可用性的请求。

2.7.3 RequstVoteRPC

Candidate 用来请求其他Server拉票的请求。

输入参数
参数描述作用
termCandidate任期Follower会比对任期比自己当前任期低,则返回false比自己当前任期高,更新自己,则返回true
candidateIdLeader的ID请求投票的Candidate
lastLogIndexlastLogTermCandidate最新一个日志条目的IndexCandidate最新一个日志条目的任期投票的依据条件之一
返回参数
参数描述
term请求的Server当前任期
voteGranted被投票与否
请求效果

当Follower等待接收RPC(AppendEntriesRPC、HearBeatRPC、RequestVoteRPC)超时后,Follower自己会变成Candidate,然后向其他Server发起投票请求。对于接收者,如果自己的votedFor为空或者为candidateId,并且Candidate最新的日志至少和自己一样,那么就投票。

如果Candidate接收到的返回term比自己的大,更新自我的currentTerm,然后立即转变成Follower。

2.7.4 Rules for all Server

还有一些针对所有服务器的规则,即服务器的一些状态变化所遵循的规则,如Follower转向Candidate后需要将自身currentItem自增1等等,在后续都会介绍。

3. Leader Election

领导者选举(Leader Election)是Raft算法中较为关键一环。最初Server都处于Follower状态,一个Server会一直保持Follower状态以接收Leader、Candidate发来的RPC请求以及Leader发送的周期性心跳RPC请求,直到等待超时,这个超时时间称为election timeout,而后Follower认为没有可用Leader,变成Candidate发起Leader Election,赢得大部分选票即⌈N/2⌉+1(N为集群中的服务器数量,[]表示取整) 即可赢得选举。

Follower 会自增currentTerm,投票给自己并转变状态至Candidate参与投票,最终会产生三种结果,其实是两种,只是针对某一Candidate本身来说是三种结局。

3.1 自己赢得选举

  Candidate赢得选举需要获得大部分投票者的选票(对应了五大特性中的Election Safety) ,并在此之后,Candidate转变成Leader,并向其他Server发起心跳RPC以阻止其他Server发起选举。

3.2 别的Candidate赢得选举

  同样,别的Candidate赢得选举变成Leader后也会发起心跳RPC,其他Candidate在等待选票的同时也会有可能收到“新”Leader的心跳RPC,收到心跳RPC之后,参选的Candidate会对比心跳RPC的任期与自身的任期,如果比自身要高就退化成了Follower,比自身低则不“承认”这个leader继续等待选票结果。

3.3 No winner

  当然还有第三种情况,就是没有Leader被选举出来,这种情况多半是因为较多的Follwer变成了Candidate,大家都投票给了自己,导致没有Candidate能够获取大部分选票即在超时时间内没有最终成为Leader,那么Candidate会继续增加当前任期,再次发起选举。

对于3.3这种情况,Raft采用的解决方式是每个Server的election timeout是随机的且不固定,一般从10ms~500ms之间随机选择,超时时间在这个时间段,大多数情况下只有一个服务器会超时。

4. Log Replication

日志复制是Raft算法的第二关键过程,在一个任期内,Leader会接收Client的请求,就会发送AppendEntriesRPC请求给Follower,只有当大部分Follower返回了接收成功,Leader才会将日志条目提交(之前领导者任期内的日志也会被提交)并应用到状态机,同时返回给Client 请求处理成功的消息。当然有部分Follower可能因为网络问题或者运行较慢导致没有及时响应Leader,那么Leader即使返回给Client 处理成功了,还是会一直不断尝试给Follwer发送AppendEntriesRPC请求,直至所有的Follower都成功的存储了日志条目。但是有趣的是对于Leader来说,Raft算法却规定其日志条目仅会在原来的基础上进行增加,而不会删除覆盖任何之前的日志,Raft称之为Leader Append-Only特性

正如2.7.1中的参数描述,Leader发送RPC请求时会带上自身的commitIndex用来表示Leader中现在已经提交的日志条目索引号,Follower接收到commitIndex之后知道Leader目前已经提交(即可以安全应用到状态机)的日志条目到哪儿了,这时Follwer就会更新自己提交日志条目索引,并将相应索引及之前的日志条目应用到状态机中。

Raft算法中用日志匹配特性(Log Matching Property)来保证不同服务器日志之间的一致性。

  • 在不同服务器的日志中,如果存在两个条目拥有相同的索引和任期号,那么这两个条目一定存储了相同的指令。
  • 在不同服务器的日志中,如果存在两个条目拥有相同的索引和任期号,那么他们之前的所有日志条目也都一一对应全部相同。

第一条很好理解,服务器的日志由Leader创建,每个任期仅有一个Leader,那么term相同说明是同一个Leader,而同一个Leader生成的日志条目索引也是唯一的,因此在第一条的条件下,这两个条目一定是由同一个Leader生成的同一个索引下的条目复制过来的。

而实现第二条则是Raft在AppendEntriesRPC请求中增加了一致性检查:请求的参数中带上了prevLogIndex、prevLogTerm(本次AppendEntriesRPC之前的最近的日志条目的index和term),如果Follower发现自己没有这个条目,那么Follower就会拒绝本次AppendEntriesRPC,其实也就是拒绝承认该Leader。当然如果返回成功,Leader也就知道Follower的日志条目会和自己的一样了。

但是当Leader出现崩溃时,不同Follower的本地logs就会有所不同。

如上图,当一个新leader诞生了,任期为8,那么在此之前出现follower可能是几种情况:

(a) (b)情况是follower运行比较慢/网络比较差,缺失了部分日志

(c) (d)情况是follower响应之前的Leader比较快,此时日志条目已经有了比新leader还要多,但还未提交

(e) (f)情况是follower很容易宕机,然后自己当了领导人,然后接收了很多日志之后,没来得及提交就又宕机了。

因此,针对这样的Follower,Raft算法中新Leader会强制Follower跟自己的日志保持一致,在AppendEntriesRPC请求里对每个Follower对应都有一个nextIndex参数,其初始值是Leader自己的日志数+1,当nextIndex下日志条目与Follower的不一致,Follower会拒绝此次请求,那么Leader就会将相应Follower对应的nextIndex--,然后再发送给Follower,直到二者的日志索引一致。

与建联Leader成功后,Follower从与Leader齐准的日志索引位置开始无脑删除不同日志并追加Leader同步过来的日志条目,之后,该Follower会在Leader的任期内与Leader保持日志条目的同步,从而保证了一致性。

5. Safety

前面讲述了 Raft 在Election Safety、Leader Append-Only、Log Matching三种特性下如何进行领导选举、日志复制,并且尽可能的保持了Server之间的一致性。但是还会有些异常case,是这三种特性无法解决的。

5.1 Leader Election Restriction

如果Follower在某一次Leader提交日志前就宕机了,并且后续由于election timeout超时变成Candidate参与选举,同时自己观测到的任期也更新到了最新的,那么他就有很大的可能称为Leader,此时如果由该Follower来当Leader者,那么就会出现其他服务器提交日志被覆盖的情况,这种情况下,状态机会执行不同的指令,即无法保证状态机的一致性了。这是不被允许的,对于所有的一致性算法,已经提交的日志都会通过各种机制被存储下来,Raft也不例外,只是采用了一种较为简单巧妙的方法来解决这个问题。

在领导者选举过程中,Raft认为:当Follower收到RequestVoteRPC请求时,如果candidateId的日志条目没有自己新(先比较任期号大小,大者新;任期号相同情况下比较索引号大小,同样大者新)时就不投票给该Candidate,同时当选的Leader不允许覆盖删除已经提交的日志。之前章节3中提到过,只有获得集群中大部分Server的投票才可以当选新Leader,那么意味着已经提交的日志集中在至少一个Server上,反过来就是说至少有一个Candidate已经获得了全部已经提交的日志。

这一限制同样保证了领导者完整(Leader Completeness)特性:如果一个领导者提交了一个日志条目,那么后续的领导者的日志相同索引号下一定也有这一日志条目。

5.2 Log Replication Rules

在日志复制过程中,也会有些问题,比如领导者可以将自己的日志条目(包含自己之前领导者的日志条目)复制给各个服务器,但是领导者无法知道这些之前任期里的日志是否已经被服务器提交,如上图。

(a) S1 是Leader,S2 这个跟随者复制了索引位置 2 的日志条目,S3、S4、S5只复制了索引位置 1 的日志条目。

(b) 此时 S1 崩溃了,S5 election timeout 并在任期 3 里通过 S3、S4 和自己的选票赢得了选举,变成了Leader,Client此时发出请求,S5 在索引 2 处有了任期为3的日志条目,与 S1 不同

(c) 此时 S5 也崩溃了并且 S1 重新启动,并通过 S1、S2、S3、S4 选举成功,开始复制日志给他们。此时来自任期 2 的那条日志复制到了集群中的大多数Server上(S1、S2、S3),但是还没有被提交。然后此时Client又发出请求,S1 在索引 3 处有了任期 4 的日志条目,但还没发起AppendExtraRPC。

出现了(d)情况:S1 又一次崩溃,S5 可以重新被选举成功(任期号更大,赢得来自 S2,S3 和 S4 的选票),然后覆盖了他们在索引 2 处的日志。

出现了(e)情况:S1 在崩溃之前,再一次把自己新任期里的日志条目复制到了大多数Server上,由于S1、S2、S3的最新日志都是任期 4 的日志条目,S5 是无法再被选举当作Leader了。这也从侧面印证了5.1中提到的新规则可以加强一致性。

针对这种情况,Leader不知道Follower是否已经提交了之前Leader任期内提交的日志,Leader不会通过拿到各个Follower成功响应的方式提交这些日志,而是会默认提交这些日志,只有Leader自己任期内的日志才会需要得到大多是Follower响应才能提交。这里也很好理解,因为前面Leader提交的日志是前任Leader接收Client请求并获得大多数Follower成功响应后应用到状态机内的,默认就应当被其他Server状态机直接应用,而Leader自己任期内的日志条目则是Client在Leader当前任期内发来请求记录下来的,按照Raft的规则进行追加日志、应用日志以及响应客户端。

5.3 Safety argument

基于5.1 5.2 我们甚至可以推理出领导者完整(Leader Completeness)特性。

如上图,S1 是T任期的Leader,S5 是U任期的Leader,T < U 即 S1 是 S5 之前任期的Leader。利用反证法论证,那么此时假设 S1 提交了一个日志条目X,这个日志条目不在 S5 的日志中, 并且 S5 是第一个不包含日志条目的领导人,分析如下:

  1. 由于领导者从不删除覆盖之前的日志,那么S5在参与领导选举时日志条目中没有X

  2. S1 如果能够提交日志条目X,那么说明此时集群中大部分Server已经接收了这条日志条目X,因为 S1 只有收到大部分Server返回AppendEntriesRPC成功响应才能提交日志。

  3. 大部分投票的Server此时已经存储了日志条目X,并且其中部分Server在之后 S5 参与领导选举时投票给了S5。(矛盾的关键)

  4. 如果Server要投票给 S5,那么意味着 S5 的日志至少要和 Server一样新。

  5. 日志一样新就要分两种情况

    1. 如果日志任期号相同,那么 S5 的日志索引号要比投票的Server大,那么意味着 至少比日志条目X的索引号大,那么 S5 的日志就必须包含了日志条目X,不成立
    2. 如果 S5 最新日志条目的任期号较大,那么意味着 S5 最新日志条目的任期号要比T大,至少一样大。那么在 S5 参与选举之前的领导者一定包含这条日志条目X,(根据假设 S5 是 第一个不包含日志条目X的领导人),根据日志匹配特性,S5 最新日志条目与之前领导者相同索引下的日志条目是一样,那么它们之前的日志条目一定也是一样的,那么也推理出 S5 的日志就必须包含了日志条目X,不成立。
  6. 因此,所有任期比 T任期 大的领导者一定包含了所有 T 已提交的日志条目。

  7. 如5.2中图 (e) 的索引 2一样,日志匹配原则保证了未来的领导者也同时会包含被间接提交的条目。

根据领导者完整特性,又可以推导出状态机安全(State Machine Safety)特性,即如果一个Server将某一索引下的日志应用到了状态机,那么其他Server在相同索引下的不会应用到一个不同的日志。因为根据日志匹配特性,能够被本Server提交的日志及该日志之前的日志都是与Leader日志保持一致的,并已经被Leader提交了的。而领导者完整特性又保证了后续任期内日志的一致性,从而保证了状态机安全的特性。

那么基于2.6中五个特性,Raft算法能够使得集群各个Server按照相同的日志索引顺序应用相同的日志条目到各自的状态机内,从而实现了一致性算法。

5.4 Follower and Candidate Crash

上面我们主要关注的是Leader异常会导致的问题,对于追随者和候选人,其实crash并不会太影响,等待恢复重启后转变成Follower,继续等待RPC请求,然后同步日志即可。

5.5 Timing and availability

从上文也可以看出,如果想要保证高可用性,对时间的要求必然是比较高的,比如Follower 的 election timeout过短,或者请求时间过长等导致频繁发起选举,没有稳定Leader,Raft无法正常工作。

Raft算法中对时间最为敏感的还是Leader Election时的时间间隔,Raft提出如果需要一个稳定长期的Leader,那么系统需要满足以下不等式:

broadcastTime << electionTimeout << MTBF(Mean Time Between Failures)

广播时间(broadcastTime)指的是一次请求从开始到收到响应的时间,选举超时时间(electionTimeout)指的是章节3里提到的等待RPC请求的超时时间,平均故障时间(MTBF)指的是一台服务器两次故障之间的平均间隔。

广播时间要远小于Follower的选举超时时间(electionTimeout),一般是0.5~20ms,这样领导者就能及时发送心跳以保证Follower状态稳定(不转变成Candidate),而选举超时时间的随机化也使得任期内无Leader变得不可能,一般会在10~500ms之间。另外,选举超时时间要远小于平均故障时间,这样Leader崩溃以后,能够及时选出新Leader,系统的不可用性也会降低,趋近于选举超时时间内的不可用。

6. Cluster membership changes

上述Raft算法都是在一个集群配置没有发生变化时做的讨论。但实际过程中,我们会涉及到集群配置的变化,比如宕机机器的下线,集群配置调整等等。

那么这里会涉及到一个过渡态的问题,集群在配置迁移过程中,依然需要响应Client的请求,那么当部分机器迁移到新配置,部分还在旧配置,那么此时就有两个集群的存在,然后就会有可能出现两个Leader,脑裂。

为了保证安全性,配置更改一般采用两阶段方法。Raft会将集群切到一个过渡的配置,称为联合共识(joint consensus)。一旦联合共识配置被提交了,系统切为新配置。新老配置的结合即为联合共识,此配置集群遵循以下规则:

  • 日志条目会复制给所有Server,无论新、老配置
  • 新、老配置下的Server都可以发起领导选举
  • 只有分别获得新配置、老配置下大多数Server的投票才能够当选Leader

Raft算法采用两阶段提交的方式是在保证集群切换配置的安全性的同时,不影响集群对Client的响应

第一阶段,当Leader接收到需要从旧配置 C-old 变更到新配置 C-new 的请求时,Leader会为了联合共识阶段将这个配置存储一个特殊的日志条目(称之为 C-old,new配置日志)并把它复制给所有的Server。一旦Server接收了该日志条目后,不管这个日志条目是否提交,Server都会使用新配置。同时后续Leader会使用 联合共识 **的配置来决定 C-old,new 配置 日志何时会被提交。此后所有日志都需要 C-oldC-new 两个多数派的确认。当 Leader 收到旧配置、新配置下大多数Server对 C-old,new配置 日志的成功响应,C-old,new配置 日志被提交了,那么由于领导人完全特性,后续只有拥有 C-old,new配置 日志条目的服务器才有可能被选举为领导人。

第二个阶段,Leader会创建一条关于 C-new 的日志条目并复制给集群。当这条 C-new 的日志条目被提交,后续日志确认只需要得到 C-new 的成功响应即可,不再需要 C-old 中机器的响应,并且未使用新配置的Server也会下线。如图 11,C-oldC-new 没有任何机会同时做出单方面的决定;这保证了安全性。

如果Leader在成员变更过程中崩溃宕机,那么 C-oldC-old,new 都有可能被选举成功成为新Leader。

  1. 如果 C-old 当选, 新Leader由于日志上没有 C-old,new 配置日志,则继续使用 C-old,其他Follower上即使有 C-old,new 日志也会被新Leader的日志覆盖,回退到 C-old,成员变更失败
  2. 如果 C-old,new 当选,则继续将未完成的成员变更流程走完。

7. Log Compaction

日志压缩主要通过快照的方式实现的,快照相对简单粗暴且有效将系统状态写入到持久化设备的方式。Raft采用的日志压缩方案就是利用快照保存的是当前状态机的状态。

下面就是Raft快照方式,将已提交的日志对应状态机内容、所“照下来”的最后一个日志条目的索引与任期快照下来。

Leader偶尔会因为有些Server过于落后或者刚加入集群,其日志与Leader差别太大,那么此时通过快照的方式可以让这些Server能够更快的跟上Leader的日志。

8. Conclusion

Raft一致性算法属于多数派算法,而多数派算法保证了一点:至少有一个节点保留了上文阶段的信息,进入到下文阶段,这也是Raft诸多特性的基础。而Raft最核心特性就是安全性以及活性,安全性保障了上下文信息的完整性以及状态机的一致性;同时虽然整篇论文没有提到活性(Active),但它保障了系统自驱向前演进,只需要大多数节点认可就可以继续演进。