Lec 20 复制状态机
阅读资料:
论文: In Search of an Understandable Consensus Algorithm (raft.github.io), 2014
在阅读论文时,快速浏览第5.4.3节、第7节、第9.1节和第9.2节;前4节提供了Raft的背景和动机。第5和第6节是主要的技术性章节。图2是一份很好的参考资料,在你阅读论文或者结束后可以回头再看。不要试图在转到论文的第5页之前就死记硬背整张表;可以跳过它,在阅读过程中或者最后再回来看。
辅助理解工具:A visualization of Raft
思考题
两个副本不一致意味着什么?两个副本可能如何变得不一致?
考虑一个设定,其中有一个协调者、一个主副本和一个备份副本。
- 这种设置解决了哪些问题?(例如,在这种系统中,是否有可能防止数据变得不一致?)
- 这种设置不能解决哪些问题?
- 网络分区是什么意思?
- 网络分区与我们过去考虑的网络故障有何不同?
复制状态机
视图服务器的作用是什么?
副本如何学习自己是主副本还是备份副本?
在这样的系统中,写操作是如何工作的?
- 系统的哪些模块进行通信,通信顺序是什么样的?
如果主副本失败会发生什么?
如果网络分区阻止主副本与视图服务器通信会发生什么?
RSM(复制状态机)提供什么样的一致性?
- 所有系统都需要这样的一致性吗?
复制状态机问题(replicated state machine,RSM)
- 将2PC扩展到多服务器和多协调器会出现啥问题?
- 在使用主备机制时,网络分区会导致什么问题?
- RSM中,每个组件的角色是什么? 协调器、视图服务器、主服务器和备份服务器
- 当发生故障(服务器故障、网络分区)时,RSM中每个组件的角色是什么? 包括当一个组件发现了失败,需要对这个失败采取的措施;失败被处理的时候会发生什么等等。
- 为什么我们允许视图服务器是集中式组件,但没有将协调器的角色集中化?
Raft
Raft如何处理以下三种类型的故障?:领导者故障、候选者或者跟随者故障,以及网络分区(网络分区意味着集群的一部分无法与另一部分的任何机器通信)
你认为Raft好理解吗?你的看法
复制状态机
视图服务器的作用是什么?
复制品如何学习自己是主复制品还是备份复制品?
在这样的系统中,写操作是如何工作的?
- 系统的哪些模块进行通信,通信顺序是什么样的?
如果主复制品失败会发生什么?
如果网络分区阻止主复制品与视图服务器通信会发生什么?
领导者选举
- 每个候选者为自己投票并广播其请求;请求中包含什么?
- 为了让一台机器响应投票请求,需要发生什么?
- 一台机器如何赢得选举?
- 如果没有人赢得选举会发生什么?
- 领导者如何继续保持领导地位?
失败处理
- 领导者失败时会发生什么?
- 跟随者失败时会发生什么?
- 候选者失败时会发生什么?
- 如果存在网络分区会发生什么?
复制技术
为了提高可用性,我们在两个服务器上复制数据。
最简单方法: 没啥特殊的,只是在两个副本上复制数据。

消息顺序可能导致副本间不一致。(此处我们假设允许这两个写操作并发执行,以演示消息顺序如何引发不一致问题)

尝试方案2:指定一个副本作为主副本(primary replica),并部署协调器(coordinators)来协助管理故障

在这个方案中, 客户端只跟协调者通信,而非副本。协调者C向主副本发送请求,主副本选择操作的顺序,确定所有非确定性的值;主副本只有在得到备份已经全部更新后才会向协调者恢复ACK。
如果主副本故障了,协调者会转向备份(协调者如何联系其他副本),理想情况下,S1 最终会恢复,或者我们会用其他机器替代它,系统就能重新拥有主备两个节点。但在本例中,我们只关注如何正确切换到备份服务器。

假设所有机器都保持正常运行,但发生了网络分区(network partition),导致网络实际上被分割为两部分。网络分区(network partition)意味着:位于分割线同侧的机器可以相互通信,但无法与另一侧的机器进行通信,如下图所示。

因为这两个副本都认为他们是主副本,我们的数据将会变成不一致的。
复制状态机
方案3: 采用视图服务器(View Server)来判定哪个副本作为主副本(Primary Replica),以期解决网络分区(Network Partitions)问题。
首先视图服务器会保持一个表,用来维护一组视图。、向主备节点(Primary/Backup)通知其角色分配。

协调者询问视图服务器哪个副本为主副本

接下来的操作与之前一致,协调者与主副本联系,主副本将更新发送给backup,并取得ACK,接着再发送ACK协调者。

在这个期间,主副本和备份副本会定期ping视图服务器,因此视图服务器可以发现故障。
在我们的架构中,整个系统只有一个视图服务器(View Server),却可以存在多个协调器(Coordinators)。当发生故障时(例如前文所述的各类情况),采用单一视图服务器可能带来哪些优势?
Solution: 一致性强,集中视角,防止脑裂 □
如果发生主副本故障,协调者无法获得响应,此时协调者接下来会询问视图服务器哪个是主副本,视图服务器会用S2响应

请注意,此时系统中已不存在备用节点。虽然我们期望最终能恢复S1或引入新机器作为备用节点,但在本示例中,我们仅关注如何安全地将S2提升为新的主节点。
那如果网络分区导致S1无法与视图服务器通信? 从某种意义上说,这是最糟糕的网络分区情况:视图服务器(VS)会判定S1已故障(因而将S2切换为备用节点),而实际上S1仍能与除VS外的所有节点保持通信。

S1节点行为逻辑:若接收到协调者C的请求,会以主副本身份进行处理,认定S2为备用节点。而对于S2节点若接收到C的请求则会拒绝请求(自认备用节点身份,不直接与客户端通信)
随着网络分区持续,VS会重新选主,S2成为VS视图中的主节点(S1自认为也是主节点)。S1节点行为:接受到S1的请求后, 因无法获取S2的ACK确认,因为其拒绝处理请求。而S2的节点行为,将以主节点身份响应(符合视图服务器的预期拓扑)

