Raft 一致性协议算法 《In search of an Understandable Consensus Algorithm (Extended Version)》

 
Raft是一种用于管理日志复制的一致性算法。它能和Paxos产生同样的结果,有着和Paxos同样的性能,但是结构却不同于Paxos;Raft比Paxos更易于理解,并且能够为实际的系统构建提供更好的基础。为了增强可理解性,Raft将一致性涉及的例如leader 选举, 日志复制及安全性等关键元素进行了分离,并且提供了更强的一致性以减少必须考虑的状态。基于实际的个人学习调查,Raft比Paxos更易于学生学习。Raft同时也包含了一种新的机制用于改变集群成员配置,并通过overlapping majority来保证安全性。

1.  Introduction

      一致性算法允许一群机器像一个整体一样工作,即使其中的一些成员发生故障也不会出现问题。正是基于这一点,它在构建可靠的大规模软件系统的过程中起着关键的作用。Paxos一直主导着过去十年对 一致性算法的讨论:许多一致性算法的实现都是以Paxos为基础或者受到它的影响,并且Paxos已经成为了用来教授学生关于一致性算法的主要工具。
不幸的是,Paxos太难以理解了,尽管已经做了很多尝试想使它变得更加平易近人。另外,为了支持实际的系统构建,它的结构也需要做出非常复杂的改变。因此,系统架构师和学生都对Paxos感到很痛苦。
在我们自己和Paxos经历了一番痛苦挣扎之后,我们决定发明一种新的一致性算法来为系统的构建和教学提供更好的基础。我们的主要目标有点特别,是为了让它更加易于理解:我们要为实际的系统构建定义一个比Paxos更加易于理解的一致性协议。此外,我们希望该算法能够培养系统构建者的开发直觉。而这对系统构建者是必不可少的。能让算法正常工作很重要,但是能让别人清除的知道它是怎样工作更加重要。
 
这项工作的结果就是一个叫做Raft的一致性算法。在设计Raft的时候,我们使用了一些额外的技术用于增加可理解性,包括分割(Raft分离了leader 选举, 日志复制及安全性)以及状态空间的减少(和Paxos相比,Raft降低了不确定性以及sever之间能达到一致的方法)。一个由来自两个大学的43位学生组成的用户调查显示Raft要比Paxos易于理解的多;在同时学习了两种方法之后,其中的33名学生回答Raft的问题要比回答Paxos更好。
Raft在很多方面和现存的一致性算法类似,但是它也有以下这些独特的特性:
  • Strong leader:Raft有着比其他一致性算法更强形式的leadership。例如,日志条目只能从leader流向其他server。这简化了对于日志复制的管理并且使Raft更加易于理解。
  • Leader election:Raft使用随机的时钟来选举leader。这只是在一致性算法原有的心跳检测的基础上增加了少量的特殊机制。使得解决冲突变得更加简答单快速。
  • Membership changes:Raft通过一种新的joint consensus的方法来实现server集合的改变,其中两个不同配置下的majority在过度阶段会发生重合。这能让集群在配置改变时也能继续正常运行。
我们相信不论对于教学还是作为系统实现的基础,Raft都要优于Paxos和其他的一致性算法。它比其他算法更简答也更加易于理解;它能完全满足实际系统的需求;它有很多开源的实现并且被很多公司使用;它的安全性已经被完全证实了;并且它的效率也完全可以和其他算法相媲美。
论文的第2章介绍了状态机的相关问题,第3章描述了Paxos的优缺点,第4章介绍了我们达成可理解性目标的一般方法,第5到8章详细介绍了 Raft一致性算法,第9章描述了对Raft的评估,第10章讨论了于Raft相关一些成果。

2. Replicated State Machine

一致性算法是在复制状态机的背景下提出来的。在这个方法中,一组服务器上的状态机对同一个状态计算产生多个完全相同的副本,这使得即使其中一些服务器崩溃了,这组服务器也还可以继续正常执行。复制状态机通常用于解决分布式系统中容错相关的一系列问题。例如,GFS,HDFS, RAMCloud,这些拥有单一集群领导者的大规模应用系统,会使用一个独立的复制状态机来管理领导选取及存储集群配置信息来应对领导者的崩溃。复制状态机典型的例子包括 Chubby 和 ZooKeeper。
如图-1所示,复制状态机功能时通过日志复制来实现。每台服务器存储一份包含一系列命令的日志,内部状态机依照日志中的命令顺序执行。因为每台机器的状态机都是确定的,所以计算将得到同样的状态和输出结果。
一致性算法的任务就是保证复制日志的一致性。服务器上的一致性模块,接收来自客户端的命令,并追加到日志中。它和其它服务器上的一致性模块进行通信,确保每一个服务器上的日志都包含相同顺序的相同请求。即使其中的一些服务宕机了。请求命令复制完成后,状态机会按照日志中的命令顺序进行执行。并将结果返回给客户端。由此,这些服务器就构成了表面统一的,高可靠性的复制状态机。
实际应用中的一致性算法通常具有以下特性:
  • 确保非拜占庭(Non-Byzantine)情况下的安全性(从来不会返回一个错误的结果),包括网络的延迟、分区及数据包的丢包、冗余和乱序情况。(Byzantine fail 分布式系统容错问题)
  • 高可用性,只要集群中的主体大多数机器能够运行,可以互相通信及和客户端通信,这个集群就可用。因此,一个拥有 5 台机器的集群最多可以容忍其中的 2 台的宕机(fail)。 Server发生故障时,可以认为是暂停了;它们可能稍后会恢复到存储在stable storage中的状态并且重新加入集群。
  • 不依赖 timing 保证一致性,时钟错误和极端情况下的消息延迟至多只会引起可用性问题。
  • 通常情况下,一条命令能够尽可能快的在大多数节点对一轮远程调用作出响应时完成,少部分慢的机器不会影响系统的整体性能。

3. Paxos 

