Go 实现:etcd/raft、dragonboat
Rust 实现:TiKV
C++ 实现 :nebula-graph-storage、 RethinkDB、logcabin
拜占庭将军问题
拜占庭将军问题(Byzantine failures
),是由莱斯利·兰伯特提出的点对点通信中的基本问题。含义是在存在消息丢失的不可靠信道上试图通过消息传递的方式达到一致性是不可能的。
拜占庭位于如今的土耳其的伊斯坦布尔,是东罗马帝国的首都。由于当时拜占庭罗马帝国国土辽阔,为了达到防御目的,每个军队都分隔很远,将军与将军之间只能靠信差传消息。在战争的时候,拜占庭军队内所有将军和副官必须达成一致的共识,决定是否有赢的机会才去攻打敌人的阵营。但是,在军队内有可能存有叛徒和敌军的间谍,左右将军们的决定又扰乱整体军队的秩序。在进行共识时,结果并不代表大多数人的意见。这时候,在已知有成员谋反的情况下,其余忠诚的将军在不受叛徒的影响下如何达成一致的协议,拜占庭问题就此形成
。
拜占庭将军问题是一个协议问题,拜占庭帝国军队的将军们必须全体一致的决定是否攻击某一支敌军。问题是这些将军在地理上是分隔开来的,并且将军中存在叛徒。叛徒可以任意行动以达到以下目标:欺骗某些将军采取进攻行动;促成一个不是所有将军都同意的决定,如当将军们不希望进攻时促成进攻行动;或者迷惑某些将军,使他们无法做出决定。如果叛徒达到了这些目的之一,则任何攻击行动的结果都是注定要失败的,只有完全达成一致的努力才能获得胜利 。
拜占庭假设是对现实世界的模型化,由于硬件错误、网络拥塞或断开以及遭到恶意攻击,计算机和网络可能出现不可预料的行为 。
根本难题
在中本聪发明比特币以前,世界上并没有一个非常完美的方法来解决“拜占庭将军问题” 。
究其根底,“拜占庭将军问题”最终想解决的是互联网交易、合作过程中的四个问题:
(1)信息发送的身份追溯 ;
(2)信息的私密性 ;
(3)不可伪造的签名 ;
(4)发送信息的规则
区块链轻而易举地解决了这一问题,它为信息发送加入了成本,降低了信息传递的速率,而且加入了一个随机元素使得在一定时间内只有一个将军可以广播信息。
在拜占庭将军问题中,第一个广播信息的将军就是第一个发现有效哈希值的计算机,只要其他将军接收到并验证通过了这个有效哈希值和附着在上面的信息,他们就只能使用新的信息更新他们的总账复制,然后重新计算哈希值。下一个计算出有效哈希值的将军就可以将自己再次更新的信息附着在有效哈希值上广播给大家。然后哈希计算竞赛从一个新的开始点重新开始。由于网络信息的持续同步,所有网络上的计算机都使用着同一版本的总账。
一、概述
1.1 What is Raft?
Raft是一种易于理解的共识算法。它在容错和性能上与Paxos相当。不同之处在于它被分解成相对独立的子问题,并且它清晰地处理了实际系统所需的所有主要部分。我们希望Raft将共识提供给更广泛的受众,而这些更广泛的受众将能够开发出各种质量比现在更高的基于共识的系统。
1.2 等等,什么是共识(consensus)?
共识是容错分布式系统的一个基本问题。共识涉及多个服务器对值(values
)达成一致。一旦他们对一个值做出决定,这个决定就是最终的。典型的共识算法在大多数服务器可用时取得进展;例如,5台服务器组成的集群可以在2台服务器故障的情况下继续运行。如果更多的服务器失败,它们将停止进展(但永远不会返回错误的结果)。
共识通常出现在复制状态机(replicated state machines
)的上下文中,这是构建容错系统的一种通用方法。每个服务器都有一个状态机(state machine
)和一个日志(log
)。状态机是我们希望使其具有容错功能的组件,例如散列表。即使集群中的少数服务器出现故障,客户端也会认为它们正在与单个可靠的状态机交互。每个状态机接受来自其日志的输入命令。在我们的哈希表示例中,日志将包括像set x to 3
这样的命令。共识算法用于对服务器日志中的命令达成一致。共识算法必须确保,如果任何状态机应用set x to 3
作为第n条命令,那么其他状态机将不会应用不同的第n条命令。因此,每个状态机处理相同的命令系列,从而产生相同的结果系列并到达相同的状态系列。
1.3 Raft 可视化
下面是在浏览器中运行的Raft集群。您可以与它交互,以查看Raft的操作。左边显示了5个服务器,右边显示了它们的日志。我们希望尽快制作一个视频来解释发生了什么。这个可视化(RaftScope)仍然很粗糙;pull requests
非常欢迎。
The Secret Lives of Data 是Raft的一个不同的可视化版本。它的导向性更强,互动性更少,所以它可能是一个更温和的起点。
1.4 出版物
这是“ the Raft paper
”,详细描述了由Diego Ongaro和John Ousterhout撰写的Raft:寻找一种可理解的共识算法(扩展版本)。这篇论文的略短版本在2014年USENIX年度技术会议上获得了最佳论文奖。
Diego Ongaro的博士论文更详细地扩展了论文的内容,其中包括一个一个更简单的集群成员变更算法。
1.5 Where can I ask questions?
询问关于Raft及其实现的问题最好的地方是Raft -dev谷歌组。一些实现也有自己的邮件列表;检查他们的 README。
二、寻找一种可理解的共识算法(扩展版本)
在分布式系统中,一致性算法至关重要。在所有一致性算法中,Paxos 最负盛名,它由莱斯利·兰伯特(Leslie Lamport
)于 1990 年提出,是一种基于消息传递的一致性算法,被认为是类似算法中最有效的。
Paxos 算法虽然很有效,但复杂的原理使它实现起来非常困难,截止目前,实现 Paxos 算法的开源软件很少,比较出名的有 Chubby、LibPaxos。此外,Zookeeper 采用的 ZAB(Zookeeper Atomic Broadcast)协议也是基于 Paxos 算法实现的,不过 ZAB 对 Paxos 进行了很多改进与优化,两者的设计目标也存在差异——ZAB 协议主要用于构建一个高可用的分布式数据主备系统,而 Paxos 算法则是用于构建一个分布式的一致性状态机系统。
由于 Paxos 算法过于复杂、实现困难,极大地制约了其应用,而分布式系统领域又亟需一种高效而易于实现的分布式一致性算法,在此背景下,Raft 算法应运而生。
Raft 算法在斯坦福 Diego Ongaro
和 John Ousterhout
于 2013 年发表的《In Search of an Understandable Consensus Algorithm
》中提出。相较于 Paxos,Raft 通过逻辑分离使其更容易理解和实现,目前,已经有十多种语言的 Raft 算法实现框架,较为出名的有 etcd、Consul、cockrouchDB ,tidb, kubernetes,goraft 。
摘要
Raft
是一种用于管理复制日志(replicated log
)的共识算法。它产生的结果相当于(multi-)Paxos
,它的效率与Paxos
一样,但它的结构与Paxos
不同;这使得Raft
更容易理解
并且还为构建实用的系统提供了更好的基础。为了增强可理解性,Raft分离了共识的关键元素,如领导者选举、日志复制和安全性,并且它强制了更强的一致性,以减少必须考虑的状态的数量。用户研究的结果表明,Raft
对学生来说比Paxos
更容易学习。Raft
还包括一种改变集群成员的新机制,它使用重叠多数来保证安全。
2.1 简介
共识算法允许一组机器作为一个连贯的组工作,可以在其中一些成员出现故障时能继续工作。正因为如此,它们在构建可靠的大规模软件系统中起着关键作用。Paxos[15,16]
在过去十年中主导了共识算法的讨论:大多数共识的实现都是基于Paxos或受其影响,Paxos
已经成为教授学生共识的主要工具。
不幸的是,Paxos
很难理解,尽管有许多尝试使它更容易接近。
此外,它的架构需要复杂的更改来支持实际的系统。因此,系统开发人员和学生都在与Paxos作斗争。
在与Paxos
进行了一番斗争之后,我们开始寻找一种新的共识算法,可以为系统构建和教育提供更好的基础。我们的方法不同寻常,因为我们的主要目标是可理解性:我们能否为实际系统定义一个共识算法,并以一种比Paxos
更容易学习的方式来描述它?此外,我们希望算法能够促进直觉的发展,这对系统构建者来说是必不可少的。重要的不仅仅是算法的工作,还有它为什么工作的显而易见。
这项工作的结果是一种叫做Raft
的共识算法。在设计Raft时,我们应用了特定的技术来提高可理解性,包括分解(Raft分解成领导者选举、日志复制和安全性)和状态空间缩简(相对于Paxos
, Raft
减少了不确定性的程度以及服务器之间不一致的方式)。一项对来自两所大学的43名学生的用户研究表明,Raft
明显比Paxos
更容易理解:在学习了两种算法之后,这些学生中有33人能够回答关于Raft
的问题比Paxos
的问题要好。
Raft
在许多方面类似于现有的共识算法(最值得注意的是,Oki and Liskov’s Viewstamped Replication [29, 22]
),但它有几个新的特点:
- Strong leader:
Raft
使用了比其他共识算法更强大的领导形式。例如,日志条目仅从leader流向其他服务器。这简化了对复制日志的管理,使Raft更容易理解。 - Leader election:
Raft
使用随机计时器来选举领导者。这只是为任何共识算法已经需要的心跳增加了少量的机制,同时简单而快速地解决冲突。 - Membership changes:
Raft
用于更改集群中的服务器集的机制使用了一种新的联合共识(joint consensus
)方法,其中两个不同配置的大多数在转换期间重叠。这允许集群在配置更改期间继续正常运行。
我们相信Raft
优于Paxos
和其他共识算法,无论是出于教育目的还是作为实现的基础。它比其他算法更简单,更容易理解;它的描述完全足以满足实际系统的需要;它有几个开源实现,并被几家公司使用;其安全性能已被正式规定和证明;其效率与其他算法相当。
2.2 复制状态机 (Replicated state machines
)
共识算法通常出现在复制状态机[37]的上下文中。在这种方法中,服务器集合上的状态机计算相同状态的相同副本,即使其中一些服务器关闭,状态机也可以继续运行。
复制状态机用于解决分布式系统中的各种容错问题。例如,大型系统只有一个集群领导,比如GFS
[8], HDFS
[38],和RAMCloud
[33],通常使用单独的复制管理领导者选举和存储必须在领导者崩溃中幸存的配置信息的状态机。复制状态机的例子包括Chubby
[2]和ZooKeeper
[11]。
复制状态机通常使用复制日志实现,如图1所示。每个服务器存储一个包含一系列命令的日志,它的状态机按顺序执行这些命令。每个日志以相同的顺序包含相同的命令,因此每个状态机处理相同的命令序列。由于状态机是确定性的,所以每个状态机都计算相同的状态和相同的输出序列。
保持复制日志的一致性是共识算法的工作。服务器上的共识模块从客户端接收命令并将其添加到日志中。它与其他服务器上的共识模块进行通信,以确保每个日志最终都以相同的顺序包含相同的请求,即使某些服务器出现故障。一旦正确复制了命令,每个服务器的状态机将按日志顺序处理它们,并将输出返回给客户端。因此,这些服务器似乎形成了一个单一的、高度可靠的状态机。
用于实际系统的共识算法通常具有以下特性:
- 它们确保在所有非拜占庭条件下的安全性(永远不会返回不正确的结果),包括网络延迟、分区和数据包丢失、复制和重新排序。
- 只要大多数服务器都是可操作的,并且可以彼此通信,也可以与客户机通信,它们就具有完整的功能(可用)。因此,由五台服务器组成的典型集群可以容忍任意两台服务器的故障。服务器通过停止被假定为失败;它们稍后可能会从稳定存储上的状态恢复并重新加入集群。
- 它们不依赖于时间来确保日志的一致性:错误的时钟和极端的消息延迟在最坏的情况下会导致可用性问题。
- 在一般情况下,只要大多数集群响应了一轮远程过程调用,命令就可以完成;少数慢速服务器不需要影响整体系统性能。
2.3 Paxos有什么问题?
在过去的十年中,Leslie Lamport的 Paxos
协议[15]几乎已经成为共识的代名词:它是课程中最常教授的协议,大多数共识的实现都将其作为起点。Paxos首先定义了一个能够就单个决策(如单个复制日志条目)达成一致的协议。我们把这个子集称为single-decree Paxos
。然后,Paxos将该协议的多个实例组合在一起,以促进一系列决策,如日志(multi-Paxos
)。Paxos确保了安全性和活跃性,并且支持集群成员关系的更改。该方法已被证明是正确的,在正常情况下是有效的。
不幸的是,Paxos有两个重要的缺点。第一个缺点是Paxos非常难以理解。[15]的完整解释是出了名的不透明;只有付出巨大努力的人才能理解它。因此,有几次尝试用更简单的术语来解释Paxos[16,20,21]。这些解释集中在single-decree
的子集上,但它们仍然具有挑战性。在NSDI 2012的一项非正式调查中,我们发现很少有人对Paxos感到满意,即使是在经验丰富的研究人员中。我们自己也在与Paxos作斗争;我们自己也在与Paxos作斗争;在阅读了一些简化的解释并设计出我们自己的替代协议之后,我们才能够理解完整的协议,这个过程花了将近一年的时间。
我们假设Paxos的不透明性来自于它选择单法令子集作为其基础。
单一法令的Paxos是密集而微妙的:它分为两个阶段,没有简单的直观解释,也不能独立理解。因此,很难直观地理解为什么单法令协议可以工作。multiPaxos的组合规则增加了显著的额外复杂性和微妙性。
我们认为,在多个决策上达成共识的总体问题(即,一个日志而不是一个单一的条目)可以用其他更直接和明显的方式分解。
Paxos的第二个问题是它没有为构建实用的实现提供良好的基础。原因之一是目前还没有广泛认可的多paxos算法。Lamport的描述主要是关于单一法令的Paxos;他概述了multi-Paxos的可能方法,但遗漏了许多细节。人们曾多次尝试充实和优化Paxos,如[26]、[39]和[13],但这些尝试彼此不同,也与Lamport的草图不同。系统比如Chubby[4]已经实现了类似paxos的算法,但在大多数情况下,它们的细节都没有公布。
此外,对于构建实用的系统来说,Paxos体系结构是一个糟糕的体系结构;这是单一法令分解的另一个后果。例如,单独选择一组日志条目,然后将它们合并到一个连续日志中,这样做几乎没有什么好处;这只是增加了复杂性。围绕日志设计系统更简单、更有效,其中新条目按约束顺序依次添加。另一个问题是Paxos在其核心使用了对称的点对点方法(尽管它最终建议采用一种弱形式的领导作为性能优化)。这在只做出一个决策的简化世界中是有意义的,但很少有实际系统使用这种方法。如果必须做出一系列决定,先选出一个领导人,然后让领导人协调这些决定会更简单、更快。
因此,实际的系统与Paxos几乎没有相似之处。每个实现都从Paxos开始,发现实现它的困难,然后开发一个截然不同的体系结构。这既耗时又容易出错,而且理解Paxos的困难加剧了这个问题。Paxos的公式可能是证明其正确性的定理的一个很好的公式,但真正的实现与帕克索斯证明没有什么价值。以下来自Chubby实现者的注释是典型的:
There are significant gaps between the description of
the Paxos algorithm and the needs of a real-world
system. . . . the final system will be based on an unproven protocol [4]
由于这些问题,我们得出结论,Paxos没有为系统构建或教育提供良好的基础。考虑到共识在大规模软件系统中的重要性,我们决定看看是否可以设计一种比Paxos性能更好的替代共识算法。Raft就是这个实验的结果。
2.4 为可理解性而设计
我们在设计Raft时有几个目标:它必须为系统构建提供一个完整而实用的基础,这样它就可以显著地减少开发人员所需的设计工作量;它必须在所有条件下都是安全的,并在典型的操作条件下可用;而且它必须对普通操作有效。但我们最重要的目标——也是最困难的挑战——是理解能力(understandability)。它必须能够让大量观众轻松地理解算法。此外,必须能够开发关于算法的直觉,以便系统构建者能够进行在实际实现中不可避免的扩展。
在Raft
的设计中,我们有许多需要在不同方法中进行选择的地方。在这些情况下,我们基于可理解性来评估备选方案:解释每个备选方案有多难(例如,它的状态空间有多复杂,它是否具有微妙的含义?),以及读者完全理解该方法及其含义有多容易?
我们认识到这种分析具有高度的主观性;尽管如此,我们还是使用了两种普遍适用的技术。第一种技术是众所周知的问题分解方法:在任何可能的情况下,我们将问题划分为可以相对独立地解决、解释和理解的独立部分。例如,在Raft中,我们分离了领导者选举、日志复制、安全性和成员变更。
我们的第二种方法是通过减少要考虑的状态数量来简化状态空间,使系统更加一致,并在可能的情况下消除不确定性。具体来说,日志不允许有漏洞,Raft
限制了日志之间不一致的方式。尽管在大多数情况下,我们试图消除非确定性,但在某些情况下,非确定性实际上提高了可理解性。特别是,随机方法引入了不确定性,但它们倾向于通过以类似的方式处理所有可能的选择来减少状态空间(“选择任何;没关系”)。我们使用随机化来简化Raft
领袖选举算法。
2.5 Raft 共识算法
Raft
是一种用于管理第2节中描述的复制日志的算法。图2总结了算法的简练形式,以供参考,图3列出了算法的关键属性;本节其余部分将逐个讨论这些图形的元素。
Raft
通过首先选举一个杰出的领导者( distinguished leader
)来实现共识,然后让领导者完全负责管理复制的日志(the replicated log
)。leader从客户端接收日志条目,在其他服务器上复制它们,并告诉服务器什么时候将日志条目应用到它们的状态机是安全的。有一个leader可以简化对复制日志的管理。例如,领导者可以在不咨询其他服务器的情况下决定在日志中放置新条目的位置,数据以简单的方式从领导者流向其他服务器。leader可能出现故障或与其他服务器断开连接,在这种情况下,将选出新的leader。
基于leader方法,Raft将共识问题分解为三个相对独立的子问题,这些子问题将在下面的子节中讨论:
- Leader election 当现有的领导者失败时,必须选择一个新的领导者(章节5.2)。
- Log replication leader必须接受来自客户端的日志条目,并在集群中复制它们,迫使其他日志与它自己的日志一致(章节5.3)。
- Safety Raft的关键安全属性是图3中的状态机安全属性:如果任何服务器应用了一个特定的日志条目到它的状态机,那么没有其他服务器可以对相同的日志索引应用不同的命令。章节5.4描述了Raft如何确保这个属性;解决方案涉及5.2节中描述的对选举机制的附加限制
在介绍了共识算法之后,本节讨论了可用性问题和时间在系统中的作用。
2.5.1 Raft基础
一个Raft集群包含几个服务器;5是一个典型的数字,系统能容忍两台服务器故障。在任何给定的时间,每个服务器都处于三种状态之一:领导者、追随者或候选人(leader, follower, or candidate
)。在正常的操作中,只有一个领导者,所有其他的服务器都是追随者。追随者是被动的:他们自己不提出要求,只是对领导和候选人的要求做出回应。leader处理所有客户端请求(如果客户端联系了follower, follower将其重定向给leader)。第三个状态,即候选状态,用于选举5.2节所述的新领导人。图4显示了状态及其转换;下面将讨论转换。
Raft
将时间划分为任意长度,如图5所示。term 用连续的整数编号。每一届任期都以选举(election
)开始,如5.2节所述,一名或多名候选人试图成为领导人。如果一个候选人赢得选举,那么他将在剩下的任期内担任领导人。在某些情况下,选举会导致选票分裂。在这种情况下,任期结束时将没有领导人;新任期(新选举)即将开始。Raft
确保最多有一个在任期内的领导者。
不同的服务器可能会在不同的时间观察到任期之间的过渡,在某些情况下,服务器可能不会观察到选举甚至整个任期。术语在Raft
中就像一个逻辑时钟[14],它们允许服务器检测过时的信息,比如过时的领导者。每个服务器存储一个当前的term number
,它随时间单调增加。当服务器通信时,交换当前 term ;如果一个服务器的当前项小于另一个服务器的当前项,则将其当前项更新为较大的值。如果候选人或领导人发现自己的任期(term)已经过期,就会立即恢复到追随者状态。如果服务器接收到带有过期任期号的请求,它将拒绝该请求。
Raft
服务器使用远程过程调用(RPC
)进行通信,而基本的共识算法只需要两种类型的RPC
。 RequestVote RPC
由候选人在选举期间发起(章节5.2),AppendEntries RPC
由领导者发起,用于复制日志条目并提供一种形式的心跳(heartbeat
)(章节5.3)。第7节添加了第三个RPC
,用于在服务器之间传输快照。如果服务器没有及时接收到响应,则会重试RPC
,并且它们会并行地发出RPC
以获得最佳性能。
2.5.2 领导者选举
Raft
使用心跳机制来触发领导者选举。当服务器启动时,它们开始作为追随者。一个只要服务器从领导者或候选人接收到有效的RPC
,它就保持在追随者状态。领导者定期向所有追随者发送心跳(AppendEntries RPC,不携带日志条目),以维护他们的权威。如果一个追随者在一段称为选举超时(election timeout
)的时间内没有收到任何通信,那么它就认为没有可行的领导者,并开始选举一个新的领导者。
要开始选举,追随者增加其当前任期并过渡到候选人状态。然后,它为自己投票,并并行地向集群中的其他每个服务器发出RequestVote RPC
。候选人一直处于这种状态,直到发生以下三种情况之一:(a)它赢得了选举,(b)另一个服务器确立了自己的领导地位,或者©一段时间没有赢家。下面各段将分别讨论这些结果。
如果候选人在同一任期内获得整个集群中大多数服务器的投票,则该候选人将赢得选举。每个服务器在给定的任期内最多投票给一名候选人,以先到先得( first-come-first-served
)的方式(注:章节5.4增加了对投票的额外限制)。多数决定原则确保最多一名候选人可以赢得某一特定任期的选举(图3中的选举安全属性)。一旦候选人赢得选举,它就成为领导者。然后,它向所有其他服务器发送心跳消息,以建立其权威并防止新的选举。
在等待投票期间,候选人可能会收到一份来自另一个声称是领导者的AppendEntries RPC
。如果领导者的任期(包括在其RPC中)至少与候选人的当前任期一样大,那么候选人承认领导者是合法的,并返回追随者状态。如果RPC中的术语小于候选人的当前术语,则候选人拒绝RPC并继续处于候选人状态。
Raft
使用随机的选举超时来确保分裂投票的情况很少,并且它们能够迅速得到解决。为了在第一时间防止分裂选票,选举超时从固定的间隔中随机选择(例如,150 - 300毫秒)。这将分散服务器,因此在大多数情况下,只有一台服务器会超时;它赢得了选举,并在其他服务器超时之前发送心跳。同样的机制也用于处理分裂投票。每个候选人在选举开始时重新启动随机选举超时,并等待该超时时间过去后再开始下一次选举;这降低了在新的选举中再次出现分裂投票的可能性。第9.3节说明了这种方法可以迅速选出领导人。
选举就是一个例子,说明了可理解性如何引导我们在设计方案之间做出选择。最初,我们计划使用一个排名系统:每个候选人被分配一个唯一的排名,用于在竞争的候选人之间进行选择。如果一个候选人发现了另一个排名更高的候选人,它就会回到追随者状态,这样排名更高的候选人就可以更容易地赢得下次选举。我们发现这种方法在可用性方面产生了微妙的问题(如果一个排名较低的服务器失败了,它可能需要暂停并再次成为候选服务器,但如果它过早地这样做,它可能会重置选举领导者的进程)。我们对算法进行了几次调整,但每次调整后都会出现新的拐角情况。最终我们得出结论,随机重试方法更明显,也更容易理解。
2.5.3 日志复制 (Log replication
)
一旦选出领导者,它就开始为客户的要求提供服务。每个客户端请求都包含一个由复制状态机执行的命令。leader将该命令作为一个新条目附加到其日志中,然后并行地向每个其他服务器发出AppendEntries RPC
以复制该条目。当条目被安全复制(如下所述)时,leader将该条目应用于其状态机,并将执行结果返回给客户机。如果follower崩溃或运行缓慢,或者网络包丢失,leader将无限地重试AppendEntries RPC
(即使在它响应客户端之后),直到所有follower最终存储所有日志条目。
日志的组织如图6所示。每个日志条目存储一个状态机命令,以及领导者收到该条目时的任期(term )号。日志条目中的term编号用于检测日志之间的不一致性,并确保图3中的某些属性。每个日志条目还有一个整数索引,用于标识其在日志中的位置。
领导者决定何时将日志条目应用到状态机是安全的;这样的条目称为已提交(committed
)。Raft保证提交的条目是持久的,最终将由所有可用的状态机执行。一旦创建该条目的领导者将其复制到大多数服务器上(例如,图6中的条目7),日志条目就会被提交。这也会提交领导者日志中的所有前面条目,包括之前领导者创建的条目。第5.4节讨论了在领导变更后应用这一规则时的一些微妙之处,也表明了承诺的这种定义是安全的。leader跟踪它知道的要提交的最高索引,并包含未来的AppendEntries RPC
的索引(包括心跳),以便其他服务器最终发现。一旦跟踪器了解到一个日志条目已提交,它将该条目应用到其本地状态机(按日志顺序)。
我们设计了Raft
日志机制来维护不同服务器上日志之间的高度一致性。这不仅简化了系统的行为,使其更可预测,而且是确保安全的重要组成部分。Raft
维护了以下属性,它们共同构成了图3中的日志匹配属性 (Log Matching Property
):
- 如果不同日志中的两个条目具有相同的索引和 term,则它们存储相同的命令。
- 如果不同日志中的两个条目具有相同的索引和 term,则这些日志中前面所有条目都是相同的。
第一个属性源于这样一个事实,即leader在给定的术语中用给定的日志索引创建最多一个条目,并且日志条目永远不会改变它们在日志中的位置。第二个属性是由AppendEntries
执行简单的一致性检查来保证的。当发送AppendEntries RPC
时,leader将条目的索引和term包含在其日志中,紧跟在新条目之前。如果追随者没有在其日志中找到具有相同索引和term的条目,那么它将拒绝新的条目。一致性检查归纳成这几步:日志的初始空状态满足LogMatching Property
;当日志被扩展时,一致性检查会保留“日志匹配属性”。因此,只要AppendEntries成功返回,leader就知道follower的日志与它自己通过新条目记录的日志是相同的。
正常运行时,leader和follower的日志保持一致,因此AppendEntries
一致性检查不会失败。但是,leader崩溃可能会导致日志不一致(旧的leader可能没有完全复制其日志中的所有条目)。这些不一致可能会在一系列领导者和追随者崩溃中加剧。图7说明了追随者日志与新领导者日志的不同之处。follower可能会丢失当前leader 上的条目,或有不存在当前leader 上的额外条目,或者两者都存在。日志中缺失和多余的条目可能跨越多个任期(term
)。
在Raft
中,leader通过强制追随者的日志复制自己的日志来处理不一致。这意味着追随者日志中的冲突条目将被领导者日志中的条目覆盖。第5.4节将说明当与另一个限制相结合时,这是安全的。
为了使追随者的日志与自己的日志保持一致,领导者必须找到两个日志一致的最新日志条目,删除在那之后追随者日志中的所有条目,并在那之后将领导者的所有条目发送给追随者。所有这些操作都是对 AppendEntries RPCs
执行的一致性检查的响应。leader为每个follower维护一个nextIndex
,这是leader将发送给该follower的下一个日志条目的索引。当一个领导者第一次掌权时,它将所有的nextIndex值初始化到其日志中最后一个索引的后面(图7中的11)。如果一个追随者的日志与领导者的不一致,AppendEntries
一致性检查(AppendEntries consistency check
)将在下一个AppendEntries RPC中失败。在拒绝之后,leader递减nextIndex
并重试AppendEntries RPC
。最终,nextIndex
将达到leader和follower日志匹配的点。当这种情况发生时,AppendEntries
将成功,它将删除follower日志中的任何冲突条目,并从leader日志中追加条目(如果有的话)。一旦AppendEntries
成功,追随者的日志将与领导者的日志保持一致,并且在剩余的任期内将保持这种方式。
如果需要,可以对协议进行优化以减少被拒绝的AppendEntries RPC
的数量。例如,**当拒绝AppendEntries请求时,follower可以包含冲突条目任期和它存储的那个任期(term)第一个条目的索引。**有了这些信息,leader可以递减nextIndex
以绕过该项中所有冲突的条目;每个有冲突条目的术语都需要一个AppendEntries RPC
,而不是每个条目都需要一个RPC。在实践中,我们怀疑这种优化是必要的,因为失败很少发生,而且不太可能有许多不一致的条目。
有了这种机制,领导者在掌权时不需要采取任何特殊行动来恢复日志的一致性。它刚刚开始正常操作,当AppendEntries一致性检查失败时,日志会自动收敛。leader从来不会覆盖或删除自己日志中的条目 (the Leader Append-OnlyProperty
in Figure 3)。
这种日志复制机制展示了第2节中描述的理想的共识属性:**只要大多数服务器是正常的,Raft就可以接受、复制和应用新的日志条目;**在正常情况下,一个新条目可以通过单轮rpc复制到大多数集群中;一个缓慢的追随者不会影响性能。
2.5.4 安全
前面的部分描述了Raft如何选举领导者和复制日志条目。然而,到目前为止所描述的机制还不足以确保每个状态机以相同的顺序执行完全相同的命令。例如,当leader提交几个日志条目时,一个follower可能不可用,然后它可以被选为leader并用新的条目覆盖这些条目;因此,不同的状态机可能执行不同的命令序列。
本节通过添加一个关于哪些服务器可以被选为leader的限制来完成Raft
算法。该限制确保任何给定任期包含以前任期中提交的所有条目(领导者完备性性属性,the Leader Completeness Property from Figure 3
),然后,我们制定了更精确的 commitment 规则。最后,我们给出了Leader完备性属性的证明草图,并展示了它如何导致复制状态机的正确行为。
2.5.4.1选举限制
在任何基于领导者的共识算法中,领导者最终必须存储所有提交的日志条目。在一些共识算法中,比如Viewstamped Replication[22],即使领导者最初不包含所有提交的条目,也可以选出领导者。这些算法包含额外的机制来识别缺失的条目,并在选举过程中或之后不久将它们传输给新领导者。不幸的是,这会导致相当多的额外机制和复杂性。Raft使用了一种更简单的方法它保证从的时刻起,前一项中提交的所有条目都出现在每个新leader上它的选举,不需要转移那些条目领导者。这意味着日志条目只在一个方向上流动,从领导者到追随者,并且领导者永远不会覆盖其日志中的现有条目。
Raft
使用投票过程来阻止候选人赢得选举,除非其日志包含所有已提交的条目。候选人必须与集群中的大多数人联系才能当选,这意味着每个提交的条目必须至少存在于其中一个服务器中。如果候选人日志至少与大多数日志中的任何其他日志一样最新(“up-to-date”的定义在下面),那么它将保存所有提交的条目。RequestVote RPC
实现了这个限制: RPC
包含关于候选人日志的信息,如果投票者自己的日志比候选人的日志更新,那么投票者就会拒绝投票。
Raft
通过比较日志中最后一项的索引和项来确定两个日志中哪一个是最新的。如果日志的最后一个条目具有不同的任期,那么具有较晚的任期的日志是最新的。如果日志以相同的任期结束,那么较长的日志就是最新的。
2.5.4.2 提交以前任期的条目
如5.3节所述,leader知道它当前任期中的条目一旦存储在大多数服务器上,该条目就被提交了。如果一个leader在提交一个条目之前崩溃,未来的leader将尝试完成该条目的复制。然而,一旦前一个任期的条目存储在大多数服务器上,领导者不能立即得出结论,认为该条目已经提交。图8 说明了一种情况,旧的日志条目存储在大多数服务器上,但仍然可以被未来的领导者覆盖。
为了消除图8所示的问题,Raft
从不通过计算副本来提交以前术语的日志条目。通过计算副本,只提交来自leader当前任期的日志条目;一旦以这种方式提交了来自当前项的条目,那么由于日志匹配属性(Log Matching Property
),所有先前的条目都将间接提交。在某些情况下,领导者可以安全地得出结论,即提交了一个旧的日志条目(例如,如果该条目存储在每个服务器上),但Raft为了简单起见采取了更保守的方法。
Raft在提交规则中引起了这种额外的复杂性,因为当leader从以前的任期中复制条目时,日志条目保留了它们原来的任期号。在其他共识算法中,如果一个新的领导者重复了之前“任期”的条目,它必须使用新的“任期号”。Raft的方法使对日志条目的推理变得更容易,因为它们在时间和日志之间保持相同的术语数。此外,Raft中的新leader会比其他算法发送更少的先前任期的日志条目(其他算法必须发送冗余的日志条目来重新编号它们)。
2.5.4.3 安全参数
给定完整的Raft算法,我们现在可以更精确地证明Leader完备性属性成立(这个论点是基于安全证明( the Leader Completeness Property
);看到9.2节)。我们假设Leader完备性不成立,我们就证明了一个矛盾。假设任期 T
(leaderT) 的leader从它的任期中提交了一个日志条目,但是该日志条目没有被某个未来任期的leader存储。考虑最小任期 U > T
,其leader (leaderU)不存储条目。
- 提交的条目必须在选举时从 leaderU 的日志中不存在(领导者从不删除或覆盖条目)。
- leaderT在集群的大多数人身上复制了条目,而 leaderU 从集群的大多数人那里获得了投票。因此,至少有一个服务器(“投票者”)既接受了来自 leaderT的条目,又投票给了 leaderU ,如图9所示。选民是达成矛盾的关键。
- 在投票给 leaderU之前,投票者必须接受 leaderT的承诺条目;否则它将拒绝来自 leaderT 的 AppendEntries 请求(它的当前任期将高于T)。
- 投票者在投票给leaderU时仍然存储条目,因为每个介入的leader都包含条目(假设),leader永远不会删除条目,follower只会删除与leader冲突的条目。
- 投票人将选票授予了leaderU,所以leaderU的日志必须和投票人的日志一样是最新的。这导致了两个矛盾之一。
- 首先,如果投票者和 leaderU 共享相同的上一个日志项,那么 leaderU 的日志必须至少和投票者的日志一样长,所以它的日志包含投票者日志中的每一个条目。这是一个矛盾,因为投票者包含了已提交的条目,而 leaderU 被假定不包含。
- 否则,leaderU 的最后一个log项必须大于投票者的log项。而且,它比T大,因为投票人的最后一个日志项至少是T(它包含来自任期T的提交条目)。创建leaderU 的最后一个日志项的早期leader必须在其日志中包含已提交的条目(根据假设)。然后,根据日志匹配属性,leaderU 的日志也必须包含已提交的条目,这是一个矛盾。
- 这就完成了矛盾。因此,所有大于T的任期的前导项必须包含在项T中提交的来自任期T的所有条目。
- Log Matching Property保证未来的leader也将包含间接提交的条目,例如图8(d)中的索引2。
有了Leader完整性属性( Leader Completeness Property
),我们可以从图3中证明状态机安全属性(State Machine Safety Property
),它指出,如果一个服务器在给定索引上应用了一个日志条目到它的状态机,那么其他服务器将不会对同一个索引应用不同的日志条目。当服务器将一个日志条目应用到它的状态机时,它的日志必须与通过该条目向上的领导者的日志相同,并且该条目必须提交。 现在考虑任意服务器应用给定日志索引的最低任期;日志完备性属性(Log Completeness Property
)保证所有较高任期的前项将存储相同的日志条目,因此在较晚任期中应用索引的服务器将应用相同的值。因此,状态机安全属性成立。
最后,Raft要求服务器按日志索引顺序应用条目。结合状态机安全属性,这意味着所有服务器将以相同的顺序将完全相同的一组日志条目应用到它们的状态机。
2.5.5 追随者和候选崩溃
在此之前,我们一直在关注领导者的失败。追随者和候选崩溃处理起来比领导者崩溃简单得多,它们都以相同的方式处理。如果一个追随者或候选人崩溃,那么未来发送给它的RequestVote
和AppendEntries
RPC将失败。Raft通过无限重试来处理这些失败;如果崩溃的服务器重新启动,则RPC将成功完成。如果服务器在完成RPC后但在响应之前崩溃,那么它将在重新启动后再次接收相同的RPC。Raft rpc是等幂的,所以这不会造成伤害。例如,如果一个追随者收到一个如果AppendEntries请求包含了日志中已经存在的日志项,那么它会忽略新请求中的那些条目。
2.5.6 时间和可用性
我们对Raft的要求之一是安全性不能依赖于时间:系统不能仅仅因为某些事件发生得比预期快或慢而产生错误的结果
。然而,可用性(系统及时响应客户端的能力)必须不可避免地依赖于时间。例如,如果消息交换的时间比服务器崩溃的典型时间长,候选人就不会等待很长到赢得选举;没有一个稳定的领导者,Raft 就无法前进。
领导者选举是Raft中时间最苛刻的方面。只要系统满足以下时间要求,Raft将能够选出并维持一个稳定的领导者:
broadcastTime ≪ electionTimeout ≪ MTBF
在这个不等式中,broadcastTime
是服务器并行发送 RPC 到集群中的每个服务器并接收它们的响应所需的平均时间;electionTimeout
是5.2节中描述的选举超时;MTBF
(Mean Time Between Failure
)是指单个服务器的平均故障间隔时间。广播时间(broadcastTime
)应该比选举暂停时间小一个数量级,以便领导人能够可靠地发送阻止追随者开始选举所需的心跳信息;考虑到选举暂停时采用的随机方法,这种不平等也使得分裂选票的可能性不大。选举超时应该比MTBF
小几个数量级,这样系统才能稳步前进。当领导者崩溃时,系统将在大约选举超时时间内不可用;我们希望这只代表总时间的一小部分。
广播时间和MTBF是底层系统的属性,而选举超时是我们必须选择的。Raft的rpc通常要求接收方将信息持久化到稳定的存储中,因此广播时间可能在0.5ms到20ms之间,这取决于存储技术。因此,选举超时可能在10毫秒到500毫秒之间。典型的服务器MTBF
是几个月或更长时间,这很容易满足时间要求。
2.6 集群成员变更
到目前为止,我们一直假设集群配置(参与共识算法的服务器集)是固定的。在实践中,有时需要更改配置,例如在服务器发生故障时更换服务器或更改复制的程度。虽然这可以通过使整个集群脱机、更新配置文件,然后重新启动集群来实现,但这会使集群在转换期间不可用。此外,如果存在任何手动步骤,则有操作人员出错的风险。为了避免这些问题,我们决定自动化配置更改,并将其合并到Raft共识算法中。
为了确保结构变化机制的安全性,在过渡期间不能出现两位领导人同时当选一届的情况。不幸的是,任何服务器直接从旧配置切换到新配置的方法都是不安全的。一次原子地切换所有服务器是不可能的,因此在转换过程中集群可能分裂为两个独立的主体(参见图10)。
为了确保安全性,配置更改必须使用两阶段方法。有多种方法可以实现这两个阶段。例如,一些系统(例如,[22])使用第一阶段禁用旧配置,使其不能处理客户端请求;然后第二阶段启用新配置。在Raft中,集群首先切换到过渡配置,我们称之为联合共识(joint consensus
);一旦提交了联合共识,系统就会过渡到新的配置。联合共识结合了新旧配置:
- 日志条目将复制到两种配置下的所有服务器。
- 任何来自任一配置的服务器都可以作为领导者。
- 协议(选举和条目提交)要求新旧配置中分别有多数。
联合共识允许各个服务器在不同的时间在不同的配置之间转换,而不影响安全性。此外,联合共识允许集群在整个配置更改期间继续为客户端请求提供服务。
集群配置使用复制日志中的特殊条目进行存储和通信;图11说明了配置更改过程。当leader收到将配置从Cold更改为Cnew的请求时,它将存储配置以进行联合共识(图中Cold,new)作为一个日志条目,并使用前面描述的机制复制该条目。一旦给定的服务器将新的配置条目添加到其日志中,它就会将该配置用于所有未来的决策(服务器总是在其日志中使用最新的配置,而不管该条目是否已提交)。这意味着leader将使用Cold,new规则来确定何时提交Cold,new的日志条目。如果领导者崩溃,新的领导者可以在Cold或Cold,new下选择,这取决于获胜的候选人是否收到Cold,new的。在任何情况下,Cnew 都不能在此期间做出单方面的决定。
一旦Cold,new已经提交,Cold和Cnew 在未经对方批准的情况下做出决定 Leader完备性属性确保只有具有Cold,new日志条目的服务器才能被选为Leader。现在,领导可以安全地创建Cnew描述的日志条目并复制到集群。同样,该配置将在每个服务器上立即生效。
在Cnew规则下提交了新配置后,旧配置就无关紧要了,不在新配置中的服务器可以关闭。如图11所示,在任何时候,Cold和Cnew都不能做出单方面的决定;这保证了安全。
对于重新配置,还有三个问题需要解决。第一个问题是新服务器最初可能不存储任何日志条目。如果在此状态下将它们添加到集群,可能需要相当长的时间才能赶上,在此期间可能无法提交新的日志条目。为了避免可用性缺口,Raft在配置更改之前引入了一个额外的阶段,在这个阶段中,新服务器以无投票成员的身份加入集群(leader将日志条目复制给它们,但它们不被视为多数)。一旦新的服务器跟上了集群的其余部分,重新配置就可以按照上面所述进行。
第二个问题是集群领导者可能不是新配置的一部分。在这种情况下,leader一旦提交了Cnew日志条目,就退出(返回follower状态)。这意味着会有一段时间(当它正在committing Cnew时),leader正在管理一个不包括它自己的集群;它复制日志条目,但不计算自己的多数。leader转换发生在Cnew提交时,因为这是新配置可以独立操作的第一个点(总是可以从Cnew中选择一个leader)。在此之前,可能只有来自Cold的服务器才能被选为领导者。
第三个问题是删除的服务器(那些不在Cnew)可以中断集群。这些服务器将不会接收到心跳,因此它们将超时并开始新的选举。然后他们将发送带有新任期号的RequestVote
RPC,这将导致当前领导者恢复到追随者状态。最终将选出一个新的领导者,但被移除的服务器将再次超时,该过程将重复,导致可用性较差。
为了防止这个问题,当他们认为当前的领导者存在时,服务器忽略RequestVote
rpc 。具体来说,如果服务器在听取当前领导者的最小选举超时内接收到RequestVote
RPC,它不会更新其任期或授予其投票。这不会影响正常的选举,在正常的选举中,每个服务器在开始选举之前至少等待一个最小的选举超时。然而,它有助于避免被移除的服务器造成的中断:如果一个领导者能够让心跳到它的集群,那么它就不会被更大的term number所取代。
2.7 日志压缩(Log compaction
)
Raft的日志在正常操作期间会增长,以包含更多的客户端请求,但在实际系统中,它不能无限制地增长。随着日志的增长,它占用更多的空间,需要更多的时间来重放。如果没有丢弃日志中积累的过时信息的机制,这最终将导致可用性问题。
快照是最简单的压缩方法。在快照中,整个当前系统状态被写入稳定存储上的快照,然后丢弃到该位置的整个日志。在Chubby和ZooKeeper中使用了快照,本节其余部分将介绍Raft中的快照。
压缩的增量方法,如日志清理[36]和日志结构合并树[30,5],也是可能的。它们一次操作的是数据的一小部分,因此它们可以在一段时间内更均匀地分散压缩负载。他们首先选择一个数据区域,该区域积累了许多已删除和覆盖的对象,然后从该区域更紧凑地重写活动对象,并释放该区域。与快照相比,这需要大量额外的机制和复杂性,快照通过始终操作整个数据集来简化问题。虽然日志清理需要对Raft进行修改,但状态机可以使用与快照相同的接口实现 LSM
树。
图12显示了Raft中快照的基本思想。每个服务器独立地拍摄快照,只覆盖其日志中提交的条目。大部分工作包括状态机将其当前状态写入快照。Raft还在快照中包含少量元数据:最后一个包含的索引( last included index
)是快照替换的日志中最后一个条目的索引(状态机应用的最后一个条目),最后一个包含的任期(last included term
)是该条目的任期。保留这些是为了支持对快照之后的第一个日志条目的AppendEntries
一致性检查,因为该条目需要一个先前的日志索引和任期。要启用集群成员关系更改(第6节),快照还在日志中包含了上次包含索引时的最新配置。一旦服务器完成了快照的写入,它可能会删除从最后一个包含的索引开始的所有日志条目,以及任何先前的快照。
虽然服务器通常独立拍摄快照,但领导者必须偶尔向落后的追随者发送快照。当leader已经丢弃了需要发送给follower的下一个日志条目时,就会发生这种情况。幸运的是,这种情况在正常操作中不太可能发生:跟随领导者的追随者已经有了这个条目。然而,异常缓慢的追随者或新服务器加入集群(第6节)则不会。要使这样的追随者更新,方法是由领导者通过网络向其发送快照。
leader使用一个名为InstallSnapshot
的新的RPC向落后太多的follower发送快照;看到图13。当一个追随者收到一个快照与此RPC时,它必须决定如何处理其现有日志项。通常,快照将包含收件人日志中尚未包含的新信息。在这种情况下,追随者丢弃了它的整个日志;它全部被快照所取代,并且可能有未提交的条目与快照冲突。如果跟随者接收到的是描述其日志前缀的快照(由于重传或错误),则快照覆盖的日志条目将被删除,但快照之后的条目仍然有效,必须保留。
这种快照方法背离了Raft的强领导者原则,因为追随者可以在领导者不知情的情况下拍摄快照。然而,我们认为这是合理的。虽然有一个领导者有助于避免在达成共识时产生冲突的决定,但在快照时已经达成共识,所以没有决定冲突。数据仍然只从领导者流向追随者,现在只有追随者可以重新组织他们的数据。
我们考虑了另一种基于领导者的方法,在这种方法中,只有领导者会创建一个快照,然后它会将这个快照发送给它的每个追随者。然而,这有两个缺点。首先,将快照发送给每个追随者会浪费网络带宽并减慢快照进程。每个追随者都已经拥有了生成自己的快照所需的信息,而且服务器从其本地状态生成快照通常比通过网络发送和接收快照便宜得多。其次,领导人的执行将更加复杂。例如,领导者需要向追随者发送快照,同时向他们复制新的日志条目,以免阻塞新的客户端请求。
第二个性能问题是,写入快照可能会花费大量时间,我们不希望这会延迟正常操作。解决方案是使用写时复制技术,这样就可以在不影响正在写入的快照的情况下接受新的更新。例如,使用功能数据结构构建的状态机自然支持这一点。或者,操作系统的写时复制支持(例如,Linux上的fork)可以用来创建整个状态机的内存快照(我们的实现使用这种方法)。
2.8 客户端交互
本节描述客户端如何与Raft交互,包括客户端如何找到集群领导者以及Raft如何支持线性语义( linearizable semantics
)[10]。这些问题适用于所有基于共识的系统,Raft的解决方案与其他系统类似。
Raft的客户端将所有的请求发送给leader。当客户端第一次启动时,它连接到一个随机选择的服务器。如果客户端首选的不是领导者,服务器将拒绝客户端的请求,并提供有关它最近收到的领导者的信息(AppendEntries
请求包括领导者的网络地址)。如果leader崩溃,客户端请求将超时;然后客户端再尝试随机选择的服务器。
Raft的目标是实现可线性化语义(每个操作似乎在调用和响应之间的某个点上立即执行,只执行一次)。然而,正如目前所描述的,Raft可以多次执行一个命令:例如,如果leader在提交日志条目后崩溃,但在响应客户端之前,客户端将用一个新的leader重试该命令,导致它被第二次执行。**解决方案是客户端为每个命令分配唯一的序列号。**然后,状态机跟踪为每个客户机处理的最新序列号,以及相关的响应。如果它接收到序号已经被执行的命令,它将立即响应,而不需要重新执行请求。
只读操作可以在不向日志中写入任何内容的情况下进行处理。然而,如果没有额外的措施,这将会有返回陈旧数据的风险,因为响应请求的leader可能已经被它不知道的更新的leader所取代。可线性读取必须不会返回过时的数据,Raft需要两个额外的预防措施来保证这一点,而不使用日志。首先,leader必须拥有提交条目的最新信息。领导者完整性属性保证leader拥有所有已提交的条目,但在其任期开始时,它可能不知道这些条目是哪些。为了找到答案,它需要提交其term中的一个条目。Raft
通过让每个 leader在任期开始时在日志中提交一个空白的no-op
条目来处理这个问题。其次,领导者必须在处理只读请求之前检查它是否已经被罢免(如果最近的领导者已经当选,它的信息可能是陈旧的)。Raft
通过让leader在响应之前与大多数集群交换心跳消息来解决这个问题。或者,leader可以依赖于心跳机制来提供一种形式的租约[9],但这将依赖于安全的定时(它假设有界的时钟倾斜)。
2.9 实现与评估
我们已经将 Raft 实现为RAMCloud
[33]配置信息存储的复制状态机的一部分,并协助RAMCloud
协调器的故障转移。Raft实现包含大致2000行c++代码,不包括测试、注释或空行。源代码可以免费获得[23]。根据本文的草稿,Raft还有大约25个处于不同开发阶段的独立第三方开源实现[34]。此外,许多公司正在部署基于raft的系统[34]。
本节其余部分使用三个标准评估Raft:可理解性、正确性和性能。
2.9.1 可理解性
为了衡量Raft相对于Paxos的可理解性,我们在斯坦福大学高级操作系统课程和加州大学伯克利分校分布式计算课程的高年级本科生和研究生中进行了一项实验研究。我们录制了一个Raft的视频讲座和另一个Paxos的视频讲座,并制作了相应的小测验。Raft讲座涵盖了本文的内容,除了日志压缩;Paxos讲座涵盖了足够的材料来创建一个等效的复制状态机,包括单法令的Paxos,多法令的Paxos,重新配置,以及实践中需要的一些优化(如领导人选举)。测验测试了学生对算法的基本理解,也要求学生对极端情况进行推理。每个学生看一个视频,做相应的测试,看第二个视频,做第二个测试。大约一半的参与者先做了Paxos部分,另一半先做了Raft部分,以解释从研究的第一部分中获得的表现和经验的个体差异。我们比较了参与者在每个测试中的分数,以确定参与者是否对Raft有更好的理解。
我们还在测试后对参与者进行了调查,看看他们觉得哪种算法更容易实现或解释;这些结果如图15所示。绝大多数参与者报告Raft更容易实现和解释(每个问题41个问题中有33个)。然而,这些自我报告的感觉可能不如参与者的测试分数可靠,参与者可能会因为我们假设Raft更容易理解而产生偏见。
2.9.2 正确性
我们已经为第5节中描述的共识机制开发了正式规范和安全性证明。正式规范[31]使得图2中总结的信息使用TLA+规范语言[17]。它大约有400行长,作为证明的主题。对于任何实现Raft的人来说,它本身也很有用。我们使用TLA证明系统[7]机械地证明了日志完整性属性。然而,这个证明依赖于没有机械检查的不变量(例如,我们还没有证明该规范的类型安全性)。此外,我们还编写了状态机安全属性的非正式证明[31],它是完整的(仅依赖于规范)和相对精确的(大约3500字长)。
2.9.3 性能
Raft的性能类似于其他共识算法,如Paxos。性能最重要的情况是一个已建立的leader复制新的日志条目的时机。Raft使用最少的消息数量(从leader到半个集群的一次往返)来实现这一点。它也有可能进一步提高Raft的性能
。例如,它很容易支持批处理和流水线请求,以获得更高的吞吐量和更低的延迟。对于其他算法,文献中已经提出了各种优化方法;其中许多都可以应用到Raft中,但我们把这些留给未来的工作。
我们使用我们的Raft
实现来衡量 Raft 领导者选举算法的性能,并回答了两个问题。首先,选举进程是否会迅速趋同?其次,领导者崩溃后可以达到的最小停机时间是多少?
为了度量领导者的选举,我们反复使一个由五台服务器组成的集群的领导者崩溃,并计算检测崩溃和选举新领导者所需的时间(参见图16)。为了产生最坏的情况,每个试验中的服务器都有不同的日志长度,因此一些候选人没有资格成为领导者。此外,为了鼓励分裂投票,我们的测试脚本在终止其进程之前触发了来自领导者的心跳RPC的同步广播(这近似于leader在崩溃之前复制一个新日志条目的行为)。leader在其心跳间隔内均匀随机崩溃,该心跳间隔是所有测试的最小选举超时的一半。因此,最小的可能停机时间大约是最小选举超时时间的一半。
图16中的顶部图表显示,选举超时中的少量随机化足以避免选举中的分裂选票。在没有随机性的情况下,在我们的测试中,由于许多选票分裂,领导人选举的时间总是超过10秒。仅添加5毫秒的随机性就能显著提高效率,从而使停机时间中值达到287ms。使用更多的随机性可以改善最坏情况的行为:在50毫秒的随机性下,最坏情况的完成时间(超过1000次试验)是513毫秒。
图16中的底部图表显示,可以通过减少选举超时来减少停机时间。选举超时为12-24ms,平均只需要35ms就可以选出一个领导人(最长的试验耗时152ms)。但是,将超时时间降低到超过这个值是违反规定的Raft的时间要求:领导者很难在其他服务器开始新的选举之前广播心跳。这可能导致不必要的领导变更和降低整个系统可用性。我们建议使用保守的选举超时,如150-300ms;这样的超时不太可能导致不必要的领导者变化,并且仍将提供良好的可用性。
BFT(Byzantine Fault Tolerance
)