一旦S1能够与VS再次取得通信,VS会响应通知S1,它不再当前的视图。核心规则:若某机器在视图n中担任主节点(primary),则其在视图n-1中必须曾是主节点或备份节点(backup)——视图1除外(系统初始化阶段不受此限),防止凭空出现主节点。
如果视图服务器故障了怎么办?
Solution: 视图服务器故障会导致新主无法选出,协调器和副本无法更新视图,不能应对分区恢复(副本重连)。
怎么办? 方法1,将视图服务器变成高可用服务,用 Paxos / Raft / ZAB 等共识协议实现视图服务器。□
复制状态机和视图服务器的关系是什么?
Solution:实际上,方案3可以看作是复制状态机的方法。视图服务器作为RSM的一部分或者是辅助机制来使用理解。也就是说,看起来是一个独立组件,实际上会集成到共识算法Raft、Paxos里面。
视图服务器的作用是什么?
Solution: 主节点选举、成员管理、故障检测以及状态广播(将视图发送给客户端和副本节点)
副本如何知道自己角色?
Solution:副本通过“视图服务器”来学习自己的角色:
- 副本定期向视图服务器发送心跳或注册请求;
- 视图服务器根据心跳情况决定新的主、副本;
- 视图变化(View Change)时,视图服务器会:
- 通知新主:你现在是主;
- 通知其他副本:你现在是备份,主是 X;
- 副本根据视图更新自己的角色。
复制状态机提供了什么样的一致性?所有系统都需要这种一致性吗?
Solution: RSM提供了线性一致性(Linearizability),也称为强一致性(Strong Consistency)。复制状态机模型的核心思想是:所有副本从相同的初始状态出发,以相同的顺序执行相同的操作,就会始终保持状态一致。 第二个问题回答,不需要。线性一致性虽然最直观、最容易编程,但它的代价也最大
论文阅读:Raft共识算法
思考题
领导者选举
- 每个候选者为自己投票并广播其请求;请求中包含什么?
- 为了让一台机器响应投票请求,需要发生什么?
- 一台机器如何赢得选举?
- 如果没有人赢得选举会发生什么?
- 领导者如何继续保持领导地位?
失败处理
- 领导者失败时会发生什么?
- 跟随者失败时会发生什么?
- 候选者失败时会发生什么?
- 如果存在网络分区会发生什么?
在阅读论文时,快速浏览第5.4.3节、第7节、第9.1节和第9.2节。
前四节提供了Raft的背景和动机。第五和第六节是主要的技术性章节。图2是一份很好的参考资料,在你阅读论文或者结束后可以回头再看。不要试图在转到论文的第5页之前就死记硬背整张表;可以跳过它,在阅读过程中或者最后再回来看。
摘要
Raft是一种用于管理复制日志的共识算法。它生成的结果等同于(多)Paxos算法,并且具有与Paxos相同的效率,但其结构与Paxos不同;这使得Raft比Paxos更易于理解,也更容易构建实际系统。为便于理解,Raft分离出几个关键要素分离,如领导者选举、日志复制和安全性,并通过增强一致性来减少需要考虑的状态数量。Raft还包含一种新的机制,用于更改集群成员,该机制利用重叠多数派来确保安全性。
1. 引言
共识算法让一组机器作为一个连贯的整体协同工作,即便其中部分成员发生故障也能继续运作。正因为如此,共识算法在构建可靠的大规模软件系统中扮演着关键角色。过去十年间,Paxos算法在共识算法的讨论中占据了主导地位:大多数共识的实现都基于Paxos或受其影响。不幸的是,尽管有许多尝试试图使其更易理解,Paxos依然非常难以掌握。
我们的方法与众不同,主要目标在于可理解性。在设计Raft时,我们采用了特定的技术来提升可理解性,包括解耦合(Raft将领导者选举、日志复制和安全性分开)和状态空间的减少(相比Paxos,Raft减少了不确定性和服务器之间不一致的情况)。
Raft在许多方面与现有共识算法类似,但它具有一些创新特点:
- 强领导者(strong leader): Raft采用比其他共识算法更强的领导机制。例如,日志条目仅从领导者流向其他服务器。这简化了对复制日志的管理,并使Raft更易于理解
- 领导者选举(leader election):Raft使用随机计时器来选举领导者。这只在任何共识算法所需的心跳基础上增加了少量机制,却能简单而迅速地解决冲突。
- 成员变更:Raft更换集群服务器集合的机制采用了一种新的联合共识方法,在配置变更期间两个不同配置的多数派相互重叠。这允许集群在配置变更过程中继续正常操作。
本文的剩余部分将介绍复制状态机问题(第2节),讨论Paxos的优缺点(第3节),描述我们在提升可理解性方面的一般方法(第4节),呈现Raft共识算法(第5-8节),评估Raft(第9节),并讨论相关工作(第10节)。
2. 复制状态机
有复制状态机(Replicated state machines)的地方就有共识算法。在这种方法中,一组服务器上的状态机会计算出同一个状态的副本,即它们的状态是一致的。即便其中一些服务器宕机,其它服务器仍然可以继续运行。复制状态机用于解决分布式系统中的多种容错问题。例如,许多拥有单一集群领导者的大规模系统,如GFS、HDFS和RAMCloud,通常使用一个独立的复制状态机来管理领导者选举并存储必须在领导者崩溃时仍需保存的配置信息。Google Chubby和ZooKeeper就是复制状态机的示例。

图一是复制状态机架构。共识算法管理一个包含来自客户端的状态机命令的复制日志。状态机从这些日志中处理相同顺序的命令,因此会产生相同的输出。
复制状态机通常通过复制日志实现,如图1所示。每台服务器存储包含一系列命令的日志,状态机按顺序执行这些命令。每个日志包含相同顺序的相同命令,因此每个状态机处理相同的命令序列。由于状态机是确定性的,它们计算相同的状态和相同的输出序列。
保持复制日志一致性是共识算法的任务。服务器上的共识模块从客户端接收命令并将其添加到日志中。它与其他服务器上的共识模块通信,以确保每个日志最终包含相同的请求,并按相同的顺序排列,即使有些服务器出现故障也是如此。一旦命令被正确复制,每个服务器的状态机按照日志顺序处理它们,并将输出返回给客户端。结果是,这些服务器看起来像一个高可靠的单一状态机。
用于实际系统的共识算法通常具有以下特性:
- 它们确保安全性(在任何情况下都不返回错误结果),即使在非拜占庭条件下(包括网络延迟、分区、数据包丢失、重复和乱序)也是如此。
- 只要有多数服务器正常运行并可以互相通信以及与客户端通信,系统就是完全功能性的(即可用的)。因此,典型的五台服务器的集群可以容忍其中任何两台服务器的故障。假设服务器故障为停止故障;它们可能在从稳定存储中恢复状态后重新加入集群。
- 它们不依赖时间来确保一致性:故障时钟和极端消息延迟在最坏情况下只会影响系统的可用性。
- 日志一致性的问题主要体现在故障时钟和极端消息延迟上,这在最坏情况下可能会导致可用性问题。
- 在常见情况下,一条命令在大多数集群响应单轮远程过程调用后即可完成;少数缓慢的服务器不会影响整体系统性能。
3. Paxos 有什么问题
在过去的十年里,Leslie Lamport 的 Paxos 协议几乎成了“共识”的代名词:这是课程中最常教授的协议,大多数共识实现也以此为起点。Paxos 首先定义了一个能够在单个决策(如一个复制日志条目)上达成一致的协议,我们将其称为single-decree Paxos。然后,Paxos 将该协议的多个实例结合起来,以实现一系列决策(即日志),这就是multi-Paxos。Paxos 确保了安全性和活性,并支持集群成员的更换。其正确性已经得到证明,且在常规情况下效率很高。
然而,Paxos 存在两个显著的缺陷。首先,Paxos 非常难以理解。完整的解释出了名的晦涩,只有少数人能够理解它,且需要付出巨大的努力。我们假设 Paxos 的晦涩源于其选择单决策作为基础。single-decree Paxos 是一种高度密集且微妙的设计,它被分为两个阶段,这两个阶段既没有简单的直观解释,也不能独立理解。因此,很难对该协议为何有效形成直观理解。而Multi-Paxos 的组合规则则增加了显著的复杂性和微妙性。我们认为,对多个决策(即日志而非单条记录)达成共识的整体问题可以以更直接、更清晰的方式进行分解。
Paxos 的第二个问题是,它未能为构建实用实现提供良好基础。原因之一是尚未有被广泛认可的Multi-Paxos 算法。Lamport 的描述主要集中在single-decree Paxos 上,他对Multi-Paxos 提出了可能的方法,但遗漏了许多细节。诸如 Chubby 之类的系统实现了类似 Paxos 的算法,但大多数情况下其细节尚未公开。
此外,Paxos 的架构不适合构建实用系统;这是单决策分解的另一个后果。例如,选择一组日志条目后再将它们整合成一个顺序日志几乎没有什么好处,反而增加了复杂性。设计一个围绕日志的系统会更简单且更高效,日志的新条目按顺序依次追加。另外一个问题是,Paxos 在其核心使用对称的点对点方法(尽管最终提出了一种较弱的领导者形式来优化性能)。在只做单一决策的简化场景中,这种方法有意义,但很少有实际系统采用这种方法。如果需要做一系列决策,那么先选举一个领导者,然后由该领导者协调决策会更简单、更快速。
4. 为了易于理解而设计
我们运用了两种普遍适用的技术。第一种技术是广为人知的“问题分解”方法:尽可能地将问题分解为可独立解决、解释和理解的部分。例如,在 Raft 中,我们将领导者选举、日志复制、安全性和成员变更分开处理。
我们的第二种方法是通过减少需要考虑的状态数量来简化状态空间,使系统更具连贯性,并尽可能消除非确定性。具体来说,不允许日志中出现空洞,Raft 还限制了日志之间可能出现的不一致情况。尽管我们在大多数情况下尝试消除非确定性,但在某些情况下非确定性实际上有助于理解,特别是随机化方法引入了非确定性,但它们通过以相似方式处理所有可能的选择来减少状态空间(“选择任意一个都可以”)。我们在 Raft 的领导者选举算法中使用了随机化以简化设计。
5. Raft 共识算法
Raft是一种用于管理复制日志的算法,对算法进行了简明总结。算法的关键特性:
- 选举安全性:在一个给定的任期内,最多只能选举出一个领导者。§5.2
- 领导者追加日志:领导者从不覆盖或删除日志中的条目;它只会追加新的条目。§5.3
- 日志匹配:如果两个日志在相同的索引和任期包含一个条目,那么在该索引之前的所有条目都必须相同。§5.3
- 领导者完整性:如果一个日志条目在给定任期内被提交,那么该条目将会出现在所有更高编号任期的领导者日志中。§5.4
- 状态机安全性:如果一个服务器已经将某个日志条目应用到其状态机的某个索引位置,那么其他任何服务器都不会对该索引应用不同的日志条目。§5.4.3
这些将在各个小节详细说明。

