前言
题目:《In Search of an Understandable Consensus Algorithm (Extended Version)》
作者: Diego Ongaro and John Ousterhout (Stanford University)
0. 摘要
raft 是一个用来管理多副本的共识算法, 他的功能等价于多阶Paxos,且和paxos效率不相上下,不过结构不同于Paxos;相比于Paxos,raft更容易理解,为构建可实践系统提供了更好的基础。为了增强可理解性,raft拆分了共识的关键要素,如主节点选举、日志复制和安全,除此增强了连贯性(coherency)以减少必须考虑的状态数。从使用学习结果来看,相比于Paxos,raft更有容易学习对于学生来说。Raft还包含了改变集群成员关系的方案,采用了大多数重叠以保证安全性。
1. 概述
共识算法允许一组机器存在一些失败成员仍能正常运行还能保证一致性。因为这个特性,他们在建立可靠的大规模软件系统扮演了重要的角色。Paxos[15,16]在过去十年里在共识算法了成为标杆,大多数共识算法的实践都是基于Paxos或者受它影响,因而Paxos逐渐成为教授学生共识算法的基本理论。
不幸的是,Paxos非常难以理解,尽管有很多让他更容易理解的尝试。除此之外,它的结构使得用于支撑实用系统时比较困难。因此,系统构建者和学生都受到Paxos的“残害”。
在我们饱受Paxos困扰时,我们尝试找出新的共识算法能够提供更好的基础用于系统构建和教育。我们的方法比较特别,因为我们最主要的目标是可理解性:我们能够定义一个能够支撑实践系统且比Paxos更容易理解学习的共识算法吗?因此,我们希望我们的算法让开发更加直观,这对系统构建者很重要。重要的不仅是为了算法可以运行,而且让算法能够工作的原因要很清晰。
这项工作的成果是个被称为Raft的共识算法。在设计Raft时,我们采用特别的方法来提高可理解性,包括解耦(主节点选举、日志复制和安全)和削减状态空间(相比于Paxos,Raft减少了不确定性,且允许服务器相互不一致)。在两所大学中经过43名学生的学习,表明Raft比Paxos更容易理解:经过学习两种算法后,他们中的33名学生在回答问题时,对于Raft的问题回答的更好相比于Paxos。
Raft与现存的一致性算法在很多地方较为相似,但是Raft有几个新颖的特性:
- 强leader:与其他共识算法相比,raft中leader的权力更大。比如,日志条目只会从leader流向其他节点。这简化了日志复制的管理使得raft更容易理解;
- Leader选举:Raft采用随机计时器来选举主节点。这个特性只在任何共识算法都需要的心跳机制上增加一些机制,但是能够简单快捷地解决冲突;
- 成员关系改变: Raft用来变更集群中服务器集合的机制使用了一个新的*联合共识(joint consensus)*方法,其两个不同配置中的大多数服务器会在切换间有重叠。这让集群能够在配置变更时正常地继续操作。
我们坚信Raft无论是在作为教育作用还是实践的基础,相比于paxos以及其他共识算法,更具有优越性。它比其他算法简单且更容易理解;它被描述地足够完整,足以满足实践系统的需求;它已经有几个开源的实现版本且被公司使用;它的安全特性已经被形式化定义并证明;Raft的效率与其它算法相似。
本文的剩余部分介绍了多副本状态机问题(第二章),讨论了Paxos的优势与劣势(第三章),描述了我们为了可理解性使用的通用方法(第四章),给出了Raft共识算法(第5~8章),评估了Raft(第九章),并讨论了相关工作(第十章)。
2. 多副本状态机
共识算法通常在多副本状态机问题[37]的上下文忠出现。通过这种方法,在一系列服务器上的状态机会计算相同状态的相同副本,且即使在一些服务器宕机是也可以继续操作。多副本状态机通常被用来解决各种各样的问题在分布式系统中。例如,有单集群leader的大型系统(如GFS[8]、HDFS[38]、和 AMCloud[33])通常使用独立的多副本状态机来管理领导选举并存储必须能在leader崩溃时幸存的配置信息。多副本状态机的例子还包括Chubby[2]和ZooKeeper[11]。
多副本状态机通常是采用多副本日志实现,如图一所示。每台服务器存储一份日志包含一系列命令,它的状态机会有序地执行。每份日志在相同的位置包含相同的指令,因此每个状态机会处理相同的指令序列。因为状态机是确定的,每一台状态机都会计算出相同的状态并得出相同的输出序列。
保证副本日志一致是共识算法的基本工作。服务上的共识模块从客户端接收命令,并添加到日志。它与其它服务器上的共识模块通信来确保每个日志最终包含相同顺序的相同请求,即使一些服务器故障也是如此。一旦指令被恰当地多副本化,每个服务器的状态机就可以按日志顺序处理它们,并将输出返回给客户端。这样,所有服务器对外会表现为单个高可靠性的状态机。
为可实践系统通常有如下特性:
1. 他们确保所有非拜占庭下条件下的安全,包括网络延迟,丢包,重复和乱序;
2. 只要集群的大多数服务可以互相通信那么其所有功能都可用。因此,通常使用的5节点集群可以容忍两个节点故障。假设服务宕机停止,他们可能在随后的从稳定存储中恢复并重新加入集群;
3. 他们不依赖时间来保证日志的一致性:在极端情况下,始终故障和极端的消息延迟会导致可用性问题;
4. 在通常情况下,一条指令只要在多数机器上完成rpc调用,少数慢的节点不会影响整个系统的可用性。
3. Paxos有什么问题
在过去十年中,Leslie Lamport的paxos协议几乎成为共识的代名词:Paxos成为课堂上最常被教授的共识协议。也是大多数共识算法实现的起点。Paxos首先定义了一个能够对单个决策达成一致的协议,单个多副本日志条目。我们称这个为单决策,接着Paxos将多个实例与该协议结合,以实现一系列的决策例如一个日志(multi-paxos)。Paxos同时确保了安全性和活性(liveness),且它支持集群中成员的变更。它的正确性已经被证明,且Paxos在一般场景下很高效。
不幸的是,Paxos有两个明显的缺陷。首先是Paxos非常难以理解。它的完整解释[15]非常隐晦,很少人能成功的理解,只有少部分人付出极大的努力后能够理解它。因此,出现了很多尝试解释Paxos通过简单地方式[16,20,21]。这些解释集中于单决策子集,至今他们仍具有挑战性。这些解释着手于单决策Paxos这一子集,尽管这仍很有挑战。在对NSDI2012出席者的非正式调查中,我们发现尽管在经验丰富的研究者中,也几乎没有人觉得Paxos容易。我们自己就受Paxos困扰,直到阅读了一些简化的解释后我们才理解了完整的协议,所以我们设计了自己的替代的协议,这一过程花了差不多一年时间。
我们假设Paxos的隐晦性来源于其选择单决策子集作为基础。单决策Paxos比较冗余且晦涩难懂:它分为两个阶段,没有简单地直观的解释和不能独立地理解。因此,人们很难对单决策协议可行有一个直观的解释。Multi-Paxos的规则由增加了很大的额外的复杂性和隐晦性。我们认为达到多决策共识(例如,一个日志而不是单个日志条目)的整个问题可被分解为更直观更显然的其他方式。
第二个问题是它没有为构建使用的实现提供良好的基础,主要原因在于人们对multi-Paxos没有形成广泛地一致意见。Lamport的描述几乎都关于单决策Paxos;他概括了multi-Paxos的可能的方法,但缺少许多细节。后来出现了很多试图具体化并优化Paxos的尝试,如[26, 39, 13],但这些方法互相之间都不一样且与Lamport的蓝图也不通。像Chubby[4]这样的系统实现了类Paxos算法,但在大多数条件下的细节都没有发表。
除此之外,Paxos的结构不足以支撑构建系统,这也是将算法分解为单决策的另一个结果。例如,单独选择一组日志然后将它们合并进序列日志中,这并没有什么收益,却增加了复杂性。设计一个围绕系统的日志更加简单高效,其中新的日志以约束的顺序有序的追加。另一个问题是Paxos用了对称点对点方案在它的核心中(尽管其最后提出了一个弱领导权方法作为性能优化)。者在仅需要做一个决策在简单地世界中是有意义的,但对于这种方式的实用系统来说没有意义。如果必须做出一系列决策,那么选出一个leader更快更简单,随后让leader去做协调。
从而实用系统很难与Paxos保持一致。每一个实现都是基于Paxos,发现很难实现它,然后开发了一个非常复杂的结构,不仅消耗时间还容易出现问题,加上理解困难Paxos加重了这些问题。Psxos的表达形式可能对于其理论证明具有良好的表现,但是在真正的实践过程中,由于Paxos的难以实现使得良好的理论证明失去了意义。
正因为这些问题,我们总结认为Paxos不能为系统构建还是教育提供一个良好的基础。带着大规模软件构建的重要性,我们决定看看我们是够能够设计出一套比Paxos表现更优异的共识算法。Raft就是实验的结果。
4可理解的设计
我们在设计Raft有几个目标:必须能够为系统构建提供完整和实用基础,以至于他能为开发者减少许多设计工作;必须能够在所有情况下都安全和在可操作的场景下可用;对常规的操作必须有效。但是我们最重要的目标是也是最困难的挑战就是可理解性。他必须被大多数使用者轻松的理解。另外,他必须能建立对算法的直觉,以至于系统构建者能够基于这个算法进行扩展,这在现实世界中是不可避免的。
在Raft设计过程中,有大量的点我们不得不选择替代的方案。在这些场景中,我们基于可理解性对这些替代方案进行评估:解释每个替代方案有多困难(状态空间有多复杂?是否有难以实现的点?),对于一个读者完整理解这个方法和他的实现有多容易?
我们认识到这样的分析具有高度的主观性,尽管如此,我们还是采用了两种能够被大家接受的方案。首先是众所周知的方案分解法:我们尽可能将问题分解为能够被解决、解释并理解的相对独立的子问题。例如,在Raft中,我们将其分为领导选举、日志复制、安全性和成员变更。
我们第二个方法便是简化状态空间,通过减少需要考虑的状态的数量使系统更具有连贯性并尽可能消除不确定性。尤其是日志不能有空洞,Raft限制了日志可以变得不一致的方式。尽管大多数情况我们尽量消除不确定性,但仍然有一些情况不确定性反而可以提升可理解性。在实践中,随机化的方法引入了不确定性,但是它们通常会通过用相同的方法解决所有可能的选择,因此减少了状态空间。我们使用的随机化的方法简化了Raft的领导选举算法。
5. Raft共识算法
Raft是一种用来管理第二章中描述的形式的多副本日志的算法。图2 以浓缩的形式总结了算法以供参考,图三列出算法的关键性质,这些图中元素将在本章剩余部分分条进行讨论。
Raft首先选举一个唯一的leader,全权让他管理副本日志来实现共识。leader接收客户端的日志条目,将他们复制到其他服务器上,然后告诉服务器什么将日志安全地应用到状态机上。通过一个leader简化副本日志的管理。例如,leader可以决定哪里放置新的条目在日志中而不用询问其他服务,而且数据流只是从leader流向其他服务。如果leader因失败宕机与其他服务失联,这时剩余的集群会选出新的leader。
考虑leader的方案,Raft将共识问题分解为三个相对独立的子问题,这些子问题会在接下来的小节中讨论:
- Leader选举: 发生新的leader选举必须是当前的Leader失败;
- 日志拷贝: leader必须接受来自客户端的日志条目,并把这些日志拷贝集群中的其他节点,并强制这些节点与自己的日志达成一致;
- 安全: Raft中的安全特性就是图3中的状态机安全特性:如果任何服务已经应用了特别的日志条目到它的状态机,没有其他服务在相同的日志索引上应用不同的命令。在5.4节中详细地描述了Raft如何保证这些特性;其解决方案包括对章节5.2中描述的选举机制的一个额外的约束。
在给出共识算法后,本章主要讨论了可用性问题和定时在本系统的角色。
5.1 Raft基础
一个Raft集群包含几个服务,5是最常见的服务个数,他允许系统有两个失败。在任何给定的每一台服务都处于下面三个状态之一:leader、follower和candidate。在正常的操作中,只会存在一个leader,其他节点都是follower。Follower是被动的:他们不主动发起任何请求指向他们自己的但是会简单相应来自leader或者candidates,主节点处理所有来自客户端的请求(如果一个客户端请求到follower,这个follower将会重定向到主节点)。第三个状态,candidate,通常用来选举新的leader如5.2节描述所示。图4展现了状态和转移,转移将会在接下来的讨论。
如图5所示,Raft将时间分为任意长度的时间片。任期被量化为连续的整数。每个任期都开始于一次选举,其中可能存在一个或多个候选人(candidate)参与leader的竞选如5.2描述所示。如果一个candidate赢得选举,它将作为一名leader服务在这个任期中。在一些场景中,一场选举会产生均票的结果。在这种情况下,这个任期中将不存在leader,新的任期会马上开始。Raft确保每个任期中只会有一个leader产生。
不同的服务节点在不同的时间可能会存在不同的转移,在一些情况中下,一个节点可能在所有的任期中都没有开启一次选举。任期在Raft中可作为逻辑时钟,能够让服务检测到过时的信息比如老旧的leader。每一个服务节点都会存储一个任期号,并随着时间单调递增。当前的任期将会发生交换只要服务间发生通信,入股一个服务的当前的任期号小于其他的,他会更新自己到更大的值。如果一个candidate或者leader发现他的任期过时了,便会立马转换成follower。如果一个服务收到一个带着过时任期号的请求,它会立马拒绝。
Raft服务间通信采用远程过程调用(RPC),基本的共识算法要求只有两种类型的RPC,请求投票类的RPC由candidate在选举中初始化,追加日志类RPC由leader在日志复制时初始化和提供心跳。第七章增加了第三种心跳,用来服务间的快照发送。如果他们没有收到响应在一个时间周期内,服务将会重试,他们会并行发起RPC以获得最佳性能。
5.2 主节点选举
Raft采用心跳机制来触发Leader选举。当服务节点启动,起始都作为follower节点。只要节点还能够收到leader或者candidate发来的有效心跳就会仍然作为follower。leader周期性的向follower节点发送心跳,以维持他的权威。如果一个follower在一个时间片内没有收到通信成为选举超时,然后他就会认为当前没有可用的leader,将会开始一次选举来选出新的leader。
为了开启一次选举,follower将会先增加当前的任期号,然后转变为candidate状态。然后它会先给自己投一票,然后生成一个投票请求RPC并行发给集群中的每个服务节点。candidate会维持这个状态直到发生三件事发生:(a)赢得选举;(b)其他服务节点成为了新的leader;(c)一个时间片内没有产生新的leader。每种情况将会在接下来的段落中详细介绍。
如果候选人从整个集群中收到大多数投票在同一个任期内将会赢得此次选举。每个服务将最多投给一个候选人在一个任期内,一般遵循先来先得的基本规则(注意:在5.4节投票新增了一个约束)。大多数规则主要是确保在一个任期内只有一个candidate赢得选举(如图3.中的选举安全特性)。一旦一个候选人赢得选举它将成为leader。随后leader将发送心跳给其他节点以建立他的权威并且阻止新的选举。
Candidate在等待投票的过程中,可能会收到日志追加的RPC来自于声称为主节点其他服务节点。如果这个leader的任期不小于candidate的当前任期,candidate认为该主节点是合法的并将自己的状态转变为follower。如果RPC的任期小于candidate的任期,便拒绝这个RPC请求并保持这个状态。
第三种情况来源于candidate既没有赢得选举也没有输掉:如果多个follower在同一时间成为candidate,票数可能会被均分导致没有candidate获得大多数的选票。当发生这种情况时,每一个candidate将会等待超时增大其term和开始另一轮RequestVote RPC来开始新的一轮选举。然而不采取额外的措施,投票均分可能会无限反复。
Raft采用随机选举超时以减少投票均分的情况,如果发生可以快速解决。为了第一时间解决均票的情况,选举超时将会从一个给定的区间(如150~300ms)随机选择。对于所有服务器都采用这种机制,从而在大多数情况下只会有一个节点首先超时,他赢得选举然后在其他任何节点超时之前发送心跳。在处理均票的问题也是采用这种策略。每个candidate在选举开始都会重置它的选举超时时间通过随机的方式,然后等待超时知道开启下一轮选举;这种方式减少了在新的选举中均票的情况。9.3节中表明通过这种方式能够快速选出一个leader。
选举就是一个例子,展示了可理解性如何指导我们在可替代的设计中做出选择。最初我们计划采用一个排序系统:每个候选人都有一个唯一的排名,以用来在相互竞争的候选人中选举。如果一个候选人发现另一个候选人具有更高的排序,它将会转变为follower以至于高排名的候选人能够更容易的赢得下一场选举。我们发现这种方案会在可用性具有一定的瑕疵(当高排名的节点失败后,一个低排名的节点需要等待超时才能再一次要成为candidate,但如果成为candidate太早,它可能会重置leader选举的进度)。我们对算法做了几次调整,但每一次调整都会产生新的问题。最终我们才总结出随机重试的方案最具有明显且可理解。
5.3 日志复制
一旦一个leader已经被选举,就会开始接收客户端的请求。每一个客户端的请求都包含一条指令要被副本状态机执行。leader会将这个指令作为一个新的条目追加到自己的日志中。然后并行发起日志追加RPC给其他服务节点以复制条目。当条目已经安全的被复制(下面详细描述),leader将会将这个条目应用到自己的状态机中,并返回执行结果给客户端。如果follower运行慢或者宕机了,或者网络丢包,leader会一直重试知道每个follower都完整地存储所有日志条目(尽管可能已经响应给客户端)。
日志组织如图6所示。每个日志条目存储为一个状态机指令并被leader附上任期号。日志中的任期号用来检测日志间是否一致和并确保图3的特性。每一条日志也会有一个整数索引标识在日志中的位置。
一般由leader决定什么时候安全地将日志条目应用状态机;Raft保证提交的条目是持久的并最终被所有可用的状态机执行。一旦这个日志条目被大多数节点成功复制就会被提交。这也会提交在leader日志中的以前的条目,包括上任leader的日志。5.4节中讨论一些问题当应用这个leader会跟踪它知道的被提交的最高的index,且它会在之后的AppendEntries RPC中包含这个index,这样其它server最终会发现它。一旦follower得知一个日志条目被提交,它会将该条目(按日志顺序)应用到它本地的状态机中。规则在leader改变之后,其还展示了“提交是安全的”的定义。
我们设计的Raft日志机制能维护不同服务器间高级别的一致性。不仅是简化系统行为并使其可预测,更是保证安全的重要组件。Raft维持如下的特性共同构成了图3中的“日志匹配性质(Log Matching Property)”:
- 如果两个不同日志中的条目拥有相同的索引和任期号,那他们一定是存储相同的指令;
- 如果两个不同日志中的条目拥有相同的索引和任期号,那么日志中之前的所有条目都是相同的;
第一个特性基于这样一个事实,leader在一个任期内对于给定index只会创建一条日志,且日志条目不会改变他们在日志中的位置。第二个特性由AppendEntries提供的一个简单的一致性校验保证。当发送日志追加的RPC时,leader会包含其日志中紧随新条目之前的索引号和任期号。如果follower在其日志记录中没有发现相同的任期号和索引,将会拒绝这个新日志条目。一致性检验会以归纳的步骤执行:日志最初的空状态满足“日志匹配性质”,且每当日志被扩展时一致性检验会保证“日志匹配性质”。因此,每当AppendEntries成功返回,leader会得知follower的日志和它自己的日志直到新条目的位置都是相同的。
在正常的操作中,leader和follower通常保持相同的日志,因此追加条目一致性检测从不失败。然而,leader宕机后可能会导致日志不一致(以往的leader没有完全拷贝自己日志中的条目给其他节点)。这些不一致可能在随着一系列的主节点和从节点宕机二加重。图7阐述了follower的日志不同于主节点的原因。
follower可能丢失了leader有的一些条目,它也可能存在朱主节点没有的日志条目。日志丢失和多余可能横跨多个任期。
在Raft中,主节点通过强制从节点复制自己的日志条目的方式来处理不一致。这意味着follower的与主节点冲突日志可能会被主节点的日志覆盖。5.4节中表明当增加一个约束后这将是安全的。
为了让follower的日志与主节点保持一致,,leade必须找到两个日志记录中最新的相同的记录,删掉follower中在这个点之后日志,然后发送给follower在这个点之后leader的所有日志。所有这些操作都在响应AppendEntries RPC的一致性检验时发生。
leader会维持每个follower的nextIndex,它是leader下一个要发送的日志索引号。当leader首次掌权时,它会将所有的nextIndex值初始化为其日志的最后一个条目的下一个index(在图7中该值为11)。如果一个follower的日志和主节点不一致,日志追加一致性检测将会失败在下一次日志追加RPC中。没拒绝一次,leader将会把nextIndex减一然后重新发送日志追加RPC。最终nextIndex将会到达主从匹配的一个点。当这种情况发生,追加日志成功,将会移除follower中冲突的日志然后追加主节点的条目。一旦追加日志成功,follower的日志将会和主节点保持一致,且在这个任期内都会一致。
如果需要的话,协议可被优化以减少拒绝AppendEntries RPC的次数。例如,当拒绝一个日志追加请求,follower可以在响应中带上冲突条目的任期号和当前任期的第一条日志索引号。有了这条消息,leader可以一次知道下一次发给该节点的nextIndex从而跳过所有冲突的条目。一条RPC可以携带多条冲突的日志条目而不是一次一条。在实践中,我们怀疑这种优化是否需要,因为失败频次低,也不会存在有很多不一致的条目。
有了这样的机制,leader在掌权时不需要采取任何特殊的行动来恢复日志一致性。它只需要开始正常的操作,然后日志会在响应追加日志一致性检验故障时自动恢复。leader绝不覆盖或删除自己的日志条目(leader只追加日志特性,图3)。
这种日志复制机制体现了第二章描述的理想的共识特性:只要大多数服务器在线,Raft就可以接受、复制、并应用新日志条目;在正常情况下新日志条目只需要一轮RPC就可以被复制到集群中大多数节点上;单个缓慢的follower不会影响性能。
5.4 安全
在前面的章节中描述了Raft如何选举leader和复制日志条目。然而,目前描述的机制不能完全有效地保证每个状态机正确地执行相同的指令在相同的顺序。例如,主节点提交了一些日志时但是follower不可用,然后它可能被选举为leader并将这些条目覆盖为新的条目,这样不同的状态机可能执行了不同的指令序列。
本节通过增加一个约束在哪些可能成为leader的服务来完善Raft算法。这些约束确保了leader包含任何任期中所提交的完整日志条目(Leader完整特性,图3.)。有了这个选举约束,我们可以让提交的规则更加精确。最后,我们给出了领导完整性性质的简要证明,并展示了它如何让多副本状态机的行为正确。
5.4.1 选举约束
在任何基于leader的共识算法中,leader必须最终存储所有的日志条目。在一些共识算法中,例如Viewstamped Replication[22],leader不能被选举如果它没有包含所有提交的条目。这些算法采用额外的机制来判定缺失的日志条目,并在选举时或选举完成后的很短的时间里,将它们传输给新的leader。不幸的是,这需要引入额外的机制,使得复杂性增加。Raft使用了一种更简单的方法,该方法会保证在过去的term中已被提交的条目在选举之初就在每个新的leader上,而不需要将这些条目再传输给leader。这意味着日志条目只流向一个方向,leader到follower,leader绝不会覆盖现存的条目在他的日志中。
Raft采用投票机制阻止一个没有包含所有完整提交的日志的candidate成为leader。一个candidate要成为leader必须联系大多数节点,这意味着每个被提交过的条目一定出现在这些服务器中的至少一个上。如果该candidate的日志和大多数节点一样新,那么它将包含所有提交的日志。请求投票的RPC需实现这样的约束:RPC包含candidate的日志信息,如果候选者的日志没有自己的日志新将会拒绝投票。
Raft通过比较最后一个日志条目的索引号和任期号来决定两个日志条目中哪一个更新。如果两个日志中的最后一个条目携带不同的任期号,那么具有更高的任期 日志更新。否则那个的日志条目更长更新。
5.4.2 提交之前的任期的条目
如5.3节描述,leader知道当前任期的条目一旦提交便已经存在大多数服务节点上。如果leader在提交之前宕机了,下一任leader将会尝试完成条目复制。然而,leader不能立即认为来自前任领导的日志条目是否提交,尽管这个已经存储在大多数服务上。图8 描述一种情况即使老的日志条目存在大多数节点也有可能被新的leader覆盖写。
为了消除图8中的问题,Raft绝不通过计算副本数来提交前任的日志条目。只有当前任期的日志条目才会通过计算副本数提交,一旦一个来自当前的任期的日志已经被提交用这种方法,根据日志匹配特性所有之前的日志也会间接的被提交。也有一些情况,leader可以安全的认为老的日志条目已经提交(例如,条目已经存在所有节点上),但是Raft为了简单采取更加保守的方法。
因为当leader复制来自之前term的日志条目时,日志条目会保持其原来的term号,所以Raft在提交规则中引入了额外的复杂性。在其他共识算法中,如果一个新的leader要复制原来任期的日志,需要将日志中的任期改为当前任期号。Raft的方法会使日志条目的推导更简单,因为无论何时它们都能在日志间维护相同的term号。另外,新的领导发送更少的老任期的日志比其他共识算法(其他算法必须在提交之前发送多余的日志条目来重新编号)。
5.4.3 安全性证明
给定完整的Raft算法,现在我们可以更精确地证明“领导完整性性质”成立(该证明基于安全性的证明,见章节9.2)。我们假设“领导完整性性质”不成立,那么我们会得出一个矛盾。假设term T T T的leader( l e a d e r T leader_T leaderT)提交了一个该term的日志条目,但是该日志条目没被之后的某个term的leader保存。考虑该leader没有保存该条目的最小term U U U( U > T U>T U>T)。
- 被提交的条目在 l e a d e r U leader_U leaderU被选举时必须不在其日志中(leader从未删除或覆写日志条目);
- l e a d e r T leader_T leaderT将该条目复制到了集群中大多数服务器上,且 l e a d e r U leader_U leaderU收到了来自集群大多数的投票。因此,至少一个服务器(“投票者”)既从 l e a d e r T leader_T leaderT接受了该条目,又为 l e a d e r U leader_U leaderU投了票,如图9所示。该投票者是达成矛盾的关键;
- 投票者必须在为 l e a d e r U leader_U leaderU投票前接受来自 l e a d e r T leader_T leaderT的已提交的条目;否则,它会拒绝来自 l e a d e r T leader_T leaderT的AppendEntries请求(它当前的term将会比T高);
- 投票者在为 l e a d e r U leader_U leaderU投票时仍保存着该条目,因为每个中间leader都包括该条目(基于假设),leader永远不会删除条目,且follower仅在条目与leader冲突时才会删除它们。
- 投票者将它的选票投给了 l e a d e r U leader_U leaderU,因此 l e a d e r U leader_U leaderU的日志必须至少于该投票者一样新。这导致两个矛盾之一。
- 首先,如果投票者和 l e a d e r U leader_U leaderU的最后一条日志任期相同,那么 l e a d e r U leader_U leaderU的日志必须至少与投票者一样长 ,所以它的包含了每一条日志条目。这里就产生了一个矛盾,因为投票者包含了该已提交的条目而我们假设 l e a d e r U leader_U leaderU没有包含该条目。
- 否则, l e a d e r U leader_U leaderU的最后一条日志任期必须大于投票者的任期。而且,他大于 T T T,因为投票者的最后一条日志任期至少为T(它包含来自任期 T T T的提交日志).创建了 l e a d e r U leader_U leaderU的最后一个日志条目的之前的leader的日志必须包含了该被提交的条目(基于假设)。那么,根据“日志匹配性质”, l e a d e r U leader_U leaderU的日志必须同样要包含该被提交的日志,这是一个矛盾。
- 这样,就产生了完整的矛盾。因此,所有term大于 T T T的leader必须包括所有在term T T T中提交的条目。
- “日志匹配性质”简介地确保了之后的leader也包含了被提交的条目,如 图8(d) 中的index 2一样。
有了领导完整特性,我们可以证明图3中的状态机安全特性,如果一个节点应用了一个给定索引号的日志条目到它的状态机中,没有其他的服务器会在相同的索引号应用不同的日志条目。当服务器将一个日志条目应用到其状态机时,它直到该条目的日志必须与leader的日志相同,且该条目必须是被提交的。现在考虑
任何一个服务应用了一个给定的日志索引;日志完整性保证了所有高任期的leader都会存储相同的日志条目,因此服务节点在后面的任期中也会应用索引时,也会应用相同的值。因此保证了状态机安全特性。
最后,Raft要求服务节点按照日志索引有序地应用条目。结合状态机安全特性,这意味着所有的服务节点能够以相同顺序准确地应用相同的日志条目集合到它们的状态机中。
5.5 Follower和candidate宕机
在这之前我们着重介绍了leader坏掉的情况。Follower和candidate宕机后的处理情况比主节点简单多了,他俩可以用相同的方式处理。如果一个Follower和candidate宕机,请求投票和日志追加的RPC将会失败。Raft通过不断地重试来处理这种失败;如果宕机的节点重新恢复,RPC将会恢复成功。如果一个节点接受了请求但是在相应前宕机了,再次启动会继续收到这个RPC。Raft的RPC是幂等的,所以这不会造成影响。例如一个Follower收到一个日志追加请求所包含的日志条目已经在他的日志中,它会忽略新请求中的条目。
5.6 定时和可用性
我们对Raft的其中一个要求便是安全一定不能依赖定时:系统不能因为一些突发事件导致比预期快或者慢就得到错误的结果。然而,可用性(系统在一定时间内响应客户端的能力)一定不可避免地依靠定时。例如,如果崩溃服务器间的消息交换消耗了比通常更长的时间,candidate将不会保持足够的时间以赢得选举。没有稳定的leader,Raft不能进展(make progress,译注:本文中指可以继续提交新值)。
领导选举是Raft中定时最重要的一个方面。只要系统满足如下的定时要求(timing requirement),那么Raft就能够选举并维护一个稳定的leader:
b
r
o
a
d
c
a
s
t
T
i
m
e
≪
e
l
e
c
t
i
o
n
T
i
m
e
o
u
t
≪
M
T
B
F
broadcastTime \ll electionTimeout \ll MTBF
broadcastTime≪electionTimeout≪MTBF
在这个不等式中,
b
r
o
a
d
c
a
s
t
T
i
m
e
broadcastTime
broadcastTime是指一个节点并行向其他其他节点并行发送RPC和接受他们的响应的平均时间。
e
l
e
c
t
i
o
n
T
i
m
e
o
u
t
electionTimeout
electionTimeout是5.2节中的描述的选举超时时间;
M
T
B
F
MTBF
MTBF是单个服务器发生两次故障间的平均时间。
b
r
o
a
d
c
a
s
t
T
i
m
e
broadcastTime
broadcastTime应该比
e
l
e
c
t
i
o
n
T
i
m
e
o
u
t
electionTimeout
electionTimeout小一个数量级,这样leader可以可靠的发送心跳消息以阻止选举发生;将随机方法应用于选举超时中,降低了均票的可能性。
e
l
e
c
t
i
o
n
T
i
m
e
o
u
t
electionTimeout
electionTimeout应该比
M
T
B
F
MTBF
MTBF小几个数量级,这样系统能够取得稳定的进展。当leader崩溃时,系统将会在大概
e
l
e
c
t
i
o
n
T
i
m
e
electionTime
electionTime的时间内不可用,我们想让这一时间仅占总时间的很小的比例。
b
r
o
a
d
c
a
s
t
T
i
m
e
broadcastTime
broadcastTime和
M
T
B
F
MTBF
MTBF是下层系统的属性,而
e
l
e
c
t
i
o
n
T
i
m
e
o
u
t
electionTimeout
electionTimeout是必须由我们选取的。Raft的RPC通常需要收件人将信息持久化到稳定存储中,所以广播时间可能在
0.5
−
20
0.5-20
0.5−20ms之间,取决于存储技术。因此,
e
l
e
c
t
i
o
n
T
i
m
e
o
u
t
electionTimeout
electionTimeout可能在10ms到500ms之间。通常服务器的MTBF为几个月或更长时间,这可以轻松满足定时要求。
6. 集群成员关系改变
到目前为止,我们都假设集群配置(configuration)(参与共识算法的服务器集合)是固定的。在实践中,偶尔需要改变集群配置,例如当失败时更换服务节点或者更改副本个数。这些操作也可以通过整个集群下线完成,更新配置文件,然后重启集群,但是在这个更改期间会导致集群不可用。另外,只要有人工操作,便有操作错误的风险。为了避免这些问题,我们决定自动化配置更改并把它们耦合到Raft共识算法中去。
为了配置能够安全地更改,在切换期间决不能有同时出现两个被选举出的leader的情况。不幸的是,直接切换老配置到新配置的任何方法都是不安全的。也不能一次切换所有的服务节点,因此集群可能潜在的被分为两个独立的大多数在切换期间(如图10)。
为了保证安全性,配置更改必须采用两阶段提交。现有很多方案来实现两阶段提交。例如,一些系统(例如[22])采用第一节点启用老的配置因此不能处理客户端的请求;然后在第二阶段采用新的配置。在Raft中,集群首先切换到可转换的配置我们成为联合共识,一旦联合共识已经被提交,联合共识同时结合新老配置。
- 日志条目会复制到两个配置的所有服务节点;
- 任何一个配置的任何服务节点都可以成为leader;
- 一致(用来选举和日志条目提交)需要在旧配置和新配置中的大多数分别达成。
联合共识让每个服务器能在不同时间切换配置而不需要做出安全性妥协。此外,联合共识允许集群继续相应客户端请求即使配置改变。
集群配置在副本日志中以特别的条目被存储和通信;图11描述了配置更改的过程。当leader接受到一个从
C
o
l
d
C_{old}
Cold到
C
n
e
w
C_{new}
Cnew的配置更改请求,它将联合共识的配置(图中的
C
o
l
d
,
n
e
w
C_{old,new}
Cold,new)存储为一个日志条目,并将该条目使用之前描述的机制复制。一旦一个节点添加了新配置条目在它的日志中,在未来的决策中都会采用这个配置(一个服务节点是否用日志中最新的配置,取决于这个配置条目是否已经提交),这意味着leader什么时候采用
C
o
l
d
,
n
e
w
C_{old,new}
Cold,new取决于这个配置什么时候被提交。如果leader宕机,新的leader选择
C
o
l
d
C_{old}
Cold还是
C
o
l
d
,
n
e
w
C_{old,new}
Cold,new取决于赢得选举的候选人是否收到
C
o
l
d
,
n
e
w
C_{old,new}
Cold,new。在任何情况下,
C
n
e
w
C_{new}
Cnew在这期间不能单独做决策。
一旦
C
o
l
d
,
n
e
w
C_{old,new}
Cold,new被提交,无论是
C
o
l
d
C_{old}
Cold还是
C
n
e
w
C_{new}
Cnew在没有对方认可都不能作决策。领导者完整性确保了只有带有
C
o
l
d
,
n
e
w
C_{old,new}
Cold,new日志条目的服务节点能够被选举为leader。现在leader可以安全地创建描述
C
n
e
w
C_{new}
Cnew的日志条目并将其复制到集群中。同样,当这个配置每个节点上被看到就很快会生效。当新的配置在
C
n
e
w
C_{new}
Cnew下已经被提交,旧的配置的节点就无关紧要了就可以下线了。如图11所示,没有时间让新老配单独作决策,这保证了安全性。
对于更新配置还存在三个问题待解决。首先就是新的服务没有初始化存任何日志条目。如果他们在这个状态下被加入集群,它可能需要很长时间来追赶,这段时间内它可能不能提交新的日志条目。为了避免可用性间隙,Raft引入额外的状态在配置更改之前,就是新加入集群的节点作为无投票权成员(leader复制日志条目给他们,但他们不会被当做大多数)。一旦新的节点追赶上集群中的其他节点,重配置可按之前描述的那样继续。
第二个问题就是集群的leader可能不是新配置中的一员。在这种情况下,一旦提交了
C
n
e
w
C_{new}
Cnew日志条目便退位为Follower。这意味着,在一段时间内(当leader提交
C
n
e
w
C_{new}
Cnew时),leader会管理不包括它自己在内的集群;他复制日志但是不会把自己算作大多数。当
C
n
e
w
C_{new}
Cnew被提交便开始切主这是新配置可最早以独立操作一个集群的点(总是能够从新配置的节点中选取一个leader)。在这之前,都只能带有老配置的节点才能成为主节点。
第三个问题便是移除节点(没有在新配置中的)可能让集群瘫痪。这些节点将不在收到心跳,他们会超时选举一个新的leader。这会让他们用新的任期号发送投票请求导致当前的leader被迫成为Follower。新的leader最终被选举。但是移除节点会超时又会选举新的leader,循环往复,导致可用性很差。
为了阻止这一问题,当他们认为当前还存在一个leader便会拒绝投票请求。特别是如果一个服务器在当前leader的最小选举超时内收到了投票请求 RPC,它不会更新其term或投票。这不会影响正常的选举,在正常的选举中,每个服务器都在开始选举前等待了最小选举超时时间。这可以帮助免受被移除的服务器的打扰:如果leader能够给其集群发送心跳,那么它不会被更大的term号废除。
7. 日志压缩
在正常操作期间,随着客户端的请求越来越多,Raft的日志会逐步膨胀,但是在实用系统中,不能无限制增长。随着日志长期增长,需要更多的空间存储以及更多的时间来复制。如果没有机制来丢弃日志中积累的过时的信息,这最终会造成可用性问题。
快照是最简单的方法来实现压缩。在快照中,当前的整个系统状态被写入一个快照在持久化存储中,然后将整个在这个点之前的日志废弃掉。快照被用在Chubby和ZooKeeper,接下来的章节将会描述Raft中的快照。
越来越多的方法致力于压缩,例如日志清理[36]和日志结构融合树[30,5],也可以。这些操作每次会处理一定比例的数据,所以它们能够更均匀地分摊压缩的负载。首先,它们会选取一个已经积累了许多被删除或被覆写的对象的数据区域;然后,它们将这个区域中存活的对象更紧凑地重写,然后释放这部分区域。与快照机制相比,这要求了额外的机制和复杂性,它通过操作整个数据集简化了问题。日志清理还需要对Raft进行修改,而状态机能使用与快照策略相同的接口实现LSM树。
图12展示Raft中快照的基本思想。每个节点都是独立进行快照,只涉及它日志中已经提交的条目。状态机的大部分工作就是将他的状态写入快照。Raft也包含少量的元数据在快照中:last included index是快照替换的最后一个条目(状态机应用的最后一个条目)的index,last included term是这一条目的任期号。这些被保存的信息用来支持对紧随快照后面的第一个条目的AppendEntries的一致性检验,因为该条目需要前一条日志的index和term。为了启用成员关系变更(章节6),快照还包含在lastincludedindex时的最新配置。一旦节点完成快照写入,便可以删除last included index之前的所有日志条目,也可以删除之前的快照。
即使所有节点都独自准备了快照,leader也必须发送快照给那些落后较多的Followers。这通常发生在leader已经丢掉要发给Follower的下一个日志条目。庆幸的是,这种场景基于正常的操作下是很少发生的:跟上leader的follower日志中已经有了这一条目。然而,除了慢节点或者新的节点加入集群都不会发生快照发送。如果想要这样的Follower跟上leader就是通过网络发送快照。
leader用了一个新的RPC叫InstallSnapshot发送快照给那些落后太多的Follower;如图13所示,当一个Follower通过这样的方式接受了快照,他必须决定如何处理现有的日志条目。通常快照会包含一些接受者日志中没有的新信息。在这种情况下,Follower将会丢弃它的整个日志条目;因为这些日志都可以被快照取代,且日志中可能含有与快照冲突的未提交的日志条目。相反,如果follower收到的快照描述的是它日志的前面一部分(由于重传或错误),那么被快照覆盖的日志条目会被删除,但是快照之后的条目仍有效且必须被保留。
这种快照方法与Raft强leader原则相违背,因为follower可以不通过leader的知识来创建快照。然而,我们认为这个偏差是合理的。虽然拥有领导者有助于避免在达成共识时出现冲突的决策,但在快照时已经达成共识,因此没有决策冲突。数据仍然由leader流向follower,只有followers可以识别他们的数据。
我们考虑到一个可以替代的基于leader的方法,这个方法依赖于leader构建快照然后将快照分发给每一个follower。然而,这种方式有两个缺陷。首先,发送快照给每一个followers会浪费网络带宽并且减缓快照的处理。每个follower已经有了他们所需要的信息建立他们自己的快照,相对于leader建立快照然后通过网络发给follower的代价小多了。其二,leader的实现将变得更加复杂。例如,leader需要并行发送快照和新的日志条目给每个followers。以免阻塞新的客户端的请求。
除此以外,还有两个问题影响快照的性能。首先,leader需要决定什么时候发送快照。如果一个节点发送快照太频繁,会浪会磁盘IO和性能;如果发送的频率过低,便会有存储能力的风险,还会增加日志重放的时间在机器重启的时候。一个简单地策略就是当日志大小达到固定字节数时创建快照。如果设置的大小明显比期望的快照大小大很多,那么创建快照时的额外磁盘带宽开销会很小。
第二个问性能题就是写入快照需要大量的时间,我们不希望这个影响了正常操作。解决方案就是采用写时复制技术以至于新的更新可以在不影响快照写入的情况下被接受。例如,由功能性数据结构构造的状态机本身就支持这一点。其次,操作系统的写时复制技术的支持(例如 fork on Linux)能够被用于构建整个状态机的内存快照(我们的实现就是基于这个方法)。
8. 客户端交互
这一节主要描述客户端如何与Raft交互,其中包括客户端如何找到集群主节点以及Raft如何支持线性语义[10]。这些问题对于所有基于共识的系统都存在,Raft对此的解决方案和其他的系统相似。
Raft的客户端发送他们所有的请求给Leader。当客户端第一次启动,它会会随机连接一台服务。如果客户端首先选择不是leader,这个节点将会拒绝客户端的请求,并为其提供它所知道的最新的leader的相关信息(AppendEntries请求需要leader的网络地址)。如果leader宕机,客户端请求将会超时;客户端下次又会重新选择一个服务节点。
我们对Raft的目标是实现线性语义(每个操作看上去是在它的调用和响应期间的某一时刻被瞬时(instantaneously)、至少一次执行(exactly once)的)。然而目前所描述的Raft可以执行一个命令多次:例如,如果一个leader提交了日志但是还没有响应客户端就宕机了,客户端将会重试这个命令到新的leader,造成这个命令被执行两次。解决方案就是客户端为每个命令进行唯一编号。然后,状态机跟踪最新的编号为每一个客户端处理,并同步响应。如果收到一个已经执行过的请求命令,状态机会不用再执行就立马返回结果。
只读操作不用写入任何东西到他们的日志。然而,如果没有额外的措施,会有读到旧数据的风险,因为响应请求的leader可能已经被新的leader取代,但是自己不感知。线性读一定不能返回旧的数据,Raft需要额外的措施来保证这一点且不用日志。首先,leader必须有条目提交的最新消息。领导者完整性保证了leader有所有提交的条目,但是在任期开始,他可能不知道哪些已经提交。为了找出提交点,leader需要在自己的任期中提交一次条目。Raft处理这个问题的策略是在自己的任期开始会提交一个空操作。其次,leader在处理之前需要校验这个读请求是否已经被取消(如果最近有新的leader产生,它的信息可能较为陈旧)。Raft处理这种情况通过与集群中大多数节点发送心跳消息在响应请求之前。这样,leader 可以依靠心跳消息来提供一个租赁形式[9],但是这依赖于定时安全性(它假定时间偏差是有界的)。
9 实现和评估
我们已经实现了Raft算法,其作为RAMCloudRAMCloud中保存配置信息的多副本状态机的一部分,并为RAMCloud协调器的故障转移提供帮助。该Rafte实现包含了约2000行C++代码,但不包括测试、注释、或空白行。该源码可以随意访问[23]。此外,还有约25个Raft的第三方开源实现[34]被用在不同的开发领域,它们基于本论文的草稿。另外,很多公司都部署了基于Raft的系统[34]。
本章其余部分将从三个角度来评估Raft:可理解性、正确性、和性能。
分割线
后面这一部分等笔者有时间就来继续翻译,主要内容到这里已经完成。