近十年以来,Leslie Lamport 的 Paxos 算法几乎成为了一致性算法的代名词:它是授课中最常讲授的算法,同时也是许多一致性算法实现的起点。Paxos 首先定义了一个能够在单一决策基础上达成一致的协议,例如一个单一复制的日志条目(single replicated log entry)。我们把这个子集叫做单一决策 Paxos(single-decree Paxos)。 之后Paxos可以将该协议的多个实例组合在一起去形成一系列的decision作为log(multi-Paxos)。Paxos保证了safety和liveness,并且它支持cluster membership的改变。它的正确性已经被证明了并且在一般的情况下也被证明是高效的。
不幸的是,Paxos 有两个明显的缺点。第一个是 Paxos 太难以理解。它的完整说明更是出乎寻常的晦涩;很少有人能完全理解。 因此,已经做了很多尝试,试图用一个更简单的版本解释Paxos。虽然它们都着力于single-decree版本,但是仍然非常具有挑战性。在一项针对NSDI 2012与会者的调查中,我们发现很少有人对Paxos感到舒服,即使是那些经验丰富的研究人员。我们自己也对Paxos感到非常痛苦,我们在不能理解完整的协议,直到我们阅读了几个简化版的描述以及设计了我们自己的替代协议,而这整个过程持续了将近一年。
我们认为Paxos的晦涩来源于它将single-decree subset作为自己的基础。Single-decree Paxos被认为是微妙的:它被划分为两个不能用直觉来显示的阶段并且不能单独理解。因此,这就导致了很难对single-decree protocol是如何工作的建立起直觉。而multi-Paxos的composition rule则更加添加了复杂性。我们坚信对于在multiple decision的情况下到达consensus这个问题肯定能以其他更直接,更明显的方式被分解。
Paxos的第二个问题是它并没有为实际的实现提供一个很好的基础。一大原因是对于multi-Paxos没有一个广受认可的算法。Lamport的描述主要针对的是single-decree Paxos;它为multi-Paxos提供了一个大概的框架,但是很多细节并没有提及。对于充实以及优化Paxos已经做了很多努力,但是它们各自之间,以及和Lamport的概述都不相同。像Chubby这样的系统已经实现了类Paxos算法,但是它的很多细节并没有公开。
另外,Paxos的架构也不利于构建实际系统;这是它按single-decree分解的另一个后果。例如,独立地选取一系列的log entry并且将它们融合成一个顺序的log并没有太多好处,仅仅只是增加了复杂度。相反,构建一个围绕按顺序扩展log的系统是更简单和高效的。Paxos的另一个问题是它将对称的peer-to-peer作为核心(虽然在最后为了优化性能建议了一种弱形式的leadership)。这在只需要做一个decision的简单场景中是可行的,但是很少有实际的系统会使用这种方法。如果要有一系列的decision要决定,那么先选择一个leader,然后再让leader去协调decision。
因此,实际系统很少和Paxos类似。各种实现都以Paxos开始,然后发现实现起来很困难,于是最后开发出了一个完全不同的架构。这是极其费时并且容易出错的,而Paxos的难以理解则更加加剧了这个问题。Paxos的正确性理论很好证明,但是实际的实现和Paxos太过不同,因此这些证明就没什么价值了。接下来这段来自Chubby的评论是非常典型的:
Paxos算法的描述和现实世界的系统的需求之间有巨大的矛盾....而最终的系统都将建立在一个未经证明的协议之上
因为这些问题的存在,我们得出这样的结论,Paxos并没有为实际系统的构建或者是教学提供一个很好的基础。基于在大规模软件系统中consensus的重要性,我们决定尝试能否设计出另外一种比Paxos有着更好性质的consensus algorithm。而Raft就是我们实验得到的结果。
 

4. Designing for understandability