Raft 通过首先选出一个特别的领导者来实现共识,然后将管理复制日志的全部责任赋予该领导者。领导者从客户端接受日志条目,将他们复制到其他服务器,并告知服务器何时可以安全地从日志条目用到其状态机。领导者的存在简化了复制日志的管理。例如,领导者可以在不咨询其他服务器的情况下决定在日志中的新条目位置,数据也以简单的方式从领导者流向其他服务器。如果领导者出现故障或与其他服务器断开连接,则会选举出新的领导者。
在介绍完共识算法后,本节还将讨论系统的可用性问题及时间在系统中的作用。
5.1 Raft基本原理
一个 Raft 集群由多个服务器组成;通常为5个,这样可以容忍3个服务器出现故障。任何时刻,每台服务器都处于三种状态之一:领导者(leader)、跟随者(followr)或候选者(candidate)。在正常操作中,系统中只有一个领导者,其他所有服务器都是跟随者。跟随者是被动的:它们不会主动发出请求,只是响应来自领导者和候选者的请求。领导者处理所有客户端请求(如果客户端联系到跟随者,跟随者会将其重定向到领导者)。第三种状态是候选者,用于选举新的领导者,详见第 5.2 节。图 4 显示了状态及其转换情况,这些转换将在下面讨论。

- 图4:服务器状态。追随者仅响应来自其他服务器的请求。如果追随者没有接收到任何通信,它会变成候选者并发起选举。获得来自全体集群多数服务器投票的候选者将成为新的领导者。领导者通常会一直运行,直到发生故障
Raft 将时间划分为任意长度的任期(term),如图 5 所示。任期用连续整数编号。每个任期开始时都会进行一次选举(election),一个或多个候选者试图成为领导者,如第 5.2 节所述。如果候选人赢得选举,它将在该任期内担任领导者。在某些情况下,选举会导致投票平局。在某些情况下,选举会导致投票分裂。在这种情况下,任期将结束且没有选出领导者,很快将开始一个新的任期(并进行新的选举)。Raft确保在一个给定的任期内最多只有一个领导者。
不同的服务器可能在不同的时间观察到任期之间的转换,在某些情况下,某个服务器可能没有观察到选举或整个任期。任期在Raft中充当了一个逻辑时钟[14,兰伯特时钟],它们允许服务器检测过时的信息,例如过时的领导者。每个服务器存储一个当前任期号,该号随时间单调递增。在服务器之间的通信中会交换当前任期号;如果某个服务器的当前任期小于另一个服务器的任期,它将更新为较大的那个任期号。如果某个候选人或领导者发现其任期已经过时,它会立即回退到跟随者状态。如果服务器收到一个过时的任期号请求,它将拒绝该请求。

- 图5: 时间被划分为若干个周期,每个周期以一次选举开始。选举成功后,单个领导者将管理集群,直到该周期结束。如果选举失败,则该周期结束时没有选出领导者。不同服务器上可以在不同时间观察到周期之间的转换。
Raft服务器通过远程过程调用(RPCs)进行通信,基本的共识算法只需要两种类型的RPC。RequestVote RPC由候选人在选举期间发起(见第5.2节),AppendEntries RPC由领导者发起,用于复制日志条目并提供心跳信号(见第5.3节)。第7节增加了第三种RPC,用于在服务器之间传输快照。如果服务器未能及时收到响应,它会重试RPC,并且它们会并行发起RPC以获得最佳性能。
5.2 领导者选举
Raft 使用心跳机制来触发领导者选举。当服务器启动时,它们首先处于跟随者状态。只要服务器接收到来自领导者或候选人的有效RPC,它将保持在跟随者状态。领导者会定期发送心跳信号(没有日志条目的AppendEntries RPC)给所有跟随者,以维持其权威。如果一个跟随者在一定时间内(称为选举超时)没有接收到任何通信,它就假设没有有效的领导者,并开始选举以选择一个新领导者。
要开始选举,跟随者会增加当前的任期号,并转变为候选人状态。它随后为自己投票,并并行向集群中的其他每个服务器发送RequestVote RPC。候选人会保持在这一状态,直到发生以下三种情况之一:(a)它赢得选举,(b)另一个服务器成为领导者,或者(c)一段时间过去了,未能选出领导者。以下段落分别讨论这些结果。
候选人如果收到集群中多数服务器的选票,且这些选票属于同一任期,它就赢得了选举。每个服务器在一个任期内最多为一个候选人投票,且按先到先得的原则(注意:第5.4节对投票有额外限制)。多数规则确保在特定任期内最多只有一个候选人能够获胜(选举安全性属性)。一旦候选人赢得选举,它就成为领导者。随后它会发送心跳消息给所有其他服务器,确立自己的权威并防止新一轮选举。
在等待投票期间,候选人可能会收到来自其他服务器的AppendEntries RPC,声称自己是领导者。如果领导者的任期(RPC中包含的)至少与候选人当前的任期相同,那么候选人会承认领导者是合法的,并转为跟随者状态。如果RPC中的任期比候选人当前的任期要小,候选人则会拒绝该RPC,并继续保持候选人状态。
第三种可能的结果是候选人既没有赢得选举,也没有输掉选举:如果多个跟随者同时变成候选人,投票可能会分裂,导致没有任何候选人获得多数选票。当这种情况发生时,每个候选人都会超时,并通过增加任期号来发起新一轮选举,并再次发起RequestVote RPC。然而,如果没有额外的措施,选票分裂可能会无限期地重复。
Raft使用随机化的选举超时来确保选票分裂很少发生,并且可以迅速解决。为了防止选举分裂,选举超时从一个固定的时间区间中随机选择(例如,150-300毫秒)。这样可以分散服务器,使得在大多数情况下,只有一个服务器会超时;它会赢得选举,并在其他服务器超时之前发送心跳信号。相同的机制也用于处理选举分裂。每个候选人会在选举开始时重新设置其随机化选举超时,并等待该超时结束后才开始下一轮选举;这样减少了新一轮选举中出现选举分裂的可能性。第9.3节展示了这种方法如何快速选举出领导者。
选举是我们在设计选择之间如何受可理解性指导的一个例子。最初我们计划使用排名系统:每个候选人分配一个唯一的排名,用来在竞争者之间做选择。如果一个候选人发现另一个候选人排名更高,它会回到跟随者状态,以便更高排名的候选人能更容易赢得下次选举。我们发现这种方法在可用性上存在微妙问题(低排名的服务器可能需要超时并重新成为候选人,如果一个更高排名的服务器失败,但如果它做得太早,它可能会重置选举进度)。我们多次调整算法,但每次调整后都会出现新的角落案例。最终我们得出结论,随机重试的方法更直观且易于理解。
5.3 日志复制
一旦领导者选举完成,它开始为客户端请求提供服务。每个客户端请求包含一个要由复制的状态机执行的命令。领导者将命令附加到自己的日志中作为一个新的条目,然后并行地向集群中的每个其他服务器发送AppendEntries RPC以复制该条目。当条目被安全复制后(如下所述),领导者将该条目应用到它的状态机并将执行结果返回给客户端。如果跟随者崩溃或运行缓慢,或如果网络数据包丢失,领导者会无限期地重试AppendEntries RPC(即使它已经响应了客户端),直到所有跟随者最终存储所有日志条目。
日志的组织方式如图6所示。每个日志条目存储
- 一个状态机命令
- 该条目被领导者接受时的任期号,用于检测日志之间的不一致性,并确保图3中的一些属性
- 还有整数索引,用于标识其在日志中的位置

