原文地址->Raft算法
摘要
Raft是用于管理被复制的日志的共识算法。它与multi-Paxos算法产生的效果相同,并且和Paxos算法一样高效。但是结构与Paxos不同。这使得Raft算法比Paxos算法更容易理解。也为构建实际系统提供了更好的基础。为了加强理解,Raft将几个关键元素分离,比如leader选举,日志复制,安全性。并增强了一致性,以减少必须考虑的状态数。一项用户研究的结果表明,与Paxos相比,Raft算法更易于学生学习。Raft也提供了用于更新集群成员关系的新的机制。它使用重叠的多数来保证安全。
1 介绍
共识算法允许一组计算机的集合作为一个一致的的小组工作,这些小组可以承受某些成员的故障。正因为如此,共识机制在构建可信的大规模软件系统中起着至关重要的作用。在过去的十年中,Paxos一直主导着共识算法的讨论。很多共识算法都是基于Paxos或者受它的影响。Paxos称为了教受学生关于共识算法的主要工具。
不幸的是,尽管进行了许多尝试以使Paxos更加平易近人,Paxos仍然非常难以理解。此外,其体系结构需要复杂的更改以支持实际系统。结果,系统构建者和学生都与Paxos斗争。
在我们与Paxos斗争之后,我们着手寻找一种新的共识算法,该算法可以为系统构建和教育提供更好的基础。我们的方法与众不同,因为我们的主要目标是易于理解:我们能否为实际系统定义共识算法,并以比Paxos容易学习的方式对其进行描述?此外,我们希望该算法有助于系统开发人员必不可少的直觉的发展。重要的不仅是算法能起作用,而且要很清除它为什么起作用。
这项工作的结果是一个称为Raft的共识算法。在设计Raft时,我们应用了特定的技术来提高可理解性,包括分解(Raft分离了领导者选举,日志复制和安全性)以及状态空间减少(相对于Paxos,Raft减少了不确定性的程度以及服务器之间可能不一致的方式)。 一项对两所大学的43名学生进行的用户研究表明,Raft比Paxos容易理解得多:在学习了两种算法之后,其中33位学生比Rax更好地回答了有关Raft的问题。
Raft在许多方面与现有的共识算法相似(最著名的是Oki和Liskov的Viewstamped复制),但是它具有几个新颖的功能:
- 强壮的leader:与其他共识算法相比,Raft使用更强大的领导方式。例如,日志条目仅从leader者流向其他服务器。 这简化了复制日志的管理,并使Raft更易于理解。
- Leader选举:Raft使用随机计时器选举leader。这可以为任何共识算法已经要求的心跳添加少量机制,同时可以快速而轻松地解决冲突。
- 成员关系变化:Raft更改集群中服务器组的机制使用了一种新的联合共识方法,其中,两种不同配置的大多数在转换过程中会重叠。 这允许群集在配置更改期间继续正常运行。
我们认为Raft在教育目的和实施基础上均优于Paxos和其他共识算法。它比其他算法更简单,更易懂。本文对其进行了足够详尽的描述以满足实际系统的需求。它具有多种开源实现,并被多家公司使用;其安全性能已得到正式规定和证明;并且其效率可与其他算法相比。
本文的其余部分介绍了复制状态机问题(第2节),讨论了Paxos的优缺点(第3节),描述了我们对可理解性的一般方法(第4节),介绍了Raft共识算法(第5–8节),评估Raft(第9节),并讨论相关工作(第10节)。
2 复制状态机
共识算法通常出现在复制状态机的环境中。通过这种方法,服务器集合上的状态机可以计算相同状态的相同副本,即使某些服务器宕机也可以继续运行。 复制状态机用于解决分布式系统中的各种容错问题。例如,具有单个集群领导者的大型系统,例如GFS,HDFS和RAMCloud,通常使用单独的复制状态机来管理领导者选举并存储配置信息,这些信息必须在领导者崩溃中幸存。复制状态机的示例包括Chubby和ZooKeeper。
复制状态机通常使用复制日志来实现,如图1所示。每个服务器都存储一个包含一系列命令的日志,其状态机按顺序执行这些命令。每个日志以相同的顺序包含相同的命令,因此每个状态机处理相同的命令序列。由于状态机是确定性的,每个计算相同的状态和输出的顺序相同。
保持复制日志的一致性是共识算法的工作。服务器上的共识模块从客户端接收命令,并将其添加到其日志中。它与其他服务器上的共识模块进行通信,以确保即使某些服务器发生故障,每个日志最终仍会以相同顺序包含相同的请求。正确复制命令后,每台服务器的状态机都将以日志顺序对其进行处理,然后将输出返回给客户端。服务器似乎形成了单个高度可靠的状态机。
图1:复制状态机架构。 共识算法管理包含来自客户端的状态机命令的复制日志。 状态机处理来自日志的相同命令序列,因此它们产生相同的输出。
实际系统的共识算法通常具有以下属性:
- 它们可确保在所有非拜占庭条件下的安全性(绝不会返回错误的结果),包括网络延迟,分区,数据包丢失,复制和重新排序。
- 只要大多数服务器都可以运行并且可以相互通信并与客户端进行通信,它们就可以正常运行(可用)。因此,由五个服务器组成的典型集群可以容忍任何两个服务器的故障。假定服务器因停止而发生故障;它们稍后可能会从稳定存储上的状态中恢复并重新加入群集。
- 它们不依赖于时序来确保日志的一致性:错误的时钟和极端的消息延迟可能在最坏的情况下导致可用性问题。
- 在通常情况下,只要集群的大多数都响应了一次远程过程调用,命令就可以完成。少数速度较慢的服务器不必影响整体系统性能。
3 Paxos算法怎么了
...
4 可理解性的设计
...
5 Raft共识算法
Raft是一个如第二部分描述的对被复制的日志进行管理的算法。图2以简明形式总结了该算法以供参考,图3列出了该算法的关键属性。这些图的元素将在本节的其余部分中进行分段讨论。
Raft通过首先选举一位杰出的Leader,然后赋予Leader完全的责任来管理复制日志来实现共识。Leader接受来自客户端的日志条目,将其复制到其他服务器上,并告诉服务器何时可以安全地将日志条目应用于其状态机。拥有一个Leader可以简化复制日志的管理。例如,Leader可以决定在何处放置新条目而无需咨询其他服务器,并且数据以简单的方式从Leader流向其他服务器。Leader可能会失败或与其他服务器断开连接,在这种情况下,将选出新的Leader。
使用Leader方法,Raft将共识问题分解为三个相对独立的子问题,这些子问题将在以下小节中进行讨论:
- Leader选举:当存在的Leader失败后必须选出一个新的Leader(5.2部分)。
- 日志复制:Leader必须接受客户端发送的日志条目并通过集群复制他们,强制其他日志接受自己的(5.3部分)。
- 安全性:Raft的关键安全属性是图3中的状态机安全属性:如果任何服务器已将特定的日志条目应用于其状态机,则没有其他服务器可以在同一日志索引下应用不同的命令。5.4节介绍了Raft如何确保此属性;解决方案包括对第5.2节所述的选举机制的附加限制。
状态 | |
---|---|
所有服务器上的一致状态 | 在响应RPCs前稳定更新存储 |
currentTerm | 最新的服务器任期(第一次引导启动时初始化为0)单调递增 |
votedFor | 当前任期投票的candidateId(如果没有则为null),为谁投票则对应的值为谁 |
log[] | 日志条目集合,被Leader接收到的每一条日志包含状态机的命令和任期。(第一条索引为1) |
所有服务器上的隔离状态 | |
commitIndex | 已知的被提交的被最高的日志索引(初始为0,单调递增) |
lastApplied | 被应用到状态机的最高的日志条目索引(初始为0,单调递增) |
Leader上的隔离状态 | 在选举后重新初始化 |
nextIndex[] | Leader对于每一台服务器,将要发送的下一条日志条目(被Leader初始化为最后一条日志索引+1) |
matchIndex[] | Leader对于每一台服务器,已知的被复制到服务器上的最高的日志条目索引(初始为0,单调递增) |
追加日志条目RPC | 由Leader调用完成日志复制(5.3节),也可以用于心跳信息(5.2节) |
---|---|
参数: | |
term | Leader的任期 |
leaderId | follower可以重定向客户端 |
prevLogIndex | 紧接新记录之前的日志条目索引(即上一条日志条目索引) |
prevLogTerm | 上一条日志条目索引的任期 |
entries[] | 用于存储的日志实体(心跳信息为空,以至于更高效的发送) |
leaderCommit | Leader的提交的索引 |
结果: | |
term | 当前任期,用于Leader更新自己的任期 |
success | 如果follower包含的日志实体匹配到prevLogIndex和PrevLogTerm |
接收者实现: | |
1.如果任期小于当前任期回复false | 5.1节 |
2.如果包含的日志实体没有匹配到prevLogIndex和PrevLogTerm回复false | 5.3节 |
3.如果存在日志实体与新的日志实体冲突(相同的索引但任期不同),删除存在的日志实体并选择新的 | 5.3节 |
4.追加日志中尚未存在的任何新条目 | |
5.如果leaderCommit大于commintIndex,将commitIndex设置为(leaderCommit,最后一条新的日志实体索引)中最小的那个 |
请求投票RPC | 由candidates调用用于收集投票数(5.2节) |
---|---|
参数: | |
term | candidate的任期 |
candidateId | 请求投票的candidateID |
lastLogIndex | candidate的最后一条日志条目索引 (5.4节) |
lastLogTerm | candidate的最后一条日志条目的任期(5.4节) |
结果: | |
term | 当前任期,用以candidate更新自己的任期 |
voteGranted | 如果candidate接受了投票则为true |
接收者实现 | |
1.如果任期小于当前任期回复false | 5.1节 |
2.如果votedFor为空或者candidateId,且candidate的日志 | |
至少与接收者的日志一样新,同意投票 | 5.2节 5.4节 |
服务器的规则:
所有服务器:
- 如果commitIndex大于lastApplied;增加lastApplied,应用log[lastApplied]到状态机(5.3节)
- 如果RPC请求或响应中的任期T大于currentTerm;设置currentTerm为T,变为follower(5.1节)
所有Follower(5.2节)
- 响应来自candidates和Leader的RPC消息。
- 如果直到选举超时也没有接受到由当前Leader发送的追加日志条目RPC消息或者对candidate的投票,变为candidate
所有Candidate(5.2节)
- 当转换为Candidate后,启动选举过程:
- 增加currentTerm
- 为自己投票
- 重置选举计时器
- 发送请求投票RPC消息到其他所有服务器
- 如果接收到大多数成员的投票信息,变为Leader
- 如果接收到来自新的Leader的追加日志条目RPC消息,转换为follower
- 如果选举超时,启动新的选举过程
Leader
- 选举过后:将初始的空追加日志条目RP(心跳)消息发送到每个服务器;在空闲时间重复此操作以防止选举超时(5.2节)
- 如果接收到来自客户端的命令,追加日志到本地,在日志条目应用到状态机后回复客户端。
- 如果对于follower,lastLogIndex大于等于nextIndex,发送带有从nextIndex开始的日志条目的追加日志条目RPC消息。
- 如果响应成功,更新对于该follower的nextIndex和matchIndex
- 如果因为日志的不一致性导致追加日志实体消息失败,递减nextIndex并重试
- 如果存在N并且N大于commitIndex,并且大多数matchIndex[i]大于等于N,且log[N]的任期与currentTerm相等,将commitIndex设置为N(5.3 5.4节)
图2
选举安全
在给定的任期,最多只能选举一个Leader。5.2节
Leader只追加特性: Leader从不覆盖或删除它的日志条目,只追加新的。5.3节
日志匹配: 如果两个日志包含的实体具有相同的索引和任期,那么直到给定索引为止,所有条目中的日志都是相同的。
Leader完备性: 如果一个日志提示在给定的任期内被提交,那么该条目将出现在领导者的日志中,显示所有编号较高的条目。
状态机安全: 如果服务器应用一条给定索引的日志实体到它的状态机,那么没有其他服务器可以应用一条不同的日志到相同的索引位置。
图3:Raft保证任何时刻这里的每一条属性都是成立的。
5.1 Raft基础
一个Raft集群包含多个服务器;五是一个典型数字,它允许系统容忍两个服务器故障。在任何给定时间,每个服务器都处于以下三种状态之一:Leader,Follower或Candidate。在正常操作中,只有一个Leader,而其他所有服务器都是Follower。 Follower是被动的:他们自己不发出请求,而只是响应Leader和Candidate的请求。Leader处理所有客户请求(如果客户联系Follower,则Follower将其重定向到Leader)。第3种状态Candidate用于选举新的Leader。图4显示了状态及其转换。 过渡将在下面讨论。
Raft将时间划分为任意长度的项,如图5所示。项用连续的整数编号。每个任期都以选举开始,在选举中,一个或多个Candidate试图按照5.2节中的描述成为Leader。 如果Candidate在选举中获胜,则它将在剩余任期中担任Leader。在某些情况下,选举将导致投票分裂。在这种情况下,任期将以无Leader结束;新任期(以新的选举)将很快开始。Raft确保给定任期内最多有一位Leader。
不同的服务器可能会在不同时间观察任期之间的转换,并且在某些情况下,服务器可能不会观察到选举甚至整个任期。任期在Raft中充当逻辑时钟,它们使服务器能够检测过时的信息,例如陈旧的Leader。每个服务器存储一个当前的任期号,该任期号随时间单调增加。只要服务器进行通信,就会交换当前任期;如果一台服务器的当前任期小于另一台服务器,则它将其当前任期更新为较大的值。如果Candidate或Leader发现其任期已过时,它将立即恢复为Follower状态。如果服务器收到带有过期条款编号的请求,则服务器将拒绝该请求。
Raft式服务器使用远程过程调用(RPC)进行通信,并且基本共识算法仅需要两种类型的RPC。RequestVote RPC由Candidate在选举期间启动(第5.2节),而AppendEntries RPC由Leader启动以复制日志条目并提供心跳形式(第5.3节)的AppendEntries RPC消息。第7节添加了第三个RPC,用于在服务器之间传输快照。如果服务器未及时收到响应,则服务器会重试RPC,并且它们并行发出RPC消息以获得最佳性能。
图4:服务器状态。Follower仅响应来自其他服务器的请求。如果Follower未收到任何通讯,它将成为Candidate并发起选举。从整个集群的大多数中获得选票的Candidate将成为新的Leader。Leader通常会运作直到失败。
图5:时间分为几个任期,每个任期都以选举开始。选举成功后,由一位Leader管理集群,直到任期结束。在这种情况下可能选举失败导致任期届满而未选出Leader。任期之间的转换可以在不同的服务器上的不同时间观察到。
5.2 Leader选举
Raft使用心跳机制触发Leader选举。当服务器启动的角色为Follower。服务器保持Follower状态一直到接收到来自Leader或者Candidate的有效的RPC消息。Leader为了维护它的权利将发送周期性的心跳消息(不带有日志实体的AppendEntries RPCs消息)到所有的Follower。如果一个Follower在一整个周期时间内没有接收到任何通信消息则称为选举超时。他们将假设没有可以访问的Leader并开始新的投票选举新的Leader。
为了开始一次选举,Follower递增它的当前任期并将状态转换为Candidate。然后为自己投一票并并行发送RequestVote RPC消息到集群中其他的所有服务器。Candidate的状态将会一直保持一直到这三种情况中其中一个发生:
- 赢得选举
- 另外一个服务器称为了Leader
- 在当前投票周期内没有赢得选举的服务器。
将会在下面分别讨论这三种情况。
如果Candidate在同一任期内从整个集群中获得大多数服务器的票数,则将赢得选举。在给定的期限内,每台服务器将按先到先得的原则为最多一个Candidate投票(注:第5.4节增加了投票的其他限制)。多数规则确保最多只有一名Candidate可以赢得特定任期的选举(图3中的选举安全属性)。Candidate赢得选举后,便成为Leader。然后,它将心跳消息发送到所有其他服务器以建立其权限并阻止新的选举。
在等待投票时,Candidate可能会从声称是Leader的另一台服务器收到AppendEntries RPC消息。如果Leader的任期(在其RPC消息中可以获得)至少与Candidate当前任期一样大,则Candidate将Leader视为合法,并返回到Follower状态。 如果RPC中的任期小于Candidate当前的任期,则Candidate将拒绝RPC并继续处于Candidate状态。
第三种可能的结果是,Candidate既不会赢得选举也不会输掉选举:如果同时有许多Follower成为Candidate,那么票数可能会分散,从而任何Candidate都不会获得多数投票。当这种情况发生时,每个Candidate都将超时,并通过增加其任期并启动另一轮RequestVote RPC来开始新的选举。但是,如果不采取额外措施,分散投票可以无限期地重复。
Raft使用随机的选举超时来确保分散票很少发生,并且可以快速解决。为了避免投票分散,首先从固定间隔(例如150-300毫秒)中随机选择选举超时。这会分散服务器超时的时间,因此在大多数情况下,只有一台服务器会超时。它会赢得选举并在其他任何服务器超时之前发送心跳信号。使用相同的机制来处理分散投票。每位候选人在选举开始时都会重新启动其随机选举超时时间,并等待该超时时间过去后才开始下一次选举。这减少了在新选举中再次进行分散投票的可能性。第9.3节显示,这种方法可以迅速选举出一位Leader。
图6:日志由条目组成,这些条目按顺序编号。每个条目包含创建它的任期(每个框中的数字)和状态机的命令。如果可以安全地将条目应用于状态机,则认为该条目已提交。
选举是如何理解指导我们在设计备选方案之间进行选择的一个示例。最初,我们计划使用排名系统:为每个Candidate分配一个唯一的排名,该排名用于在竞争Candidate之间进行选择。如果某个Candidate发现了另一名更高级别的Candidate,它将返回到Follower状态,以便更高级别的Candidate可以更轻松地赢得下一次选举。我们发现,这种方法在可用性方面产生了一些细微的问题(排名较低的服务器可能需要超时,如果排名较高的服务器出现故障,则可能再次成为Candidate,但是如果这样做过早,则可以重置选举Leader的进度)。我们对算法进行了数次调整,但每次调整后都会出现新的极端情况。 最终,我们得出结论,随机重试方法更加明显和易于理解。
5.3 日志复制
选举Leader后,便开始为客户的请求提供服务。每个客户端请求都包含可以由复制状态机执行的命令。Leader将命令作为新条目添加到其日志中,然后与其他每个服务器并行发出AppendEntries RPC消息,以复制该条目。在安全地复制了条目之后(如下所述),Leader将该条目应用于其状态机,并将执行结果返回给客户端。如果Follower崩溃或运行缓慢,或者丢失了网络数据包,则领导者会无限次(即使在响应客户端之后)重试附加该RPC消息,直到所有Follower最终存储所有日志条目为止。
日志的组织结构如图6所示。当Leader收到条目时,每个日志条目都会存储一个状态机命令以及任期号。日志条目中的任期号用于检测日志之间的不一致并确保图3中的某些属性。每个日志条目还具有一个整数索引,用于标识其在日志中的位置。
Leader决定什么时候可以安全地对状态机进行日志记录。这样的条目称为已提交。Raft保证提交的条目是持久的,并且最终将由所有可用状态机执行。一旦创建条目的Leader已在大多数服务器上复制了该日志条目(例如,图6中的条目7),则提交该日志条目。这还将提交Leader日志中的所有先前条目,包括先前Leader创建的条目。第5.4节讨论了Leader变更后应用此规则时的一些细微之处,并且还表明了对提交的定义是安全的。Leader保持记录被提交的日志的最高索引,并将该索引包括在将来的AppendEntries RPC(包括心跳)中,以便其他服务器发现。跟随者得知日志条目已提交后,便将该条目应用于其本地状态机(按日志顺序)。
我们设计了Raft日志机制来维持不同服务器上的日志之间的高度一致性。这不仅简化了系统的行为并使其更具可预测性,而且是确保安全的重要组成部分。Raft维护以下属性,它们共同构成了图3中的Log Matching属性:
- 如果两个不同的日志实体具有相同的索引和任期号,那么他们存储有相同的命令。
- 如果两个不同的日志实体具有相同的索引和任期号,则所有先前条目中的日志都相同。
第一个属性来自以下事实:Leader在给定期限内最多创建一个具有给定日志索引的条目,并且日志条目从不更改其在日志中的位置。第二个属性由AppendEntries执行的简单一致性检查保证。在发送AppendEntries RPC时,Leader将在其日志中紧接新条目之前包含条目的索引和任期号。如果Follower在其日志中找不到具有相同索引和任期的条目,则它拒绝新条目。一致性检查是一个归纳步骤:日志的初始空状态满足Log Matching属性,并且只要扩展日志,一致性检查都会保留Log Matching属性。 因此,只要AppendEntries成功返回,Leader就会知道Follower的日志与它自己通过新条目记录的日志相同。
在正常操作期间,Leader和Follower的日志保持一致,因此AppendEntries一致性检查永远不会失败。但是,Leader崩溃可能会使日志不一致(旧的Leader可能没有完全复制其日志中的所有条目)。这些不一致会加剧一系列的Leader和Follower崩溃。图7说明了Follower的日志与新Follower的日志可能不同的方式。 Follower可能缺少Leader上存在的条目,它可能具有Leader上不存在的额外条目,或者两者都有。日志中的缺失条目和多余条目可能跨越多个任期。
图7:当最上面的为Leader时,Follower日志中可能会出现任何情况(a–f)。每个框代表一个日志条目;框中的数字是其用语。Follower可能缺少条目(a–b),可能有多余的未提交条目(c–d),或者两者都有(e–f)。 例如,如果该服务器是第2任期的Leader,则可能会发生场景(f)。它迅速重启,成为第三学期的Leader,并在其日志中添加了更多条目; 在提交任期2或任期3中的任何条目之前,服务器再次崩溃并保持关闭状态。
在Raft中,Leader通过强迫Follower的日志重复自己的日志来处理不一致之处。这意味着Follower日志中的冲突条目将被Leader日志中的条目覆盖。第5.4节将说明,当再加上一个限制条件时则是安全的。
为了使Follower的日志与自己的日志保持一致,Leader必须找到两个日志一致的最新日志条目的位置,在该位置之后删除Follower日志中的所有条目,并将该位置之后的Leader的所有条目发送给Follower。所有这些操作都是响应AppendEntries RPC执行的一致性检查而发生的。Leader为每个关注者维护一个nextIndex,这是Leader将发送给该Follower的下一个日志条目的索引。当成功选举为Leader时,它将所有nextIndex值初始化为刚好在其日志中的最后一个索引之后的索引(图7中的11)。如果Follower的日志与Leader的日志不一致,则下一个AppendEntries RPC中的AppendEntries一致性检查将失败。在拒绝之后,Leader递减nextIndex并重试AppendEntries RPC。最终nextIndex将到达Leader和Follower日志匹配的位置。发生这种情况时,AppendEntries将成功执行,这将删除Follower日志中的所有冲突条目,并添加Leader的日志中的条目(如果有)。一旦AppendEntries成功通过,Follower的日志便与Leader的日志保持一致,并且在本任期的其余时间中都将保持这种状态。
如果需要,可以优化协议以减少拒绝的AppendEntries RPC的数量。例如,当发送拒绝AppendEntries请求的消息时,Follower可以将冲突条目的任期以及该任期存储的第一个索引包括在内。有了这些信息,Leader可以递减nextIndex来绕过该任期中所有冲突的条目。每个具有冲突条目的任期都需要一个AppendEntries RPC消息,而不是每个条目一个RPC。 在实践中,我们怀疑这种优化是否必要,因为故障很少发生,并且不太可能出现许多不一致的情况。
通过这种机制,Leader在启动时无需采取任何特殊措施即可恢复日志的一致性。它只是开始正常运行,并且响应于AppendEntries一致性检查的失败,日志自动收敛。Leader永远不会覆盖或删除其自己的日志中的条目(图3中的Leader Append-Only属性)。
图8:一个时序显示Leader为何无法使用较早任期的日志条目来确定提交。 在(a)中,S1是Leader,并部分复制索引2处的日志条目。S5在S3,S4及其本身的投票下当选为任期3的Leader,并在日志索引2处接受不同的条目。S1重新启动,被选为Leader,然后继续复制。至此,任期2的日志条目已在大多数服务器上复制,但尚未提交。如果S1像(d)中那样崩溃,则S5可以被选为Leader(来自S2,S3和S4的投票),并用任期3中的条目覆盖该条目。但是,如果S1在崩溃之前在大多数服务器上其当前任期复制了一个条目,如(e)所示,该条目已提交(S5无法赢得选举)。此时,日志中的所有先前条目也将被提交。
这种日志复制机制展现了第2节中描述的理想的共识属性:只要大多数服务器都在运行,Raft可以接受,复制和应用新的日志条目。通常情况下,可以通过一轮RPC将新条目复制到大多数集群中。一个慢速Follower不会影响性能。
5.4 安全性
前面的章节描述了Raft如何选择Leader并复制日志条目。但是,到目前为止描述的机制还不足以确保每个状态机以相同的顺序执行完全相同的命令。例如,当Leader提交多个日志条目时,Follower可能不可用。然后可以当选为Leader并用新的日志条目覆盖这些条目。结果,不同的状态机可能执行不同的命令序列。
本节通过添加限制哪些服务器可以当选领导者来完善Raft算法。该限制可确保任何给定任期的Leader都包含先前任期中提交的所有条目(图3中的“领导者完整性”属性)。给定选举限制,我们便使承诺规则更加精确。最后,我们为Leader完整性属性提供了一个证明草图,并显示了它如何导致复制状态机的正确行为。
5.4.1 选举限制
在任何基于Leader的共识算法中,Leader最终必须存储所有提交的日志条目。在某些共识算法中,例如“加盖时间戳的复制” ,即使最初并不包含所有提交的条目,也可以选择一个Leader。这些算法包含其他机制,无论是在选举过程中还是选举后不久,可以识别丢失的条目并将其发送给新的Leader。不幸的是,这导致了相当大的额外机制和复杂性。Raft使用一种更简单的方法来保证自新任Leader选举之日都具有所有先前提交的条目,而无需将这些条目转移给Leader。这意味着日志条目仅在一个方向上(从Leader到Follower)流动,并且Leader永远不会覆盖其日志中的现有条目。
Raft使用投票程序来防止Candidate赢得选举,除非其日志中包含所有已提交的条目。Candidate必须联系集群的大多数才能被选举,这意味着每个提交的条目都必须存在于其中至少一台服务器中。如果Candidate的日志至少与该多数服务器日志中的日志一样最新(以下精确定义了“最新”),则它将保存所有已提交的条目。RequestVote RPC实施了此限制:RPC包含有关Candidate日志的信息,如果投票者自己的日志比Candidate的日志最新,则投票者将拒绝投票。
Raft通过比较日志中最后一个条目的索引和任期来确定两个日志中哪个是最新的。 如果日志中的最后一个条目具有不同的任期,则带有较新任期的日志是最新的。如果日志以相同的任期结尾,则以索引更大的日志为准。
5.4.2 提交之前任期的日志
如第5.3节所述,Leader知道,一旦该条目存储在大多数服务器上,就会提交当前项的输入。如果Leader在提交条目之前崩溃,将来的Leader将尝试完成复制条目。但是,Leader不能立即得出以下结论:一旦上一个任期的条目存储在大多数服务器上,就将其提交。 图-8说明了一种情况,其中旧的日志条目存储在大多数服务器上,但将来的Leader仍可以覆盖。
为了消除如图8所示的问题,Raft决不通过计算副本数来提交前项的日志条目。只有Leader当前任期的日志条目才通过计算副本数来提交;一旦以这种方式提交了当前任期的条目,则由于“日志匹配”属性而间接提交了所有先前的条目。在某些情况下,Leader可以安全地断定已提交了较旧的日志输入(例如,如果该条目存储在每个服务器上),但是Raft为简化起见采取了更为保守的方法。
图9:如果S1(任期T的Leader)从其任期中提交了新的日志条目,并且S5当选为下一任期U的Leader,那么必须至少有一个服务器(S3)接受了该日志条目并投票支持S5。
Raft会在承诺规则中带来额外的复杂性,因为当Leader从先前条款中复制条目时,日志条目将保留其原始任期号。在其他共识算法中,如果新的Leader从先前的“任期号”中复制条目,则必须使用其新的“任期号”来进行复制。Raft的方法使推理日志条目变得更加容易,因为它们在整个周期和跨日志期间保持相同的任期编号。此外,与其他算法相比,Raft中的新Leader发送的先前条目的日志条目要少(其他算法必须发送冗余日志条目以对其重新编号,然后才能提交)。
5.4.3 安全性讨论
给定完整的Raft算法,我们现在可以更精确地论证“领导者完整性属性”成立(此论点基于安全性证明;请参见9.2节)。 我们假设Leader完整性属性不成立,那么我们证明了一个矛盾。 假设任期T的Leader提交了其任期的日志条目,但该日志条目未由某个将来任期的Leader存储。考虑最小项任期U>T,其领导者leaderU 不存储该条目。
- 提交时,必须在LeaderU的日志中没有已提交的条目(Leader绝不会删除或覆盖条目)。
- LeaderT在大多数集群中复制了该条目,leaderU从大多数集群中获得了投票。因此,如图9所示,至少有一个服务器(“投票者”)既接受了LeaderT的条目又投票给LeaderU,投票者是达成矛盾的关键。
- 投票者必须在接受LeaderU投票之前已经接受了LeaderT提交的条目;否则,它将拒绝来自LeaderT的AppendEntries请求(其当前期限将高于T)。
- 当每个投票者对leaderU投票时,投票者仍然存储该条目,因为每个居间的Leader都包含该条目(通过假设),Leader从不删除条目,而Follower仅在与Leader冲突时才删除条目。
- 投票者将投票结果授予了LeaderU,因此LeaderU的日志必须与投票者的日志一样最新。这导致两个矛盾之一。
- 首先,如果投票者和LeaderU共享相同的最后一个日志条款,那么LeaderU的日志必须至少与投票者的日志一样长,因此其日志包含投票者日志中的每个条目。这是一个矛盾,因为投票者中包含已承诺的条目,而leaderU被假定为不包含。
- 否则,leaderU的最后一条日志的任期号必须大于投票者的最后一条日志的任期号。而且,它比T大,因为投票者的上一个日志任期号至少为T(它包含任期T中的所有已提交的条目)。创建leaderU的最后一个日志条目的较早的Leader必须在其日志中(通过假设)包含已提交的条目。然后,通过日志匹配属性,leaderU的日志还必须包含已提交的条目,这是矛盾的。
- 这样就完成了矛盾。因此,所有任期大于T的Leader都必须包含在任期T中提交的所有任期为T的日志。
- 日志匹配属性保证未来的Leader还将包含间接提交的条目,例如图8(d)中的索引2。
给定“Leader完整性”属性,我们可以从图3证明“状态机安全性”属性,该状态表明,如果服务器已将给定索引的日志条目应用于其状态机,则其他任何服务器都不会为该状态机在相同的索引应用不同的日志条目。服务器在将日志条目应用于其状态机时,其日志必须与通过该条目的Leader的日志相同,并且必须提交该条目。现在考虑任何服务器应用给定日志索引的最低任期;日志完整性属性可确保Leader将存储所有较高任期的相同的日志条目,因此,服务器将在最新的任期中应用的索引将应用相同的值。 因此,状态机安全属性成立。
最后,Raft要求服务器以日志索引顺序应用条目。结合状态机安全属性,这意味着所有服务器将以相同的顺序将完全相同的日志条目集应用于其状态机。
5.5 Follower与Candidate崩溃
到目前为止,我们只关注Leader的失败。Follower与Candidate崩溃比Leader崩溃要容易得多,并且两者的处理方式相同。 如果Follower与Candidate崩溃,则将来发送给它的RequestVote和AppendEntries RPC将失败。 Raft通过无限期重试来处理这些故障;如果崩溃的服务器重新启动,则RPC将成功完成。如果服务器在完成RPC之后但在响应之前崩溃,则在重新启动后它将再次收到相同的RPC。Raft中的RPC是幂等的,因此不会造成伤害。例如,如果Follower收到一个AppendEntries请求,其中包括其日志中已经存在的日志条目,则它会忽略新请求中的那些相同的条目。
5.6 时间和可用性
我们对Raft的要求之一是安全不得取决于时间安排:系统不得仅由于某些事件比预期的快或慢发生而产生不正确的结果。 但是,可用性(系统及时响应客户端的能力)必定取决于时间。 例如,如果消息交换花费的时间比服务器崩溃之间的典型时间长,则Candidate将不会停留足够长的时间来赢得选举。 没有稳定的Leader,Raft无法取得进步。
领导人选举是Raft最重要的方面,时机至关重要。只要系统满足以下时间要求,Raft将能够选举和维持稳定的Leader:
broadcastTime ≪ electionTimeout ≪ MTBF
在这种不平等的情况下,broadcastTime是服务器将RPC并行发送到集群中的每个服务器并接收其响应所花费的平均时间。 electionTimeout是第5.2节所述的选举超时;MTBF是单个服务器两次故障之间的平均时间。广播时间应比选举超时小一个数量级,以便Leader能够可靠地发送所需的心跳消息,以防止Follower开始选举。考虑到用于选举超时的随机方法,这种不平等也使得分散投票变得不太可能。选举超时应该比MTBF小几个数量级,以便系统稳步前进。当Leader崩溃时,该系统将在大约选举超时时间内不可用;我们希望这只代表总时间的一小部分。
图10:直接从一种配置切换到另一种配置是不安全的,因为不同的服务器将在不同的时间进行切换。在此示例中,群集从三台服务器增长到五台。不幸的是,在某个时间点上,可以为同一任期选举两名不同的Leader,其中一位拥有多数旧配置(C_old),另一位拥有多数新配置(C_new)。
广播时间和MTBF是基础系统的属性,而选举超时是我们必须选择的东西。Raft的RPC通常需要接收者将信息持久存储到稳定的存储中,因此根据存储技术的不同,转换时间可能从0.5毫秒到20毫秒不等。选举超时可能在10毫秒至500毫秒之间。 典型服务器的MTBF长达数月或更长时间,可以轻松满足计时要求。
6 集群成员关系变化
到现在为止,我们还假设集群配置(参与共识算法的服务器集合)是固定的。实际上,有时需要更改配置,例如在服务器出现故障时更换服务器或更改复制程度。尽管可以通过使整个群集脱机,更新配置文件,然后重新启动群集来完成此操作,但这将使群集在转换期间不可用。此外,如果有任何手动步骤,则可能会导致操作员出错。为了避免这些问题,我们决定自动进行配置更改,并将其合并到Raft共识算法中。
为了确保配置更改机制的安全,在过渡期间必须没有任何可能在同一任期内选举两名领导者的意义。不幸的是,任何将服务器直接从旧配置切换到新配置的方法都是不安全的。不可能一次自动切换所有服务器,因此群集在过渡期间可能会分成两个独立的多数(见图10)。
为了确保安全,更改配置必须使用两阶段方法。有两种方法可以实现两个阶段。例如,某些系统使用第一阶段来禁用旧配置,因此它无法处理客户端请求;然后第二阶段启用新配置。在Raft中,集群首先切换到过渡配置,我们称为联合共识;提交联合共识后,系统将过渡到新配置。联合共识将新旧配置结合在一起:
- 两种配置中的日志条目均复制到所有服务器。
- 来自任一配置的任何服务器都可以充当领导者。
- 协议(用于选举和入职承诺)需要分别来自新旧配置的大多数人。
联合共识允许单个服务器在不同时间在配置之间转换,而不会影响安全性。此外,联合共识允许群集在整个配置更改期间继续为客户请求提供服务。
群集配置使用复制日志中的特殊条目进行存储和通信。图11说明了配置更改过程。当Leader收到将配置从C_old更改为C_new的请求时,它将存储用于联合共识的配置(C_old,图中的new)作为日志条目,并使用前述机制复制该条目。给定服务器将新配置条目添加到其日志后,它将使用该配置进行所有将来的决策(服务器始终使用其日志中的最新配置,而不管该条目是否被提交)。这意味着Leader将使用C_old_new规则来确定何时提交C_old_new的日志条目。如果Leader崩溃,则可以根据获胜的Candidate是否收到了C_old_new来在C_old或C_old_new下选择新的Leader。无论如何,C_new不能在此期间做出单方面决定。
图11:配置更改的时间表。虚线表示已创建但尚未提交的配置条目,实线表示最新的提交的配置条目。Leader首先在其日志中创建C_old_new配置条目,并将其提交给C_old_new(大多数C_old和C_new)。 然后,它创建C_new条目并将其提交给大多数C_new。C_old和C_new都无法独立做出决策。
一旦提交了C_old_new,C_old和C_new都无法在未经另一方批准的情况下做出决策,并且Leader 完备性确保只有具有C_old_new日志条目的服务器才能被选为Leader。现在,Leader可以安全地创建描述C_new的日志条目并将其复制到集群。同样,该配置将在看到每台服务器后立即生效。在C_new的规则下提交新配置后,旧配置将不相关,并且可以关闭不在新配置中的服务器。如图11所示,C_old和C_new都没有时间可以单方面做出决定。这样可以保证安全。
还需要解决三个问题以进行重新配置。第一个问题是新服务器最初可能不会存储任何日志条目。如果以这种状态将它们添加到群集,则它们可能要花很长时间才能赶上,在此期间可能无法提交新的日志条目.为了避免可用性差距,Raft在配置更改之前引入了一个附加阶段,在该阶段中,新服务器以无表决权的成员的身份加入群集(领导者将日志条目复制到它们,但多数情况下不考虑它们)。一旦新服务器赶上了群集的其余部分,重新配置就可以如上所述进行。
第二个问题是集群Leader可能不属于新配置。在这种情况下,Leader一旦提交了C_new日志条目,便会下台(返回到Follower状态)。这意味着在一段时间(C_new提交)时,Leader将管理一个不包含自身的集群。它复制日志条目,但不占多数。提交C_new时会发生Leader转换,因为这是新配置可以独立运行的第一步(始终可以从C_new中选择一个领导者)。 在此之前,可能只有C_old中的服务器可以被选为Leader。
第三个问题是,删除的服务器(那些不在C_new中的服务器)会破坏群集。这些服务器不会接收心跳,因此它们将超时并开始新的选择。然后,他们将使用新的任期编号发送RequestVote RPC,这将导致当前Leader恢复为Follower状态。最终将选举新的Leader,但是被删除的服务器将再次超时,并且该过程将重复进行,从而导致可用性降低。
为防止此问题,服务器在认为当前的Leader存在时会忽略RequestVote RPC。具体来说,如果服务器在当前Leader的最小选举超时时间内收到RequestVote RPC,则该服务器不会更新其任期或授予其投票权。这不会影响正常的选举,在正常的选举中,每个服务器在开始选举之前至少等待最小选举超时。但是,它有助于避免因移动服务器而造成的中断:如果Leader能够对其集群发出心跳信号,那么它将不会被更高任期的成员所取代。
7 日志压缩
在正常运行期间,Raft的日志会增长,以合并更多的客户请求,但在实际系统中,它无法无限制地增长。随着日志的增长,它会占用更多空间,并需要更多时间来重放。 如果没有某种机制来丢弃日志中累积的过时信息,这最终将导致可用性问题。
快照是最简单的压缩方法。 在快照中,将整个当前系统状态写入稳定存储上的快照,然后丢弃该点之前的整个日志。 快照在Chubby和ZooKeeper中使用,本节的其余部分介绍在Raft中进行快照。
图12:服务器用新快照替换其日志(索引1至5)中已提交的条目,该快照仅存储当前状态(在此示例中为变量x和y)。快照的最后一个包含索引和任期的作用是将快照放置在条目6之前的日志中。
诸如日志清理和日志结构的合并树之类的递增压缩方法也是可能的。它们一次处理一部分数据,因此它们会随着时间的推移更均匀地分散压缩负载。他们首先选择一个累积了许多已删除和覆盖的对象的数据区域,然后再更紧凑地重写该区域中的活动对象并释放该区域。与快照相比,这需要大量的附加机制和复杂性,从而通过始终对整个数据集进行操作来简化问题。 尽管清除日志需要修改Raft,但是状态机可以使用与快照相同的接口来实现LSM树。
图12显示了Raft中快照的基本概念。每个服务器独立地拍摄快照,仅覆盖其日志中的已提交条目。状态机的大部分工作都由状态机组成,将其当前状态写入快照。Raft在快照中还包含少量元数据:最后包含的索引是快照替换的日志中最后一个条目的索引(状态机已应用的最后一个条目),最后包含的任期是该日志的任期。保留这些内容是为了支持快照之后的第一个日志条目的AppendEntries一致性检查,因为该条目需要先前的日志索引和任期。为了启用集群成员资格更改(第6节),快照还包括自上次包含索引起的日志中的最新配置。服务器完成快照的写入后,它可能会删除最后一个包含的索引中的所有日志条目以及所有先前的快照。
安装快照RPC | 由Leader调用并按顺序发送打包的快照到Follower。 |
---|---|
参数: | |
term | Leader的任期 |
leaderId | 用以Follower可以重定向客户端的请求到Leader |
lastIncludedIndex | 快照将替换直到该索引的所有日志条目 |
lastIncludedTerm | lastIncludedIndex的任期 |
offset | 快照文件的偏移量 |
data[] | 从offset开始,快照内的数据数组 |
done | 如果这是最后一个快照则为true |
结果: | |
term | 当前任期,用以Leader更新自己 |
接收者实现: | |
1.如果任期小于当前任期则立即回复 | |
2.如果第一次打包创建新的快照文件(offset为0) | |
3.从给予的offset写数据到快照中 | |
4.如果失败的话回复并等待更多打包的数据 | |
5.存储快照文件,抛弃所有存在的或并行的相同的索引快照文件。 | |
6.如果在快照文件中包含的实体最后存在日志实体具有相同的索引与任期,保持日志实体并回复 | |
7.抛弃整个日志 | |
8.重置状态机并使用快照内容(加载快照集群配置) |
尽管服务器通常通常独立拍摄快照,但Leader有时必须将快照发送给落后的Follower。当Leader已经丢弃了需要发送给Follower的下一个日志条目时,就会发生这种情况。幸运的是,在正常操作中这种情况不太可能发生:与Leader保持同步的Follower将已经有此条目。但是,异常慢的Follower或加入集群的新服务器(第6节)则不会。 使此类Follower保持最新状态的方法是,Leader可以通过网络向其发送快照。
Leader使用一个称为InstallSnapshot的新RPC将快照发送给落后的Follower。请参见图13。当Follower收到带有此RPC的快照时,它必须决定如何处理其现有的日志记录。通常,快照将包含收件人日志中尚未包含的新信息。在这种情况下,Follower将丢弃其整个日志;它全部被快照取代,并且可能具有与快照冲突的未提交条目。相反,如果Follower收到描述其日志前缀的快照(由于重新传输或错误操作),则快照所覆盖的日志条目将被删除,但快照之后的条目仍然有效并且必须保留。
这种快照方法背离了Raft强大的Leader原则,因为Follower可以在不了解Leader的情况下进行快照。但是,我们认为这种偏离是合理的。尽管拥有Leader可以避免在达成共识时发生决策冲突,但快照时已经达成共识,因此没有决策冲突。数据仍然仅从Leader流向Follower,只是Follower现在可以重组其数据。
我们考虑了另一种基于Leader的方法,其中只有Leader将创建快照,然后将快照发送给其每个Follower。但是,这有两个缺点。首先,将快照发送给每个Follower会浪费网络带宽并减慢快照过程。每个Follower已经具有生成自己的快照所需的信息,通常,服务器从其本地状态生成快照要比通过网络发送和接收快照便宜得多。其次,Leader的实施将更加复杂。例如,Leader将需要同时向跟随者发送快照,同时向其复制新的日志条目,以免阻塞新的客户要求。
还有另外两个问题会影响每个快照形式。首先,服务器必须决定何时进行快照。如果服务器过于频繁地进行快照,则会浪费磁盘带宽和能量。如果快照太不频繁,则有耗尽其存储容量的风险,并且会增加重新启动期间重放日志所需的时间。一种简单的策略是在日志达到固定大小(以字节为单位)时拍摄快照。如果此大小设置为明显大于快照的预期大小,则用于快照的磁盘带宽开销将很小。
第二个性能问题是写快照可能要花费大量时间,我们不希望这样做会延迟正常操作。解决方案是使用写时复制技术,以便可以接受新的更新而不会影响快照的写入。例如,使用功能性数据结构构建的状态机自然支持这一点。或者,可以使用操作系统的写时复制支持(例如,Linux上的fork)来创建整个状态机的内存快照(我们的实现使用这种方法)。
8 客户端内部交互
本节描述客户端如何与Raft交互,包括客户端如何找到集群领导者以及Raft如何支持线性化语义。这些问题适用于所有基于共识的系统,并且Raft的解决方案与其他系统类似。
Raft的客户将所有请求发送给Leader。客户端首次启动时,它会连接到随机选择的服务器。如果客户的首选不是Leader,则该服务器将拒绝客户的请求,并提供有关其最近听到的Leader的信息(AppendEntries请求包括Leader的网络地址)。如果Leader崩溃,客户请求将超时;客户端,然后使用随机选择的服务器重试。
我们对Raft的目标是实现线性化的语义(每个操作似乎在调用和响应之间的某个时刻立即执行一次,恰好一次)。但是,到目前为止,Raft可以多次执行命令:例如,如果Leader在提交日志条目之后但在响应客户端之前崩溃,则客户端将使用新的Leader重试该命令,从而导致该失败再次执行。解决方案是让客户端为每个命令分配唯一的序列号。然后,状态机将跟踪为每个客户端处理的最新序列号以及相关的响应。如果收到序列号已被执行的命令,它将立即响应而无需重新执行该请求。
可以执行只读操作,而无需在日志中写入任何内容。但是,如果没有其他措施,这将存在返回陈旧数据的风险,因为响应请求的Leader可能已经受到了一个不知道的新Leader的支持。可读的读取一定不能返回陈旧的数据,并且Raft需要采取两项额外的预防措施来保证不使用日志就可以做到这一点。首先,Leader必须掌握提交条目的最新信息。Leader完整性属性可以保证Leader具有所有承担的职责,但是在任期开始之初,它可能不知道是谁。为了找出答案,它需要从其任期中提交一个条目。Raft通过让每个Leader在任期开始时在日志中输入一个空白的禁止操作条目来解决此问题。其次,Leader必须在处理只读请求之前检查其是否已处置(如果选择了新的Leader,其信息可能会过时)。Raft通过让Leader在响应只读请求之前与大多数集群交换心跳消息来解决此问题。或者,Leader可以依靠心跳机制来提供某种形式的租约,但这将依赖于时序以确保安全性(它假设时钟范围有界)。