我们在设计Raft的时候有一下几个目标:它必须为系统构建提供完整,实际可行的基础,这将大大减少系统开发者的设计工作。在任何情况下都必须确保安全性,并且保证在典型应用情境下的可用性。在通常的应用操作中必须是高效的。另外,我们最重要的一点,也是最具挑战性的一点是它必须易于理解,使我们广大的读者能够很好的理解这个算法。 并且要能够建立对这个算法的直觉,从而让系统构建者能做一些实际实现中必要的扩展。
在设计Raft的很多节点上,我们要在很多可选方法之间做出选择。在这些情况下,我们基于可理解性对这些方法进行评估:对于每一个可选方案的描述是否困难(比如,它的状态空间的复杂度是多少,以及它是否有subtle implication?)以及读者是否能轻松地完全理解这种方法。
后来我们意识到这种分析方法具有很强的主观性;于是我们使用了两种方法让分析变得更具通用性。第一种是关于问题分解的众所周知的方法:是否有可能,我们可以将问题分解为可以被相对独立地解释,理解并且被解决的几部分。例如,在Raft中,我们分解了leader election, log replication, safety和membership changes这几部分。
我们的第二种方法是通过减少需要考虑的状态数,尽量让系统更一致以及尽可能地减少非确定性,来简化state space。另外,log不允许存在hole,Raft限制了log之间存在不一致的可能。虽然在大多数情况下,我们都要减少不确定性,但是在某些情况下,不确定性确实提高了可理解性。特别地,随机化的方法会引入不确定性,但是通过以相同的方式处理所有可能的选择(choose any; it doesn't matter),确实减少了state space。我们就使用了随机化来减少了Raft的leader election algorithm。

5. Raft 一致性算法

Raft是用于管理文章第二部分描述的复制日志算法。图2是对Raft的简要描述;图3罗列了算法的一些重要属性;接下来将会对图示部分进行分段讨论。
状态:
 
在所有服务器上持久存在的:(在响应RPCs之前进行更新)
 
currentTerm              服务器最后知道的任期号(服务启动时,初始化为0,单调递增) 
 
votedFor                   在当前任期内收到的选票的候选人 id(如果没有就为 null)
 
log[]                          日志条目;每个条目包含状态机的要执行命令及从leader处得到的任期号
 
在所有服务器上不稳定存在的:
 
commitIndex            已知的被提交的最大日志条目的索引值(初始化为0,单调递增)
 
lastApplied               被状态机执行的最大日志条目的索引值(初始化为0,单调递增)
 
在领导人服务器上不稳定存在的:(每次选举之后重新初始化)
 
nextIndex[]               对于每一个服务器,需要发给它的下一个日志条目的索引(初始化为领导人上一条日志的索引值+1)
 
matchIndex[]           对于每一个服务器,已复制到该服务器的日志条目的最高索引值(初始化为0,单调递增)
 
 
 
 
 
日志追加 AppendEntries RPC
 
由leader发起日志复制(5.3节);同时也用作心跳检测
参数
 
term                            领导人的任期号
 
leaderId                      leader id,为了其他服务器能够重定向客户端到leader
 
prevLogIndex              当前日志之前的日志的索引值
 
prevLogTerm              当前日志之前的日志的leader任期号
 
entries[]                      将要存储的日志条目(心跳检测RPCs时为空,一条或多条)
 
leaderCommit            领导人提交的日志条目索引值
 
返回值
 
term                            当前的任期号,用于领导人更新自己的任期号
 
success                       如果follower服务器包含能够匹配上 prevLogIndex 和 prevLogTerm 的日志时返回true
 
接收者实现:
  1. 自身携带的任期号小于当前任期号(term < currentTerm),则返回 false(5.1节)
  2. 没有任期号与prevLogTerm相匹配,索引为prevLogIndex的日志条目,则返回 false(5.3节)
  3. 如果已经存在的日志条目与新的日志条目冲突(索引:index相同但是任期号:term 不同),则删除此日志条目及它之后所有的日志条目(5.3节)
  4. 添加任何在已有的日志中不存在的新条目
  5. 如果 leaderCommit > commitIndex,将commitIndex设置为leaderCommit和最新日志条目索引号中较小的那一个。
 
投票请求 RequestVote RPC
 
由候选人发起,收集选票(5.2节)
 
参数
 
term                 candidate的任期号
 
candidateId     请求投票的candidate id
 
lastLogIndex    candidate最新日志条目的索引值
 
lastLogTerm    candidate最新日志条目对应的任期号
 
返回值
 
term                当前的任期号,用于candidate更新自己任期号
 
voteGranted    如果 candidate 收到选票则返回 true
 
 
 
 
 
 
 
 
接收者需要实现:
  1. 自身携带的任期号小于当前任期号(term < currentTerm),则返回 false(5.1节)
  2. 如果votedFor为空或者是candidateId,并且candidate的日志至少和自己的日志一样新,则给该candidate投票(5.2节 和 5.4节)
 
 
 
服务器规则:
所有服务器:
  • 如果commitIndex > lastApplied,lastApplied自增,将log[lastApplied]应用到状态机(5.3节)
  • 如果 RPC 的请求或者响应中的任期号 term T 大于 currentTerm,则将currentTerm赋值为 T,并切换状态为追随者(Follower)(5.1节)
追随者(followers): 5.2节
  • 响应来自候选人和领导人的 RPC
  • 选举超时后,未收到来自前领导人的AppendEntries RPC,或者投票给候选人,则自己转换状态为候选人。
候选人:5.2节
  • 转变为选举人之后开始选举:
  • currentTerm自增
  • 给自己投票
  • 重置选举计时器
  • 向其他服务器发送RequestVote RPC
  • 如果收到了来自大多数服务器的投票:则成为领导人
  • 如果收到了来自新领导人的AppendEntries RPC(heartbeat):转换状态为追随者
  • 如果选举超时:开始新一轮的选举
领导人:
  • 选举时:向其他服务器发送空的AppendEntries RPC(heartbeat);在空闲时间重复发送以防止选举超时(5.2节)
  • 如果收到来自客户端的请求:添加条目到本地日志,日志条目应用到状态机后响应客户端(5.3节)
  • 如果上一次收到的追随者的日志索引大于将要发送给追随者的的日志索引(nextIndex):则通过AppendEntries RPC将 nextIndex 之后的所有日志条目发送给追随者。
  • 发送成功:则将该追随者的 nextIndex和matchIndex更新
  • 如果由于日志不一致导致AppendEntries RPC失败:将nextIndex递减并且重新发送(5.3节)
  • 如果存在一个数N,满足N > commitIndex和matchIndex[i] >= N并且log[N].term == currentTerm的 N,则将commitIndex赋值为 N
 

 
选举安全性:一个任期只能有一个leader当选
 
leader 只追加:leader不覆盖或者删除自身的日志条目,只追加新条目
 
日志匹配:如果两个日志包含一个索引和任期都相同的条目,那么日志中此条目索引之后的所有条目都相同
 
leader 完整性:如果一个日志条目在一个任期内被提交,那么次条目将会呈现在之后任期的leader的日志中。
 
状态机安全性:当一个服务器在它自己的状态机上应用了一个指定索引的日志条目后,其它服务器状态机将不会出现应用同样索引的不同日志条目情况
 
Raft首先选举出一个唯一的leader,并赋予完全的管理日志复制的责任。leader接收来自客户端的日志条目并复制到其它服务器,同时将日志条目追加到自身日志,然后告知其它服务器(Append Entries RPCs with leaderCommit)可以应用日志条目到状态机。leader大大简化了日志复制的管理。例如,leader可以自主决定日志条目的追加位置,数据以一种简单的方式从leader流向其它服务器。leader可能宕机或者失联,此时需要进行leader选举。
对于leader选举,Raft将这一一致性问题分解为三个相对独立的子过程进程处理。
  • leader选举:leader宕机,则进行新的leader选举
  • leader必须能够接收客户端的日志条目,并复制到集群中的其它服务器,强制其它服务器的日志与自己的日志同步。
  • 安全性:状态机的安全性保障,当一个服务器在它自己的状态机上应用了一个指定索引的日志条目后,其它服务器状态机将不会出现应用同样索引的不同日志条目情况,5.4描述了如何保障这一特性,
 

5.1 Raft basics 

一个 Raft 集群包括若干个服务器;对于一个典型的 5 服务器集群来说,最多能够容忍 2 台服务器宕机。集群运行期间,服务器会处于特定的三个状态:leader 、follower、candidate。正常情况下,只有一个服务器处于leader状态,其它的都是follower。follower是被动的:他们不发送任何请求,只是响应来自leader和candidate的请求。leader来处理所有来自客户端的请(如果一个客户端与follower进行通信,follower会将请求信息转发给leader)。candidate是用来选取新的领导人的。图-4 阐述了这些状态及它们之间的转换。
Raft将时间划分为长度不同的任期(term),任期以连续的证书命名,每一个任期以leader选举开始,一个或多个candidate竞争成为leader,当一个candidate竞选成功,那么它就转换为leader,负责任期内的管理。一些特殊情况下,选举会进入裂闹状态而失败,那么这一任期就没有leader,新的任期会随即开始,Raft保证一个任期至多有一个领导者。
任期(term)作为Raft的逻辑时钟,是的服务器能够检测到过期信息,如过期的leader,每个服务器都存储着一个当前任期数字,数字随任期单调递增,服务器间通信时会相互交换任期信息。如果一个服务器的任期信息比其它的服务器小,那么就更新自己的任期到当前较大的任期。如果leader 或者 candidate发现自己的任期信息已经过时,那么它们会立即转换状态为follower。当一个服务器收到一个过期任期信息的请求时,会拒绝这个请求。
Raft服务器间通过RPC方式进行通信,基础的一致性协议只需要两种请求信息:Request Vote RPCs(选举使用),Append Entries RPCs(复制日志及心跳检测时使用)。如果服务器收不到回复,会重新尝试请求。服务器采用并行机制发送RPC请求。

5.2  Leader election

Raft使用心跳机制(heartbeat)来触发leader选取。服务器启动时,处于follower状态,并且保持这种状态直到接收到来自leader或者candidate的合法RPCs。leader定时发送心跳RPCs给所有的follower,以保持自身leader角色。如果一个follwer在一定的时间内未收到来自leader的心跳信息,则判定leader下线并开始新一轮的leader选举。
开始选举时,follower首先递增自身的任期并将状态切换为candidate。然后标识voteFor为自己,并发送Request Vote RPCs到集群中所有其它的服务器。candidate会一直保持自身状态,直到一下三种情况任何一种发生:赢得选举,成为leader;其它candidate赢得选举;选举超时,未能成功选出leader。以下,我们将详细就此予以讨论。
一个candidate获得集群中同一任期内大多数服务器的投票,则判定赢得选举。每一个服务器至多只能投一次票。按照先到先服务(first-come-first-served),“大多数”的原则能够保障一个任期内只会有至多一个candidate能够赢得选举。一旦一个candidate赢得选举,成为领导者,它会立即发送心跳消息到所有其它的服务器,告知自身leader状态,阻止新一轮的leader选举。
candidate选举期间,会不断收到其它候选人发送 Request Vote RPCs,如果接收到的请求中的任期号大于等于candidate的当前任期号,则candidate认可当前投票,并将自身转换为follower状态。如果接收到的请求中的任期号小于candidate当前的任期号,则candidate拒绝此次请求,并继续保持candidate状态。
第三种可能的结果,即选举失败:同一时间,过多follower成为candidate,启动选举时,投票被过分的分割,将没有candidate能够获得“大多数”投票。当这一情况发生,所有的选举都将进入选举超时状态,候选人又会重新发起新一轮选举。然而,如果不采取额外的措施,split votes将会无限的重复发生。
Raft使用随机的选举超时时间来确保split votes很少发生,或者即使发生了,也能很快解决。为了在一开始就避免split votes 发生,Raft将选举超时设定为150~300ms之间的一个随机值。这就使得服务器能够很好的分散开来,大多数情况下,同一时间,只会有一个服务器发生选举超时。当一个服务器赢得选举,它能够在其它服务器选举超时之前向他们发送心跳信息。每一个candidate在选举开始时,重置一个随机的选举超时时间,然后等待超时时间到来后,重新启动下一轮选举。这就大大减少了下一次选举时split votes现象的发生。
选举这一例子很好的展示了我们是如何根据可理解性做出设计选择的。期初。我们计划使用评级系统,每一个candidate赋予一个唯一的评级,以用于竞争选择。当一个candidate发现其它的candidate的评级比自身高,则将自己转换为follower状态,这样评级较高的candidate就能更容易的赢得选举。但是,我们发现这一方法,在可用性方面的一些微妙的问题(当高评级的candidate选举失败后,低评级的candidate需要等待选举超时到来后,才能开始下一轮的选举)。我们队算法做了很多次调整,但每一次的调整,都会伴随着新的问题出现。最终,我们得出结论,随机重试这种方法表现的更明确,更易于理解。

5.3  Log replication

 
一旦一个leader成功获得选举,它就开始接收处理客户端请求,每个客户端请求都包含一条需要状态机执行的命令。leader将命令最为新条目追加到自身日志中,同时发送Append Entries RPCs到其它服务器,进行日志条目复制。当所有的服务器复制完成日志条目后,leader自身状态机开始执行这一命令,并将结果返回给客户端。如果发生follower宕机或者运行缓慢,网络包丢失等情况,leader会无限次重试发送Append Entries RPCs,直到所有的follower成功复制所有的日志条目。
日志存储形式如上图6,每一个日志条目都存储着一条状态机命令和一个任期号,任期号主要用于发现日志条目的不一致及其它一些图3中说明的一些属性。每一个日志条目都有一个整形索引属性,标识当前条目在日志中的存储位置。
leader决定什么时候让状态机执行日志条目是安全的,而这一日志条目称之为commited。Raft保障所有commited都是持久的,并且最终都会被集群中所有的状态机所执行。当一个日志条目被集群的众大多数服务器成功复制后,它就会被leader commited,这一过程同时也会将此条目之前的所有日志条目一并commited,包括之前任期leader创建的条目。leader 会一直跟踪最新commited的日志条目索引,并将它包含在随后的Append Entries RPCs(包括心跳)中,以便其它服务器识别,并应用到自身状态机。
Raft的日志管理机制能够保障各个服务器间的日志的高度一致性。这不仅极大的简化了系统的行为,提升了可预测性,同时也是安全机制重要的组成部分。Raft确保日志的以下特性:如果两个日志中的日志条目的任期号和索引都相同,则他们存储的command也相同;如果两个日志中的日志条目的任期号和索引都相同,则之前的所有条目也都相同。这两个特性和图3中的日志属性,共同组成了 Log Matching 属性。
第一个特性说明,leader在一个日志索引位置至多只会创建一个日志条目,并且日志中的条目位置都是固定的。第二个特性是由Append Entries执行一个简单的一致性检查老保障的。在发送Append Entries RPCs时,leader会将要发送的最新条目之前的条目索引(preLogIndex)及任期号(preLogTerm)包含进去,如果follower在其日志中找不到匹配preLogIndex及preLogTerm的日志条目,则拒绝接受发送的新的日志条目。一致性检查执行符合递进归纳特性:初始的空日志满足Log Matching Property,随着每一次日志扩充,一致性检查都确保符合Log Matching Property。由此,leader能够通过Append Entries RPCs返回的成功结果,判定所有的follower的当前及后续日志都会和自己的日志保持一致。
正常情况下,leader和follower的日志能够保持一致。所以Append Entries的一致性检查不会返回失败。但是。当leader宕机时,就会引发日志的不一致(旧的leader可能会有一部分日志还没有成功复制到follower)。日志的不一致会随着一系列leader和follower的宕机变得更加严重。如图7所示:
follower可能和新的leader的日志不同,follower可能含有leader没有的日志条目,也可能缺少leader已有的日志条目,或者两种情况都有。日志条目的丢失和多余可能涉及多个日志条目。
Raft协议中,leader通过强制follower复制自己的日志来处理日志的不一致问题。follower中不一致的条目将会被leader中的条目覆盖。
为了使follower的日志和自己保持一致,leader首先需要找到和follower日志中能够保持一致的最新的日志条目索引,然后,删除follower中此索引之后的所有条目并发送leader中此条目之后的所有条目到follower。所有这些操作都是由AppendEntries的一致性检查引发执行。leader对每一个follower都维护着一个nextIndex变量,nextIndex代表下一个将要发送到follower的日志条目的索引。当一个leader最开始负责管理,leader会将所有follower的nextIndex初始化为最后一个日志条目的下一个索引。如果follower的日志不一致,AppendEntries的一致性检查就会在下一次的Append Entries RPCs中返回失败。一次失败后,leader机会将此follower的nextIndex减1,然后重新发送Append Entries RPCs,以此,循环往复,直到找到一个leader和follower的日志能通过AppendEntries的一致性检查的nextIndex值。此时AppendEntries就会删除follower中此索引之后所有的日志条目,并复制leader此索引之后所有的日志条目到follower,从而保障leader和follower的日志一致性。并且在接下来的任期内也一致保持。
如果愿意的话,协议还可以通过减少失败的Append Entries RPCs次数来进行优化。例如,当拒绝AppendEntries RPCs时,follower可以将冲突的条目的任期和此任期内存储的第一个条目返回给leader。这样leader就可将nextIndex直接减去所有冲突的条目最早的条目。一个任期内的日志条目冲突只需要一次AppendEntries RPCs就可以,而不需要像之前那样每个条目一次AppendEntries RPCs。但是在实际应用中,我们认为此优化时完全没必要的。因为AppendEntries RPCs请求失败并不是经常发生,并且好像也不会有很多冲突的日志条目。
通过这种机制,当一个leader开始负责管理时,不需要采用任何额外的措施来恢复日志的一致性。它只需要执行正常的操作,日志自会随着AppendEntries的一致性检查自动收敛。leader永远不会覆盖自身的日志条目。
这种日志复制机制展示了我们在第二部分描述的一致性属性:Raft只要在大多数服务器正常运行的情况下就能执行日志条目的接收,复制和应用。正常情况下一次RPCs就能完成一个日志条目的复制,单个follower的操作延迟不影响整体性能。

5.4  Safety

之前的章节讨论了Raft算法是如何进行领导选举和日志复制的。但是,到目前为止所描述的机制并不能很有效的保证每一个状态机以同样的顺序执行执行同样的命令。例如,一个follower离线期间,leader提交了一些日志条目。恢复正常后,被选举为leader,然后使用新的日志条目覆盖掉之前leader提交而未成功被复制的那些条目。这样,不同服务器的状态机就可能执行了不同的命令序列。
这一章节对于可能会被选为leader的服务器添加了一些限制。使得特定任期内的leader能够包含之前任期内提交的日志条目。通过增加这些选举限制,我们进一步细化了提交规则。最后,我们呈现了e Leader Completeness Property的证明草图并且展示了它是如何指导状态机正确的执行的。

5.4.1  Election restriction

在任何leader-based的一致性算法中,leader最终都必须保存着所有提交的日志条目。Raft使用一种简单的方法使得之前leader提交的日志条目能够在一选举出新leader时就能完整的呈现在的leader上,而不需要任何的传送。这就意味着,日志条目只会从leader流向follower,leader永远不会覆盖已有的日志条目。
Raft控制选举过程,只有当candidate的日志包含所有已提交的日志条目时,它才能够被选举为leader。参与选举期间,candidate需要与大多数服务器进行通信,同时,我们知道,集群大多数原则,每一个日志条目必须存在于大多数的服务器中至少一个服务器上。这样,当一个candidate满足自己的日志至少比大多数服务器中任何一个服务器的日志新时,它就存储了及群众所有已提交的日志条目。Request Vote RPCs实现了这种限制:请求中包含candidate的日志信息,如果投票服务器的日志条目比candidate的日志新,则会拒绝此次投票。
Raft通过比较两个服务器上日志的最后一个日志条目的任期和索引来决定谁的日志时最新的。任期不同,则任期大的日志新。任期相同,则索引大的日志新。

5.4.2  Committing entries from previous terms

leader知道任期内的日志条目一旦被大多数服务器复制存储,就被提交了。如果一个leader在提交一个日志条目前宕机了,将来的leader会继续尝试完成这一日志条目的复制,提交。然而,一个leader并不能立马识别一个被大多数服务器存储的日志条目,是否已被之前的leader提交了。上图8展示了一种情景,存在大多数服务器上的日志条目被新的leader覆盖了。
为了消除这种问题,Raft从来不会通过计算备份数来决定是否提交上一个任期的日志条目。只有leader当期的日志条目需要通过计算备份数来决定提交。一旦当前任期内的一个日志条目以这种方式被提交了,那么根据 Log Matching Property 限制,所有之前的所有日志条目也就间接的被提交了。当然也存在某些情景,leader能够立即识别是否一个旧的日志条目被提交了(日志条目被所有的服务器复制存储了),但是Raft为了简洁,选择了使用更加保守的方法。
Raft之所以会有这种问题是因为leader在复制之前leader日志条目时任然保留着原始的任期号。Raft的这种方式,使得其能够更好的对相关日志条目进行推断。另外,Raft复制的之前的日志条目也相对较少。

5.4.3  Safety argument

给出了完整的Raft算法后,我们可以进一步对 Leader Completeness Property进行论证。首先我们假设Leader Completeness Property不成立,然后退出矛盾的结论。假定任期T内的leader提交了一个日志条目,但是这个日志条目没有被之后任期的leader存储。考虑存在的没有存储这条日志条目的领导者的大于任期T的小任期U。
1、该committed entry在leaderU选举期间一定不存在于它的log中(leader从不删除或者覆写entry)。
2、leaderT将entry备份到了集群的majority中,并且leaderU获取了来自集群的majority的投票,如Figure 9所示。而voter是达到矛盾的关键。
3、voter一定在投票给leaderU之前已经接受了来自leaderT的committed entry;否则它将拒绝来自leaderT的AppendEntry request(因为它的current term高于T)。
4、当voter投票给leaderU的时候它依然保有该entry,因为每个intervening leader都包含该entry(根据假设),leader从不删除entry,而follower只删除它们和leader矛盾的entry。
5、voter投票给leaderU,因此leaderU的log一定和voter的log一样up-to-date。这就导致了两个矛盾中的其中一个矛盾。
6、首先,如果voter和leaderU共享同一个last log term,那么leaderu的log至少要和voter的log一样长,因此它的log包含了voter的log中的每一个entry。这是一个矛盾,因为voter包含了committed entry而leaderU假设是不包含的。
7、除非,leaderU的last log term必须比voter的大。进一步说,它必须大于T,因为voter的last log term至少是T(它包含了term T的committed entry)。之前创建leaderU的last log entry的leader必须在它的log中包含了committed entry(根据假设)。那么,根据Log Matching Property,leaderU的log必须包含committed entry,这也是一个矛盾。
8、这完成了矛盾。因此,所有term大于T的leader必须包含所有来自于T并且在term T提交的entry。
9、Log Matching Property确保了future leader也会包含那些间接committed的entry,例如Figure 8(d)中的index 2。
给定Leader Completeness Property,证明Figure 3中的State Machine Safety Property就比较容易,即让所有的state machine以相同的顺序执行同样的log entry。

5.5  Follower and candidate crashes

到目前为止,我们的关注点都在leader失败上。follower和candidate的失败相对来说,更容易处理,处理机制同leader相同。当follower和candidate失败时,所有发送到他们的Request Vote RPCs和AppendEntries RPCs都会失败。Raft通过无限次重试来处理这种状况。当失败服务器重新恢复时,RPC请求完成请求。当服务器接收处理完RPC请求,但是在回复之前宕机。那么在它恢复时,会接收到相同的RPC请求。因为Raft的RPCs是幂等的,所以这种情况并不会引发任何问题,例如,当一个follower接收到的AppendEntrie请求包含自身已存在的日志条目时,它会忽视这此请求。

5.6  Timing and availability

我们对Raft的要求之一就是安全性不依赖于时间因素:系统不会因为一些事件发生的比预期的快或慢而产生错误的结果。然而,可用性不可避免的要依赖于时间因素。例如, 因为server崩溃造成的信息交换的时间比通常情况下来得长,candidate就不能停留足够长的时间来赢得选举。如果没有一个稳定的leader,Raft就不能正常的执行。
leader选举是Raft中时间因素影响比较大的方面。当系统满足 broadcastTime ≪ electionTimeout ≪ MTBF 时,Raft才能够维持一个稳定的Leader。在上面的不等式中,broadcastTime代表一个服务器并行的向所有的其它服务器发送RPCs并收到回复的平均时间;electionTimeout代表选举超时时间;MTBF代表单个服务器的故障发生间隔。broadcastTime应该比electricTimeout小一个数量级,这样leader就能可能的发送心跳信息到follower以阻止新的领导选举。通过随机的 electionTimeout 使用,使得split votes更加不可能出现。 electionTimeout应该比MTBF小几个数量级,这样系统就能够正常运行。当leader宕机时,系统会在  electionTimeout 内变的不可用。我们希望这种情景出现的尽量少。
electionTimeout和MTBF是系统固有的时间属性,electionTimeout则需要我们自己进行设置,Raft的RPCs需要接收者执行相关的持久化操作,所以broadcastTime会根据存储技术的差异在0.5ms和20ms之间变动。这样electionTimeout的变动范围就可能在10ms到500ms之间。通常的MTBF在几个月,甚至更多,完全满足系统的时间因素要求。

6.  Cluster membership changes

到目前为止,我们都假定集群的配置(参与一致性算法的服务器)是固定的。但是,在实际应用中,配置时常也需要做相应的变动,例如,替换宕机的服务器,改变复制的等级。我们可以将系统下线,修改配置,然后重启系统,但是这不可避免的会引起系统下线期间的不可用。另外,人为操作因素也更容易引发系统错误。为了解决这些问题,我们决定实现配置变更的自动化,并将其融合进一致性算法中。
为了保障配置变更机制的安全,在配置转换期间,不能存在同一任期内选举出现两个leader的现象。不幸的是,没有任何方法能够使得集群能够安全的实现配置转换。自动的转换全部的服务器是不可能的,所有集群在转换期间极有可能出现裂脑现象。如图10:
为了确保安全,配置变更必须采用两阶段法。有很多种方法来实现这种算法。Raft中,集群首先切换到过渡配置状态,我们称之为 joint consensus ,一旦 joint consensus 被提交,系统切换到新的配置状态。联合一致性状态既包括旧的配置,也包括新的配置:
  • 日志条目在集群中被复制到两种配置下所有的服务器。
  • 新旧配置中的服务器都有可能选举成为leader
  • 关于选举和日志条目的提交的协定同时需要新旧配置中的大多数服务器原则要求。
joint consensus允许单个服务器在不影响安全性的基础上,在不同的特定时刻进行不同配置的转换。此外, joint consensus允许集群在配置转换期间继续处理客户端的请求。
集群配置是通过特殊的日志条目通过日志复制进行存储和传输通讯的,图11展示了配置的转换过程。当leader收到配置从 Cold 到 Cnew变更的的请求时,它首先将配置作为日志条目存储为 Cold,new 并复制到其它服务器,一旦某个服务器将收到的 Cold,new 配置日志条目添加到自身的日志,那么之后其所有的决策都将以此配置 Cold,new 为依据(服务器总是以日志中最新的配置为依据进行决策,无论配置条目是否已提交)。这就意味着,leader将使用 Cold,new 配置,来决定配置条目 Cold,new 什么时候提交。当leader宕机时,新的leader将在旧配置 Cold或者联合配置 Cold,new 的机器中选举出来。这取决于获得选举的candidate是否已经收到联合配置 Cold,new 。任何情况下,具有新配置 Cnew 的服务器在这段时间内都不能做出片面的决定。
一旦 Cold,new被提交后,具有Cold或者Cnew的服务器将不能再没有其它服务器允许的情况下做出任何决策, Leader Completeness Property确保了只有具有Cold,new的服务器才能当选为leader。此时,leader将能够安全的创建Cnew的配置条目并将其复制到集群其它服务器。同样,当复制的服务器收到配置条目后就开始使用它。当新的配置被提交后,拥有旧配置的服务器将可以被关闭。
在配置转换期间存在着三方面的问题,第一个就是新的服务器初始化启动的时候不包含任何日志条目,当他们加入集群中时,需要花费相当的时间同步到最新的状态,在此期间,它将不能提交任何日志条目。为了避免可用性断层,Raft设定新加入进群的服务器状态为none-voting(leader向他们复制日志,但是不讲他们纳入大多数范围)。当新的服务器同步到最新的状态后,就可以执行正常的配置转换过程了。
第二个问题是集群leader处在旧的配置中,这种情况下,leader在将Cnew类目提交后就降级转换为follower状态。这就意味着在leader提交配置类目的这段时间了,它在管理者一个不包括自己的集群。它复制日志条目,但是却将自身排除在被分计数外。leader的身份状态转换发生在Cnew条目提交的时候,这也是新的配置第一次能够独立决策执行的时刻。在此之前,只有处于Cold的服务器才可以被选举为leader。
第三个问题是,无关的服务器(处在Cold的服务器)会扰乱集群运行。因为这些服务器不会收到心跳请求,所以他们就会产生超时并启动新一轮的选举。他们发送的Request Vote RPCs包含了新的任期号,这就会导致当前的leader接收到请求后转换为follower状态,并最终在Cold下的某个服务器当选为新的leader。但是那些无关的的服务器会无限次的不断产生超时,启动选举,最终到会系统可用性的大大降低。
为了避免这样的问题发生,服务器设定当明确认定当前leader存在的情况下,会选择忽略此类的Request Vote RPCs。特别的,当服务器在当前最小选举超时时间内收到一个 RequestVote RPC,它不会更新当前的任期号或者投出选票。这不会影响正常的选举,每个服务器在开始一次选举之前,至少等待一个最小选举超时时间。然而,这有利于避免无关服务器的扰乱:如果领导人能够发送心跳给集群,那么它就不会被更大的任期号废除。

7.  Log compaction

Raft日志会伴随着系统的日常运行持续增长。但在实际应用中,我们不能让它无限制的增长下去。日志越长,占用的存储空间越多,也将耗费状态机更多时间去重新应用日志条目。我们需要适当的机制来处理掉日志中的过期的信息,避免其影响系统的可用性。
快照是压缩的最简单的方式,通过快照将某一时刻系统的当前状态写入快照文件,保存到磁盘,然后将这一时刻之前的所有日志丢弃。例如chubby,zookeeper的快照机制。
图12展示了快照的基本思想。各个服务器独立的对已提交的日志条目进行日志快照。主要的工作是由状态机将它当前的状态写入快照文件来完成。Raft也保留了一些元数据在快照中,例如,last included index代表状态机最后应用的日志条目索引。last included term则是指这一条目的任期。因为日志条目需要包含preLogIndex和preLogTerm这两个属性以应对AppendEntries的一致性检查。为了支持集群配置变更,快照文件也在last included index之前包含了最新的配置条目。一旦某个服务器完成快照写入,他就会将last include index之前的所有日志条目都删除掉。
虽然,正常情况下,各个服务器各自完成各自的快照。但是,偶尔也需要leader向落后的follower发送自身的快照。这一情况通常发生在leader丢弃掉了需要发送到follower的日志条目的时候。当然,这种情况很少发生。和leader保持同步的follower拥有leader的所有日志,但是,落后比较大的follower或者刚加入集群的服务器却并非如此。处理此类follower的机制就是leader发送日志快照来进行同步。
leader需要使用一种新的RPC请求:InstallSnapshot来向落后的follower发送快照。如图13所示,当follower接收到此类请求时,需要判断怎么对其进行处理。通常来说,快照包含最新的日志条目(包含接收者不存在的日志条目),这样接收服务器就可以丢弃自身所有的日志条目(可能包含未提交的和和快照中有冲突的条目),然后替换为快照中的日志条目。相反,如果接收者受到的快照包含的日志条目时其自身日志之前部分的条目(因为重传或者其它错误),那么就会将快照覆盖的自身日志条目删除掉,但是这之后的日志条目仍然有效,需要保留下来。
follower未经leader允许,接收快照,违背了Raft的强领导准则。然而,这是事出有因的,leader是为了处理达到一致性状态过程中的冲突的,但是,在进行快照的时候,就已经达成一致性的目的了。数据仍然是从leader流向follower,只是follower可以重新整理他们自己的数据。
让我们来思考另外一种leader-based的一致性算法。只有leader可以创建快照,然后发送到follower。这种方式有两个缺点,首先是发送快照造成的带宽浪费,及整个快照进程的拖慢。每个服务器都已经包含了创建子什么快照的数据,因此本地化的快照创建成本更低。其次是,leader的实现会变得更加复杂,例如,leader发送快照的时候,同时需要并行的发送新的日志条目,并且不能阻塞客户端请求。
另外两个影响快照性能的因素是什么时候创建快照及不能影响系统的正常运行。关于创建快照的时机,如果创建的太频繁就会造成磁盘,带宽和能量的浪费,并且也增大了重启状态恢复的时间耗费。因为我们需要设定一个合适的日志大小作为触发快照的阈值。对于第二个问题,因为创建快照会耗费一定的时间,为了避免影响正常的系统运行,我们可以采用copy-on-write机制,这样新的请求,日志条目的更新不会影响快照的创建。例如,基于功能性结构数据的状态机就天然的支持这种特性。或者我们可以使用操作系统的copy-on-write机制来创建状态机的in-memory快照。

8.  Client interaction

这一章介绍了客户端和Raft的交互,包括客户单如何识别集群leader,以及Raft是如何支持线性化。这些问题是所有一致性算法的共性问题,Raft的实现也大体相同。
集群leader负责处理客户端所有的请求。客户端启动时,它随机的连接集群中的一台服务器。如果连接的服务器不是leader,服务器会拒绝客户端的连接,并提供集群最新leader的信息(AppendEntries请求包含leader的网络地址信息)。如果leader宕机,客户端的请求就会超时,需要重新向集群尝试连接。
Raft的目标是实现线性化(每一次操作都是即刻执行的,并且只执行一次),然而,就像前面描述的,Raft也存在可能会多次执行同一个命令的情景:例如,leader 在提交完日志条目后及回复客户端之前宕机,客户端就会重新向新的leader发起同样的命令请求。这将会导致同一命令被再次执行。解决方案是,客户端给每一次的请求命令添加一个唯一的序列码,这样服务器状态机就可以根据请求的序列码追踪相应的回复。当服务器收到一个和之前序列码相同的命令请求时,服务器就可以不必重新执行命令,而获取响应返回给客户端。
只读操作可以直接处理而不需要写入日志,但是可能会返回过期数据,因为响应的leader可能已经被新的leader所替代。线性特性不允许返回过期数据,Raft在不记录日志的情况下需要两个额外的预防措施来避免这一情况的发生。第一,leader必须拥有最新的日志条目。 Leader Completeness Property能够保证leader拥有所有已提交的日志条目。但是在任期之初,leader并不知道那些条目时所有已提交的条目。为了解决这个问题,在任期开始的时候,leader需要提交一个空的 no-op条目。第二,leader在处理只读请求之前必须先检测一下自己是否已经被替代。Raft通过让leader在处理只读请求之前向集群大多数服务器发送心跳信息来完成检测。领导人可以依赖心跳机制来实现一种租约的机制,但是这种方法依赖时序来保证安全性

9.  Implementation and evaluation

... ...

10.  Related work

已经有许多关于一致性算法的文章被发表出来,他们答题可以归纳为以下几类:
  • 对Lamport Paxos 协议的原始描述及解析。
  • Paxos的改进算法,包括对 Paxos的查遗补缺及改进,以为实际应用提供更好的基础。
  • 实现一致性算法的系统,例如 Chubby,ZooKeeper 和 Spanner。Chubby 和 Spanner 的实现算法还没有详细的公开,但他们都声称是基于 Paxos。ZooKeeper 的算法细节已经公开,但是却和 Paxos 有着很大的差别。
  • Paxos 算法的性能优化。
  • Oki 和 Liskov 的 Viewstamped Replication(VR),一种和 Paxos 差不多的替代算法。最初的算法和分布式传输协议耦合在了一起,但是在最近的更新中,算法核心的一致性协议被分离了出来。VR 使用了一种leader-based的方法,和 Raft 有许多相似之处。
Raft 和 Paxos 最大的不同之处就在于 Raft 的强领导特性:Raft 使用leader选举作为一致性协议里必不可少的部分,并且将尽可能多的功能集中到了leader身上。这样就使得算法更加简单,易于理解。例如,在 Paxos 中,leader选举和基本的一致性协议是正交的:leader选举仅仅是作为性能优化的手段,而且不是一致性所必须的。然而,这样就给算法增加了多余的机制:Paxos 同时包含了针对基本一致性要求的两阶段提交协议和针对领导人选举的机制。相比较而言,Raft 就直接将leader选举纳入到一致性算法中,并作为一致性算法两阶段的第一步。这样就减少了不必要的算法机制。
像 Raft 一样,VR 和 ZooKeeper 也是leader-based的,因此他们也拥有一些 Raft 的优点。但是,Raft 比 VR 和 ZooKeeper 拥有更少的机制,因为 Raft 尽可能的减少了 non-leaders 的功能。例如,Raft 中日志条目只会从leader流向follower。在 VR 中,日志条目的流动是双向的(领导人可以在选举过程中接收日志);这就导致了额外的机制和复杂性。根据 ZooKeeper 公开的资料看,它的日志条目也是双向传输的,但是它的实现更像 Raft。
相比较于上述我们提及的其他服务于日志复制的一致性算法的算法,Raft 拥有更少的消息类型。例如,我们统计了一下VR 和 ZooKeeper 使用的用于基本一致性需要和成员变更的消息类型数(除了日志压缩及客户端交互,因为这些都是完全独立于算法的)。VR 和 ZooKeeper 都分别定义了 10 中不同的消息类型,相对的,Raft 只有 4 种消息类型(两种 RPC 请求和对应的响应)。Raft 的消息都稍微比其他算法的要信息量大,但是都很简单。另外,VR 和 ZooKeeper 都在领导人变更时传输了整个日志;所以为了能够实践中使用,额外的消息类型就很必要了。
Raft 的强领导人方法简化了整个算法,但是同时也妨碍了一些性能优化的方法。例如, Egalitarian  Paxos (EPaxos)在某些没有领导人的情况下可以达到很高的性能。EPaxos 充分发挥了在状态机指令中的交换性。任何服务器都可以在一轮通信下就提交指令,只要其他同时被提交的指令和它进行沟通。然而,如果并发被提交的指令,互相之间没有进行通信沟通,那么 EPaxos 就需要额外的一轮通信。因为任何服务器都可能提交指令,EPaxos 在服务器之间的负载均衡做的很好,并且很容易在 WAN 网络环境下获得很低的延迟。但同时,它也在 Paxos 上增加了许多重要的复杂度。
一些处理集群成员变换的方法已经被提出或者在其他的成果中被实现,包括 Lamport 最初的讨论,VR 和 SMART。我们选择使用joint consensus方法,是因为利用了一致性协议,这样我们就只需要增加很少一部分机制就可以实现成员变更。 Lamport 的基于 α 的方法对于Raft并不适用,因为它假定一致性可以不需要leader就可以达到。和 VR 和 SMART 相比较,Raft 的重配置算法可以在不影响正常请求处理的情况下进行;相比较而言,VR 需要停止所有的处理请求。SMART 则引入了一个和 α 类似的方法,限制了请求处理的数量。同时,Raft 的方法需要更少的额外机制来实现。

11.  Conclusion

... ...

12.  Acknowledgments

... ...
 
posted @ 2018-09-28 16:48  WindWant  阅读(2313)  评论(0编辑  收藏  举报
文章精选列表