- 图6:日志由条目组成,这些条目按顺序编号。每个条目包含它创建时的任期号(每个框中的数字)和一个状态机命令。当某个条目可以安全地应用到状态机时,该条目被视为已提交。
领导者决定何时可以安全地将日志条目应用到状态机;这样的条目被称为已提交(Commited)的条目。Raft 保证已提交的条目是持久的,并且最终会被所有可用的状态机执行。日志条目一旦在领导者创建该条目的服务器上被复制到大多数服务器(例如,图6中的条目7),则该条目被认为已提交。此时,领导者日志中的所有前序条目,包括由先前领导者创建的条目,也被提交。第5.4节讨论了在领导者变更后应用此规则时的一些细节,并且还展示了这种提交定义是安全的。领导者会跟踪它知道的最高已提交索引,并将该索引包含在未来的AppendEntries RPC中(包括心跳),以便其他服务器最终得知。一旦跟随者得知某个日志条目已提交,它会按照日志顺序将该条目应用到本地的状态机。
我们设计Raft日志机制的目的是在不同服务器之间保持日志的一致性。这不仅简化了系统的行为并使其更加可预测,而且是确保安全性的一个重要组成部分。Raft维护以下属性,它们共同构成了图3中的日志匹配属性:
- 如果两个不同日志中的条目具有相同的索引和任期,则它们存储相同的命令。
- 如果两个不同日志中的条目具有相同的索引和任期,则它们在所有前面的条目中完全一致。
第一个属性是基于这样事实——在给定任期、给定日志索引时,最多只能创建一个条目(entry),并且entry从不更改其在日志中的位置。第二个属性基于 由AppendEntries 提供的简单一致性检查来保证,当发送AppendEntries RPC时,它会带上新日志条目之前的那个条目的索引和任期。如果跟随者的日志中没有找到具有相同索引和任期的条目,则会拒绝这些新条目。这种一致性检查充当了归纳步骤:日志的初始空状态满足日志匹配属性,而一致性检查在扩展日志时保持该属性。因此,每当AppendEntries成功返回时,领导者就知道跟随者的日志在新条目之前是与领导者的日志一致的。
在正常操作中,领导者和跟随者的日志保持一致,因此AppendEntries一致性检查通常不会失败。然而,领导者崩溃可能导致日志不一致(旧的领导者可能没有完全复制其日志中的所有条目)。这些不一致性可能会在一系列的领导者和跟随者崩溃后积累。图7展示了跟随者的日志可能与新领导者的日志不一致的情况。一个跟随者可能缺少领导者日志中存在的条目,可能有额外的条目,或者两者都有。日志中缺失和多余的条目可能跨越多个任期。

- 图7:当最上面的领导者上台时,可能会在跟随者的日志中发生以下任何一种场景(a-f)。每个框代表一个日志条目;框中的数字是该条目的任期。一个跟随者可能缺少条目(a-b),可能有额外的未提交条目(c-d),或者两者都有(e-f)。例如,场景(f)可能发生在这样一种情况:该服务器在任期2时曾是领导者,向日志中添加了几个条目,但在提交之前崩溃了;它快速重启,成为任期3的领导者,并向日志中添加了更多条目;在任期2或任期3的任何条目都未提交之前,该服务器再次崩溃,并且在多个任期内保持宕机状态。
在Raft中,领导者通过强制跟随者的日志与自己的日志一致来处理不一致性。这意味着,跟随者日志中的冲突条目将被领导者日志中的条目覆盖。第5.4节将展示,当与另一个限制结合时,这种做法是安全的。为了使跟随者的日志与自己的日志一致,领导者必须找到两个日志中最新的匹配条目,删除跟随者日志中该点之后的所有条目,并将该点之后的领导者的所有条目发送给跟随者。所有这些操作都发生在AppendEntries RPC执行一致性检查时。领导者会为每一个跟随者维护一个nextIndex,它表示领导者将发送给该跟随者的下一个日志条目的索引。当领导者首次成为领导时,它会将所有nextIndex值初始化为其日志中最后一个条目之后的索引(图7中的11)。如果一个跟随者的日志与领导者的日志不一致,AppendEntries一致性检查将在下一个AppendEntries RPC中失败。在拒绝之后,领导者会减少nextIndex并重试AppendEntries RPC。最终,nextIndex将达到一个点,在这个点上,领导者和跟随者的日志匹配。当发生这种情况时,AppendEntries会成功,这会删除跟随者日志中的冲突条目,并附加领导者日志中的条目(如果有的话)。一旦AppendEntries成功,跟随者的日志将与领导者的日志一致,并且在当前任期内将保持一致。
如果需要,可以对协议进行优化,以减少被拒绝的AppendEntries RPC的数量。例如,在拒绝AppendEntries请求时,跟随者可以包含冲突条目的任期和该任期的第一个索引。有了这些信息,领导者可以减少nextIndex,跳过该任期中的所有冲突条目;对于每个有冲突条目的任期,只需要一个AppendEntries RPC,而不是每个条目一个RPC。实际上,我们认为这种优化可能没有必要,因为失败发生的频率较低,并且出现许多不一致条目的情况也不太可能。
通过这个机制,领导者在上任时不需要采取任何特殊措施来恢复日志一致性。它只需要开始正常操作,日志会自动在AppendEntries一致性检查的响应中趋于一致。领导者永远不会覆盖或删除自己日志中的条目(图3中的领导者追加-only属性)。
这种日志复制机制展示了第2节中描述的理想共识属性:只要大多数服务器正常运行,Raft就可以接受、复制并应用新的日志条目;在正常情况下,新的条目可以通过一次RPC回合复制到集群的大多数服务器;一个慢速的跟随者不会影响性能。
5.4 安全性
前面的章节描述了 Raft 如何选举领导者和复制日志条目。然而,迄今为止所描述的机制不足以确保每个状态机按相同的顺序执行完全相同的命令。例如,某个跟随者可能在领导者提交多个日志条目时不可用,之后它可能被选举为领导者,并用新的条目覆盖这些条目;这样不同的状态机可能执行不同的命令序列。
本节通过对哪些服务器可以当选为领导者的限制,完善了 Raft 算法。这一限制确保了任何给定任期的领导者都包含了先前任期中已提交的所有条目(“领导者完整性”属性)。在这个选举限制的基础上,我们进一步精确了提交规则。最后,我们提供了“领导者完整性”属性的证明概要,并展示了它如何确保复制状态机的正确行为。
5.4.1 选举限制
在任何基于领导者的共识算法中,领导者必须最终存储所有已提交的日志条目。在某些共识算法中,如 Viewstamped Replication [22],即使领导者最初不包含所有已提交的条目,也可以选举出领导者。这些算法包含额外的机制,用于识别缺失的条目并在选举过程期间或之后将其传输到新领导者。但这种方法会带来相当大的额外机制和复杂性。Raft 使用了一种更简单的方法,保证每个新领导者从选举开始时就包含所有先前任期中已提交的条目,无需将这些条目传输给领导者。这意味着日志条目只在一个方向上传输,从领导者到跟随者,领导者从不覆盖其日志中已有的条目。
Raft 使用投票过程来防止候选人在其日志不包含所有已提交条目的情况下当选为领导者。候选人必须联系集群的大多数服务器才能当选,这意味着每个已提交的条目必须在这些服务器中的至少一个上存在。如果候选人的日志至少与该多数中的任何其他日志一样“最新(up-to-date)”(下面将精确地定义“最新”),那么它就会包含所有已提交的条目。RequestVote RPC 实现了这一限制:RPC 包含有关候选人日志的信息,如果投票者的日志比候选人的日志更新,则会拒绝投票。
Raft 通过比较日志中最后条目的索引和任期来判断两个日志哪个更“最新”。如果两个日志的最后条目的任期不同,那么包含更晚任期的日志就是“更新”的。如果最后的条目在同一任期,则任期较长的日志更“up-to-date”。
对于任期不同,更晚的任期日志更up-to-date
示例:
日志 A 的最后一条是
(index=5, term=3)日志 B 的最后一条是
(index=4, term=4)虽然A长,但B最后一条term更新,所以B更up-to-date。
如果两个日志最后一条的 term 相同,这说明它们最后一次参与选举的时间差不多,因为每次选举都会递增 term。
日志项位置 领导者 A 的日志 候选人 B 的日志 index=1 term=1 term=1 index=2 term=2 term=2 index=3 term=3 term=3 index=4 term=3 ——(没有) 它们最后的 term 都是 3,但 B 少了一条日志,所以 A 更 up-to-date,B 会被拒绝
5.4.2 提交前任期的条目
如第 5.3 节所述,领导者在将当前任期的条目复制到大多数服务器后知道该条目已提交。如果领导者在提交条目之前崩溃,未来的领导者会尝试完成条目的复制。然而,领导者不能立即得出结论,即一旦某个来自前任期的条目存储在大多数服务器上,该条目就被提交。图 8 展示了一个情境,其中一个旧的日志条目已经存储在大多数服务器上,但仍然可能被未来的领导者覆盖。
为了消除像图 8 中的这种问题,Raft 从不通过“多数副本存在”来提交旧的日志条目。只有来自领导者当前任期,“多数副本存在”的日志条目才会被提交;一旦当前任期的一条日志提交了,那么根据日志匹配属性(Log Matching Property)它之前的日志项也都一定一致地存在于多数服务器上。虽然在某些情况下,领导者可以安全地得出结论,某个旧的日志条目已提交(例如,如果该条目存储在每个服务器上),但 Raft 采取了更保守的方法来保持简单性。

- 图8:一个时间序列展示了为什么领导者不能通过老任期日志来决定提交情况。在(a)中, S1是领导者,并且部分复制了索引2日志;在(b)中,S1 崩溃;S5获得了S3、S4和自己的选票,并在日志索引2处接受了不同的条目。在(c)中,S5崩溃;S1重启,被选为领导者继续复制。此时,任期2中的日志被大多数服务器复制,但尚未提交。如果S1如(d)所示崩溃,S5可以成为领导者(获得 S2、S3 和 S4 的选票),并将该条目用其来自任期 3 的条目覆盖。然而,如果 S1 在崩溃前如(e)所示,在大多数服务器上复制了来自当前任期的条目,那么该条目将被提交(S5 无法赢得选举)。此时,日志中的所有前面的条目也将被提交。
Raft 在提交规则中增加了这一复杂性,因为日志条目在领导者复制来自前任期的条目时会保留其原始的任期号。在其他共识算法中,如果新的领导者重新复制前一个“任期”的条目,必须使用其新的“任期号”进行复制。Raft 的方法使得对日志条目进行推理变得更加容易,因为它们随着时间推移和跨日志保持相同的任期号。此外,Raft 中的新领导者传输的来自前任期的日志条目比其他算法少(其他算法必须传输冗余的日志条目以重新编号它们后才能提交)。
5.4.3 安全性论证
在完整的 Raft 算法基础上,我们现在可以更精确地论证“领导者完整性”属性的成立(该论证基于安全性证明,见第 9.2 节)。我们假设“领导者完整性”属性不成立,然后证明这一假设会导致矛盾。
假设任期 T 的领导者(leaderT)提交了来自其任期的日志条目,但该条目没有被未来某个任期的领导者存储。考虑任期 U > T 的最小值,其中领导者(leaderU)没有存储该条目。

- 图9:如果 S1(任期 T 的领导者)提交了来自其任期的新日志条目,并且 S5 被选为更晚任期 U 的领导者,那么必须至少有一个服务器(S3)既接受了该日志条目,又为 S5 投了票。
- 该已提交的条目在 leaderU 的日志中必定不存在(领导者永远不会删除或覆盖条目)。
- leaderT 将该条目复制到集群的大多数服务器,而 leaderU 从集群的大多数服务器中获得了选票。因此,至少有一台服务器(“投票者”)既接受了 leaderT 的条目,也投票给了 leaderU,如图 9 所示。投票者是达到矛盾的关键。
- 投票者必须在投票给 leaderU 之前接受了 leaderT 的已提交条目;否则,它将拒绝来自 leaderT 的 AppendEntries 请求(因为它的任期会大于 T)。
- 投票者在投票给 leaderU 时仍然存储该条目,因为每个中间的领导者都包含该条目(假设如此),领导者永远不会删除条目,跟随者只会在条目与领导者冲突时删除条目。
- 投票者投票给了 leaderU,因此 leaderU 的日志必须至少和投票者的日志一样“最新”。这导致了两个矛盾。
- 首先,如果投票者和 leaderU 的最后一个日志任期相同,那么 leaderU 的日志必须至少与投票者的日志一样长,因此它的日志包含了投票者日志中的每个条目。这是一个矛盾,因为投票者包含了已提交的条目,而我们假设 leaderU 不包含它。
- 否则,leaderU 的最后一个日志任期必须大于投票者的任期。而且,它必须大于 T,因为投票者的最后一个日志任期至少是 T(它包含了来自任期 T 的已提交条目)。创建 leaderU 最后一个日志条目的较早的领导者必定包含了该已提交条目(假设如此)。然后,根据日志匹配属性,leaderU 的日志也必须包含该已提交条目,这是一个矛盾。
- 这完成了矛盾的证明。因此,所有大于 T 的任期的领导者必须包含任期 T 中已提交的所有条目。
- 日志匹配属性保证了未来的领导者也会间接包含已提交的条目,例如图 8(d) 中的索引 2。
根据“领导者完整性”属性,我们可以证明图 3 中的“状态机安全性”属性,状态机安全性属性指出,如果某个服务器已将某个日志条目应用到其状态机,则其他服务器永远不会为相同的索引应用不同的日志条目。当服务器将日志条目应用到其状态机时,它的日志必须与领导者的日志在该条目之前完全一致,并且该条目必须已提交。现在考虑任何服务器应用给定日志索引的最低任期;日志完整性属性保证了所有更高任期的领导者将存储相同的日志条目,因此稍后应用该索引的服务器将应用相同的值。因此,状态机安全性属性成立。
最后,Raft 要求服务器按日志索引顺序应用条目。结合状态机安全性属性,这意味着所有服务器将按相同的顺序将完全相同的日志条目应用到它们的状态机。
5.5 追随者和候选者崩溃
到目前为止,我们主要关注的是领导者崩溃。追随者和候选人崩溃的处理要比领导者崩溃简单,并且它们的处理方式相同。如果追随者或候选人崩溃,则未来发送到它的 RequestVote 和 AppendEntries RPC 会失败。Raft 通过无限重试来处理这些失败;如果崩溃的服务器重新启动,RPC 就会成功完成。如果一个服务器在完成 RPC 后崩溃,但在响应之前,服务器重新启动后会再次接收到相同的 RPC。Raft 的 RPC 是幂等的,所以这不会造成任何问题。例如,如果一个追随者收到一个 AppendEntries 请求,其中包含已经存在于其日志中的日志条目,它会忽略请求中的这些条目。
5.6 时序和可用性
Raft 的一个要求是,安全性不能依赖于时序:系统不能因为某个事件发生得比预期快或慢而产生错误的结果。然而,可用性(系统能否及时响应客户端)必然依赖于时序。例如,如果消息交换的时间比服务器崩溃之间的典型时间要长,候选人将不会在足够长的时间内维持下去以赢得选举;没有一个稳定的领导者,Raft 无法继续执行。
领导者选举是 Raft 中时序最为关键的部分。只要系统满足以下时序要求,Raft 就能够选举并维持一个稳定的领导者:
broadcastTime ≪ electionTimeout ≪ MTBF
在这个不等式中,broadcastTime 是服务器向集群中每个服务器并行发送 RPC 并接收响应的平均时间;electionTimeout 是第 5.2 节中描述的选举超时;MTBF 是单个服务器的平均故障间隔时间。广播时间应该比选举超时小一个数量级,这样领导者就能可靠地发送心跳消息,防止追随者开始选举;由于选举超时使用了随机化方法,这个不等式还使得选票分裂变得不太可能。选举超时应该比 MTBF 小几个数量级,以便系统能够稳定前进。当领导者崩溃时,系统会在大约选举超时的时间内不可用;我们希望这只占总体时间的一小部分。
广播时间和 MTBF 是底层系统的属性,而选举超时是我们需要选择的参数。Raft 的 RPC 通常要求接收方将信息持久化到稳定存储中,因此广播时间可能从 0.5 毫秒到 20 毫秒不等,具体取决于存储技术。因此,选举超时可能介于 10 毫秒和 500 毫秒之间。典型的服务器 MTBF 通常是几个月甚至更长,这完全满足时序要求。
6. 集群成员变更
到目前为止,我们假设集群配置Configuration(参与共识算法的服务器集)是固定的。实际上,偶尔需要更改配置,例如在服务器故障时替换服务器,或更改副本的数量。尽管可以通过将整个集群下线、更新配置文件然后重启集群来实现这一点,但这样会在变更期间使集群不可用。此外,如果涉及手动操作,也有可能出现操作员错误。为了避免这些问题,我们决定将配置变更自动化,并将其纳入 Raft 共识算法中。
为了使配置变更机制安全,过渡过程中必须不存在可能同时选举出两个领导者的情况。不幸的是,任何一种服务器从旧配置直接切换到新配置的方法都是不安全的。由于无法原子性地同时切换所有服务器,集群在过渡期间有可能分裂成两个独立的多数(参见图 10)。

- 图10:直接从一个配置切换到另一个配置是不安全的,因为不同的服务器会在不同的时间切换。在这个例子中,集群从三个服务器扩展到五个服务器。不幸的是,在某些时刻,可能会为同一个任期选举出两个不同的领导者,一个由旧配置(
)中的多数选出,另一个由新配置( )中的多数选出。
为了确保安全,配置变更必须采用两阶段的方法。有多种方式可以实现这两个阶段。例如,一些系统使用第一阶段禁用旧配置,使其无法处理客户端请求;然后,第二阶段启用新配置。在 Raft 中,集群首先切换到一个过渡配置,我们称之为联合共识(joint consensus)。一旦联合共识被提交,系统就会过渡到新配置。联合共识结合了旧配置和新配置:
- 日志要同时发送给旧配置中的所有节点,以及新配置中的所有节点。
- 任何来自旧配置或新配置的服务器都可以担任领导者
- 一致性协议(无论是选举还是提交日志)都必须分别在旧配置和新配置中获得多数同意
联合共识机制允许集群中的各个服务器在不同时间点逐步切换配置,而不会影响系统的安全性。此外,在配置变更过程中,集群可以继续对外提供服务
当 Leader 收到从旧配置 Cold 切换到新配置 Cnew 的请求时,它会先生成一个联合配置(Cold,new)的日志条目,并按照标准的 Raft 日志复制机制将其复制到其他服务器。当某个服务器将这个配置变更条目追加到日志后,即使它还未提交,它也会从此开始使用该新配置来做决策。也就是说,每个服务器总是基于自己日志中“最新”的配置来做决策,哪怕它还没提交。因此,Leader 会使用 Cold,new 联合配置的规则(双多数)来判断 Cold,new 这条日志是否已经被提交。如果 Leader 在变更中崩溃,新 Leader 的产生依据是候选节点日志中是否包含 Cold,new。如果没有包含,就可能在旧配置 Cold 中选出 Leader。但无论如何,Cnew(新配置)都不能在这期间单独做出决策
一旦 Cold,new 被提交,旧配置和新配置都必须彼此“批准”才能做决策,此时才是安全的。之后,Leader 会创建一个新的日志条目,表示现在正式使用配置 Cnew,并将其复制到整个集群。一旦使用 Cnew 的规则提交了配置 Cnew,这时候旧配置就不再重要了,不属于新配置的节点可以安全地关机。系统中不会出现某个时刻,Cold 和 Cnew 同时能做决定。这个设计保证了整个变更过程的安全性。

- 图11:配置变更的时间线。虚线表示已经创建但尚未提交的配置条目,实线表示最新提交的配置条目。领导者首先在其日志中创建Cold,new配置条目,并将其提交给Cold,new(Cold和Cnew的多数派)。然后,它创建Cnew条目,并将其提交给Cnew的多数派。在任何时刻,Cold和Cnew都不能独立做出决策。
配置变更还需要解决另外三个问题。第一个问题是新服务器可能最初没有存储任何日志条目。如果它们在这种状态下被加入集群,可能需要相当长的时间才能赶上进度,在此期间可能无法提交新的日志条目。为了避免可用性中断,Raft在配置变更之前引入了一个额外阶段,在此阶段中,新服务器作为非投票成员加入集群(领导者会将日志条目复制给它们,但它们不计入多数派)。一旦新服务器赶上集群的其他部分,就可以按照上述描述进行重新配置。
第二个问题是集群领导者可能不属于新配置。在这种情况下,领导者在提交Cnew日志条目后会下台(返回到跟随者状态)。这意味着,在提交Cnew期间(即领导者正在提交Cnew时),领导者会管理一个不包含自己的集群;它会复制日志条目,但不计入多数派。领导者过渡发生在Cnew提交时,因为这是新配置可以独立运行的第一个时刻(此时可以从Cnew中选出领导者)。在此之前,可能只有Cold中的服务器可以被选为领导者。
第三个问题是被移除的服务器(不在Cnew中的服务器)可能会破坏集群。这些服务器将不会收到心跳信号,因此会超时并启动新的选举。它们随后会发送带有新任期号的RequestVote RPC,这将导致当前领导者退回到跟随者状态。最终会选出一个新的领导者,但被移除的服务器将再次超时,流程会重复,导致可用性下降。为了防止这个问题,服务器会忽略它认为当前领导者存在的RequestVote RPC。具体来说,如果服务器在听到当前领导者的心跳信号后的最短选举超时时间内收到RequestVote RPC,它不会更新其任期号或授予投票。这不会影响正常的选举,其中每个服务器在开始选举之前都会等待至少一个最短的选举超时。但它有助于避免被移除服务器带来的干扰:如果领导者能够将心跳信号发送到集群中,则它不会被更大的任期号推翻。
7. 日志压缩
Raft 的日志在正常操作过程中会增长,以包含更多的客户端请求,但在实际系统中,日志不能无限增长。随着日志变得越来越长,它占用更多的空间,并且需要更多的时间来重放。这最终会导致可用性问题,因此需要某种机制来丢弃日志中已经过时的信息。
快照是压缩的最简单方法。在快照机制中,整个当前系统状态会被写入到稳定存储的快照中,然后将此时的整个日志丢弃。Chubby 和 ZooKeeper 等系统都使用了快照,以下部分将描述 Raft 中的快照机制。
增量压缩方法,例如日志清理 [36] 和日志结构化合并树(LSM树)[30, 5],也是可能的。这些方法一次只处理一部分数据,因此它们能够更加均匀地分配压缩负载。它们首先选择一个已积累了许多已删除和被覆盖的对象的数据区域,然后以更紧凑的方式重写该区域的活动对象,并释放该区域。与快照机制相比,这需要更复杂的额外机制,因为快照简化了问题,始终操作整个数据集。虽然日志清理需要对 Raft 进行修改,但状态机可以使用与快照相同的接口实现 LSM 树。

- 图12: 一个服务器用新的快照替换它日志中已提交的条目(索引1到5),该快照只存储当前的状态(例如此示例中的变量x和y)。快照的最后包含索引和术语用于将快照定位到日志中的第6条目之前。
图12展示了 Raft 中快照的基本概念。每个服务器独立地创建快照,覆盖其日志中已提交的条目。大部分工作是由状态机将当前状态写入快照。Raft 在快照中还包含少量元数据:最后包含的索引是该快照替换的日志中最后一个条目的索引(即状态机已应用的最后一个条目),最后包含的任期是该条目的任期。这些信息被保留以支持 AppendEntries 一致性检查,因为接下来的日志条目需要一个先前的日志索引和任期。为了支持集群成员变化(第6节),快照还包括截至最后包含索引时日志中的最新配置。完成快照写入后,服务器可以删除所有通过最后包含索引的日志条目,以及任何先前的快照。
尽管服务器通常独立创建快照,但领导者必须偶尔向滞后的跟随者发送快照。这发生在领导者已经丢弃了它需要发送给跟随者的下一个日志条目时。幸运的是,在正常操作中,这种情况不太可能发生:一个已经跟上领导者进度的跟随者应该已经有该条目。然而,异常慢的跟随者或新加入集群的服务器(第6节)则不会。为了使这类跟随者保持最新,领导者将通过网络发送快照。
我们考虑了另一种基于领导者的方法,其中只有领导者创建快照,然后将快照发送给每个跟随者。然而,这有两个缺点。首先,将快照发送给每个跟随者会浪费网络带宽并减慢快照过程。每个跟随者已经拥有生成自己快照所需的信息,通常从本地状态生成快照比通过网络发送和接收快照要便宜得多。其次,领导者的实现会更加复杂。例如,领导者需要与复制新日志条目同时并行发送快照,以免阻塞新的客户端请求。
还有两个影响快照性能的问题。首先,服务器必须决定何时进行快照。如果服务器快照过于频繁,会浪费磁盘带宽和能源;如果快照过于不频繁,则可能会耗尽存储空间,并增加重启时重放日志所需的时间。一个简单的策略是,当日志达到固定大小(以字节为单位)时进行快照。如果此大小设置得远大于快照的预期大小,那么快照的磁盘带宽开销将很小。
第二个性能问题是写入快照可能需要相当长的时间,我们不希望这延迟正常操作。解决方法是使用写时复制技术,这样新更新就能被接受,而不会影响正在写入的快照。例如,使用函数式数据结构构建的状态机自然支持这一点。或者,操作系统的写时复制支持(例如,Linux 的 fork)可以用来创建整个状态机的内存快照(我们的实现使用了这种方法)。

领导者使用一个新的 RPC,称为 InstallSnapshot,将快照发送给那些落后的跟随者;见图13。当跟随者接收到这个 RPC 的快照时,必须决定如何处理其现有的日志条目。通常,快照将包含一些尚未在接收者日志中的新信息。在这种情况下,跟随者丢弃整个日志;因为这些条目已经被快照取代,且可能存在与快照冲突的未提交条目。如果接收者收到的快照描述了其日志的前缀(由于重传或错误),则快照所覆盖的日志条目将被删除,但在快照之后的条目仍然有效,必须保留。
这种快照方法与 Raft 的强领导者原则有所不同,因为跟随者可以在不知晓领导者的情况下进行快照。然而,我们认为这一改变是合理的。虽然领导者有助于避免在达成一致时发生冲突,但在进行快照时一致性已经达成,因此不会发生冲突。数据仍然只从领导者流向跟随者。
8. 客户端交互
本节描述了客户端如何与Raft交互,包括客户端如何找到集群的领导者,以及Raft如何支持线性化语义。这些问题适用于所有基于共识的系统,Raft的解决方案与其他系统类似。
Raft的客户端将所有请求发送给领导者。当客户端首次启动时,它会连接到一个随机选择的服务器。如果客户端的第一个选择不是领导者,该服务器将拒绝客户端的请求,并提供它所知道的最新领导者的信息(AppendEntries请求包括领导者的网络地址)。如果领导者崩溃,客户端请求将超时;然后,客户端会重新尝试与随机选择的服务器连接。
Raft的目标是实现线性化语义(每个操作似乎在某个时间点立即执行,且仅执行一次,在调用和响应之间的某个时刻)。然而,正如前面所描述,Raft可能会多次执行一个命令:例如,如果领导者在提交日志条目后但在响应客户端之前崩溃,客户端将重新尝试该命令并选择新领导者,导致该命令第二次执行。解决方案是客户端为每个命令分配唯一的序列号。然后,状态机跟踪每个客户端处理的最新序列号,并与相应的响应一起记录。如果收到一个序列号已执行的命令,它会立即响应而不重新执行请求。
只读操作可以在不向日志写入任何内容的情况下处理。然而,如果没有额外的措施,这可能会返回过时的数据,因为响应请求的领导者可能已经被一个新的领导者取代,而它对此一无所知。线性化读取必须不返回过时数据,Raft需要采取两个额外的预防措施来保证这一点,而不使用日志。首先,领导者必须拥有最新的已提交条目信息。领导者完整性属性保证领导者拥有所有已提交的条目,但在其任期开始时,它可能不知道哪些条目已提交。为此,它需要提交来自自己任期的一条空白的无操作(no-op)条目。Raft通过让每个领导者在其任期开始时提交一条无操作条目来处理这一点。其次,在处理只读请求之前,领导者必须检查它是否已经被废除(如果一个更近期的领导者被选举出来,可能存在它的信息过时的情况)。Raft通过让领导者在响应只读请求之前与集群大多数服务器交换心跳消息来处理这一点。或者,领导者可以依赖心跳机制提供一种租约(lease)形式,但这会依赖于时间的安全性(假设时钟偏差有界)。
9 实现与评估
我们已经将Raft实现为一个复制的状态机,存储RAMCloud的配置信息,并协助RAMCloud协调器的故障转移。Raft的实现包含大约2000行C++代码,不包括测试、注释或空行。源代码是公开的 [23]。目前大约有25个独立的第三方开源Raft实现 [34],它们处于不同的开发阶段,基于本论文的草稿。此外,各种公司正在部署基于Raft的系统 [34]。
本节其余部分将通过三个标准来评估Raft:可理解性、正确性和性能。
9.2 性能
Raft的性能与其他共识算法(如Paxos)相似。对于性能而言,最重要的场景是已经建立的领导者在复制新的日志条目时。Raft通过最少的消息数来实现这一点(即从领导者到集群一半节点的单次往返)。此外,Raft的性能还有进一步提升的空间。例如,它容易支持批处理和流水线请求,以提高吞吐量并降低延迟。许多优化方案已经在文献中提出,其他算法的许多优化也可以应用于Raft,但我们将这些留待未来的工作中进行探讨。
我们使用Raft实现来测量Raft的领导者选举算法的性能,并回答两个问题:第一,选举过程是否快速收敛?第二,领导者崩溃后可以实现的最小停机时间是多少?
为了测量领导者选举,我们反复使一个包含五个服务器的集群中的领导者崩溃,并记录检测崩溃并选举新领导者所需的时间(见图16)。为了生成最坏情况,测试中的每个服务器日志长度不同,因此有些候选者不符合成为领导者的条件。此外,为了鼓励分裂选票,我们的测试脚本在领导者终止之前触发了同步的心跳RPC广播(这近似于领导者在崩溃前复制新的日志条目时的行为)。
图16中的上图显示,选举超时的少量随机化足以避免选举中的分裂选票。在没有随机化的情况下,由于许多分裂选票,领导者选举在我们的测试中始终超过了10秒。只添加5毫秒的随机化就能显著改善,导致中位停机时间为287毫秒。增加更多随机化进一步改善了最坏情况的表现:在50毫秒的随机化下,最坏情况下的完成时间(经过1000次试验)为513毫秒。
图16中的下图显示,通过减少选举超时可以缩短停机时间。在12-24毫秒的选举超时下,选举领导者平均只需要35毫秒(最长的试验用了152毫秒)。然而,如果进一步降低超时,则会违反Raft的时间要求:领导者很难在其他服务器开始新一轮选举之前广播心跳,这可能导致不必要的领导者更换,降低系统整体可用性。我们建议使用保守的选举超时,如150-300毫秒;这样的超时不太可能导致不必要的领导者更换,并且仍能提供良好的可用性。

- 图16:检测并替换崩溃领导者的时间。 上方图表显示了选举超时中的随机性变化,底部图表则展示了最小选举超时的变化。每条线代表1000次试验(除了“150-150ms”使用了100次试验),并对应某种选举超时的设置。例如,“150-155ms”表示选举超时在150ms到155ms之间随机均匀选择。测量是在一个包含五个服务器的集群上进行的,广播时间大约为15ms。对于一个九个服务器的集群,结果也类似
10 相关工作
关于共识算法的研究有很多出版物,许多可以归入以下几类:
- Lamport 最初描述的 Paxos [15] 以及对其更清晰的解释尝试 [16, 20, 21]。
- 对 Paxos 的扩展,填补缺失的细节并修改算法,以提供更好的实现基础 [26, 39, 13]。
- 实现共识算法的系统,如 Chubby [2, 4]、ZooKeeper [11, 12] 和 Spanner [6]。Chubby 和 Spanner 的算法没有详细发布,尽管它们都声称基于 Paxos。ZooKeeper 的算法已经更详细地发布,但与 Paxos 差异较大。
- 可应用于 Paxos 的性能优化 [18, 19, 3, 25, 1, 27]。
- Oki 和 Liskov 的 Viewstamped Replication (VR),这是一种与 Paxos 同时开发的共识替代方法。最初的描述 [29] 与分布式事务的协议交织在一起,但在最近的更新中核心共识协议已被分离 [22]。VR 使用基于领导者的方法,与 Raft 有许多相似之处。
Raft 和 Paxos 的最大区别在于 Raft 的强领导性:Raft 将领导者选举作为共识协议的核心部分,并尽可能将所有功能集中在领导者中。这种方法导致了更简单的算法,更容易理解。例如,在 Paxos 中,领导者选举是与基本共识协议正交的:它仅作为一种性能优化,并不直接要求达成共识。然而,这也带来了额外的机制:Paxos 包含了一个用于基本共识的两阶段协议,以及一个单独的领导者选举机制。相比之下,Raft 将领导者选举直接整合进共识算法,并作为共识的两个阶段之一。这减少了比 Paxos 更复杂的机制。
像 Raft 一样,VR 和 ZooKeeper 都是基于领导者的,因此在许多方面与 Paxos 相比具有 Raft 的优势。然而,Raft 比 VR 或 ZooKeeper 的机制更少,因为它最大限度地减少了非领导者中的功能。例如,Raft 中的日志条目仅向一个方向流动:通过 AppendEntries RPC 从领导者向外流动。而在 VR 中,日志条目在两个方向上流动(领导者可以在选举过程中接收日志条目);这导致了额外的机制和复杂性。ZooKeeper 的已发布描述也将日志条目传输到领导者并从领导者传输,但实现显然更像 Raft [35]。
Raft 拥有比我们所知的其他共识日志复制算法更少的消息类型。例如,我们统计了 VR 和 ZooKeeper 用于基本共识和成员变更的消息类型(不包括日志压缩和客户端交互,因为这些与算法几乎独立)。VR 和 ZooKeeper 各自定义了 10 种不同的消息类型,而 Raft 只有 4 种消息类型(两种 RPC 请求及其响应)。Raft 的消息比其他算法更简洁,但它们整体上更简单。此外,VR 和 ZooKeeper 的描述中涉及在领导者变化时传输整个日志;为了使这些机制变得实用,还需要定义更多的消息类型来优化这些机制。
Raft 的强领导性方法简化了算法,但也排除了某些性能优化。例如,平等 Paxos(EPaxos)在某些条件下可以通过无领导者的方法实现更高的性能 [27]。EPaxos 利用状态机命令的可交换性。只要其他并发提议的命令与之可交换,任何服务器都可以通过一次通信轮次来提交命令。然而,如果并发提议的命令彼此不交换,EPaxos 就需要额外的一轮通信。由于任何服务器都可以提交命令,EPaxos 在服务器之间良好地平衡负载,并能够在广域网环境中比 Raft 实现更低的延迟。但它也为 Paxos 增加了显著的复杂性。
在其他工作中,已经提出或实现了几种集群成员变更的方法,包括 Lamport 最初的提案 [15]、VR [22] 和 SMART [24]。我们为 Raft 选择了联合共识方法,因为它利用了共识协议的其余部分,因此对成员变更所需的额外机制非常少。Lamport 的基于 α 的方法对 Raft 来说不可行,因为它假设无需领导者即可达成共识。与 VR 和 SMART 相比,Raft 的重新配置算法有一个优势,那就是成员变更可以在不限制正常请求处理的情况下进行;相比之下,VR 在配置变更期间会停止所有正常处理,而 SMART 对待未完成请求的数量有类似于 α 的限制。Raft 的方法也比 VR 或 SMART 更少的机制。
11 结论
算法通常是以正确性、效率和/或简洁性为主要目标设计的。尽管这些目标都值得追求,我们认为可理解性同样重要。在开发人员将算法转化为实际实现之前,其他目标无法实现,而实际实现不可避免地会偏离和扩展原始形式。除非开发人员对算法有深入的理解,并能够建立起相关的直觉,否则他们很难在实现中保留算法的期望特性。
本文解决了分布式共识的问题,Paxos 是一个广泛接受但难以理解的算法,多年来一直困扰着学生和开发人员。我们开发了一种新的算法,Raft,我们已经证明它比 Paxos 更易于理解。我们还认为 Raft 为系统构建提供了更好的基础。将可理解性作为主要设计目标改变了我们设计 Raft 的方式;随着设计的进展,我们发现自己反复使用了一些技术,比如分解问题和简化状态空间。这些技术不仅提高了 Raft 的可理解性,还使我们更容易验证其正确性。