Archipelago

Lec 7容错复制:Raft

讲义完成更新于 2026-08-27

摘要

课件:Raft (1) (l-raft.txt)、Raft (2) (l-raft2.txt);论文:Raft extended (2014);FAQ:part1、part2

Lec 7 容错复制:Raft

课件:Raft (1) (l-raft.txt)、Raft (2) (l-raft2.txt);论文:Raft extended (2014);FAQ:part1、part2

一、背景:复制状态机与主备模式

复制状态机(Replicated State Machine, RSM) 思想:客户端把操作发给主,主对操作定序后发给各备份;所有副本按相同初态、相同顺序、执行相同的确定性操作 → 得到相同的最终状态。GFS 的主备就是一个典型例子。

定义

定义:主备模式(Primary/Backup) :客户端把操作发给主节点,主节点排序后发给备份节点;所有备份执行全部操作。相同初态 + 相同顺序 + 确定性 ⇒ 相同终态

副本能容忍哪些故障?

  • ✅ 单副本的 fail-stop(停机式) 故障:风扇坏、CPU 过热自停、电源/网线被拔、磁盘满了自停;
  • ⚠️ 不一定能挡 bug 或操作员失误——它们常非故障-停机( fail-stop),且可能相关(某个输入让所有副本一起崩);
  • 🌍 地震/全城停电只有在副本物理分散时才挡得住。

副本数量通常取 3–5(够在修复期间挺过故障,又不至于太贵;论文实验用 2 副本,仅容 1 故障)。

🔬 为什么需要 Leader?

朴素的"两台机器互相接管"会因网络分区导致脑裂。引入一个明确的 leader 来给操作定序,能根除脑裂;代价是要解决"如何选主、如何换主"。(某些设计如 Paxos 没有 leader。)

二、 Raft 总览

Raft 是被嵌进每个副本里的一个库:上层是 k/v 等服务(持有状态机状态),下层是 Raft(持有日志)。它已被广泛用于工业系统——etcd(→ Kubernetes)、CockroachDB、TiKV、MongoDB、Docker Swarm 等。

🔬 定义:我们要保证什么?

或者说状态机安全的要求是什么?

若任一服务器在某个日志槽位执行了某条命令,则没有任何服务器会在同一槽位执行不同的命令(图 3 的状态机安全性)。否则换主时客户端可见状态会变,违背"对外像单机"的目标。

S0: 101 | 102
S1: 101 | 103      # 绝不允许两者各自执行第 2 槽 —— 这是必须杜绝的

Raft 维持五条不变量,正确性即由它们推得:

✅ 关键结论:Raft 五大性质(图 3)

  1. 选举安全性(Election Safety):一个任期内至多一个 leader。§5.2
  2. Leader 只追加(Leader Append-Only):leader 从不覆盖/删除自己日志的条目,只追加。§5.3
  3. 日志匹配(Log Matching):若两份日志在同一 (index, term) 有条目,则该位置之前的所有条目都相同。§5.3
  4. Leader 完整性(Leader Completeness):某任期内已提交的条目,必出现在所有更高任期 leader 的日志中。§5.4
  5. 状态机安全性(State Machine Safety):若某服务器已在某 index 应用了某条目,则其他服务器不会在该 index 应用不同条目。§5.4.3

三种状态:任一时刻每台服务器是 leader / follower / candidate 之一。常态下仅一个 leader,其余皆 follower;follower 被动响应;candidate 用于选举。

图 4:服务器状态转换

任期(term)作为逻辑时钟:时间被切成连续编号的任期,每个任期至多一个 leader(也可能因分裂选票而没有)。

图 5:任期

每台服务器存一个单调递增的 currentTerm,通信时交换;见到更大任期就更新自己并退位为 follower;见到过时任期的请求则拒绝。任期号让服务器能检测过时信息(如过时 leader)。

两种 RPC(基础共识只需这两种,第 7 节加第三种):RequestVote(candidate 选举时发)、AppendEntries(leader 复制日志 + 心跳)、以及 InstallSnapshot(传快照)。

一条命令的生命周期(时间线):客户端把 Put/Get 命令发给 leader 的 k/v 层 → k/v 层调 Start() → leader 的 Raft 把命令追加到日志 → 并行发 AppendEntries → follower 追加 → leader 等到含自己在内的多数派回复 → 该条目已提交(committed) → leader 与 follower 各自把命令交给 k/v 层执行(lab 中通过 ApplyMsg/applyCh)→ leader 回复客户端。

三、 Leader 选举

触发:follower 在一个选举超时(election timeout) 内没收到来自当前 leader/candidate 的有效 RPC,就假定无主,currentTerm++、转为 candidate、给自己投票、并行发 RequestVote。

candidate 有三种结局:(a) 赢得选举;(b) 别人成了 leader(收到任期 ≥\ge 自己的 AppendEntries 则承认并转 follower;任期更小则拒绝并保持 candidate);(c) 超时没结果,再开一轮新任期。

✅ 关键结论:为什么一个任期至多一个 leader

  • leader 必须拿到多数派的赞成票;
  • 每台服务器每个任期只投一票(candidate 投自己;否则先到先得,且须满足 §5.4 的额外限制)。

⇒ 同一任期内至多一人能拿到多数票 ⇒ 即使网络分区也至多一个 leader;即使部分服务器故障,选举仍可能成功。
注意:多数派是相对全体服务器,而非仅存活者。

如何得知新 leader? 新 leader 发出带更高任期号的 AppendEntries 心跳;因为"每任期至多一个 leader、且只有 leader 发 AppendEntries",所以看到任期 TT 的 AppendEntries 就知道 TT 的 leader 是谁。心跳会压制新选举,故心跳间隔必须明显短于选举超时。

分裂选票(split vote)与随机化:若选举计时器同时到点,人人投自己,谁也拿不到多数。Raft 让每台服务器在固定区间(如 150–300ms)内随机选取选举超时,打破对称——通常只有一台先超时、先赢、先发心跳,其余看到心跳就不再起选举。

[!TIP] 如何选 election timeout?(讲义)至少是几个心跳间隔(容忍丢心跳、避免无谓选举);又要够短以快速反应(tester 要求 5 秒内选出);随机部分要够长,让一个 candidate 在下一个超时前完成。论文建议 150–300ms。

旧 leader 不知道自己被取代怎么办?(讲义答你笔记里的问题)

  • 它可能没看到选举消息,或处于少数派分区;
  • 新 leader 意味着多数派已抬高 currentTerm:旧 leader 要么在某次 AppendEntries 回复里看到更大任期而退位,要么根本凑不齐多数派回复,从而不会提交/执行任何新条目 ⇒ 无脑裂;
  • 但少数派可能仍接受旧 leader 的 AppendEntries ⇒ 日志可能在旧任期末尾分叉。

🔬 例子:日志分叉如何产生

       10 11 12 13   <- 日志槽位号
S1:    3
S2:    3  3  4
S3:    3  3  5

S2 在任期 3 当 leader,把 10 复制给所有人、把 11 复制给 S2/S3(S1 崩了);S2 崩溃后快速重启、成为任期 4 leader,给自己加了 12 又崩;S3 在任期 5(靠 S1)当选,给 12 写了不同的条目。下一节解决这种分叉。

Leader 选举

Raft Peer 何时启动 Leader 选举?

Solution:当它在“选举超时”后未收到当前 Leader 的消息时,增加本地 currentTerm 值,尝试收集选票。注意:

  • 这可能会导致不必要的选举;虽然速度慢但安全
  • 旧的 Leader 可能仍然活跃,并认为自己是 Leader □

Raft 为 Leader 序列编号,新 Leader->新任期(term),一个任期最多有一个 Leader;也可能没有 Leader,编号有助于服务器跟随最新的 Leader,而不是过期的 Leader。

如何保证一个任期最多只有一个 Leader?

Solution:

前提:Leader 必须获得大多数服务器的赞成票;

每个服务器每个任期只能投一票:

  • 如果不是候选服务器(Candidate),则为第一个提出请求的服务器投票(符合图 2 的规则)

在给定任期内,最多只有一个服务器可以获得多数票:

  • 即使网络分区,也最多只有一个 Leader
  • 即使某些服务器发送故障,选举仍可成功

注意: 多数是指所有服务器(而不仅仅活跃的服务器)的多数票□

服务器如何获知新当选的 Leader?

Solution:Leader 会发送包含新的更高任期的 AppendEntries 心跳包;只有 Leader 会发送,而每个任期只有一个 Leader,因此如果看到任务带有任期号为 T 的 AppendEntries,就知道 T 任期的 Leader 是谁,心跳会抑制任务新的选举。因此 Leader 的心跳要比选举超时更频繁。

选举失败会发生什么?

Solution: 旧 Leader 可能没有看到选举消息

旧 Leader 不知道新 Leader 当选了,怎么办?

Solution:

  • 旧 Leader 可能没有看到选举消息
  • 旧 Leader 可能处于少数网络分区
  • 新 Leader 意味着大多数服务器已增加 currentTerm,旧 Leader 将在 AppendEntries 回复中看到新任期并下台;或者旧 Leader 无法获得大多数回复,因此旧 Leader 不会提交或执行任何新的日志条目 ,因此不会出现脑裂
  • 但少数服务器可能会接受旧服务器的 AppendEntries, 因此日志可能会在旧任期结束时出现分歧

Raft 日志

只读操作

Raft Leader 是否需要先提交只读操作(例如 Get(key))到日志中,才能回复客户端?换句话说,Leader 能否在没有提交操作的情况下,直接使用自己的 key/value 表内容来响应读取请求?

不行。 假设 S1 认为自己是 Leader,并收到了 Get(k)。 它可能最近在选举中失败了,但由于网络数据包丢失而没有意识到这一点。 新的 Leader,比如 S2,可能已经处理了该键的 Put() 操作, 因此 S1 的键/值表中的值已经过时。 提供过时的数据是线性不可逆的;这是一种脑裂现象。

图 2 要求将 Get() 操作提交到日志中。 如果 Leader 能够提交 Get() 操作,那么(在日志中的此时)它仍然是 Leader。在上述 S1 的案例中,它在不知不觉中失去了领导地位,因此无法获得提交 Get() 所需的大多数 AppendEntries 回复,因此它不会回复客户端。

但是:许多应用程序都是读操作密集型的。提交 Get() 操作需要时间。有什么方法可以避免只读操作的提交?这在实际系统中是一个重要的考虑因素。

一个关键思想: 租约。

修改 Raft 协议如下: 定义一个租约期限,例如 5 秒。 每次 Leader 获得 AppendEntries 的多数回复后, 它有权在租约期限内响应只读请求,而无需将只读请求添加到日志中,即无需发送 AppendEntries。 新的 Leader 在上一个租约期限到期之前无法执行 Put() 操作。 因此,follower 会跟踪他们上次响应 AppendEntries 的时间,并(在 RequestVote 回复中)告知新的 Leader。结果:更快的只读操作,仍然线性可控。

近况

关于提升

本讲小结

Raft 是迄今为止对现代分布式状态机复制技术解释最透彻的文章,启发了很多人去开发自己的复制实现方案。

使用场景 Raft(以及 Paxos)最常见的应用是构建容错的“配置服务”。这类服务负责跟踪大型分布式系统中服务器的职责分配情况。对于采用复制机制的系统,这项功能尤为重要。基于 Raft 的配置服务通常用于选择主节点,从而避免脑裂问题。VMware FT 的测试与设置服务器就是一个简单的配置服务示例。一些数据库,例如 Spanner、CockroachDB,使用 Raft 或 Paxos 进行数据复制。(相比之下,GFS、VMware FT 和 Chain Replication 使用更简单的主备复制机制。)有些数据库在两个方面都使用了 Raft 或 Paxos:一是用于配置服务,负责分配服务器职责(例如,每个分片的主节点和备节点);二是用于处理每个分片内部的数据。

缺点 基于 Paxos/Raft 协议的系统偶尔会出现故障,导致系统大规模宕机;例如,参见以下链接:https://decentralizedthoughts.github.io/2020-12-12-raft-liveness-full-omission/

论文阅读: 共识算法:Raft

本讲导览

Raft 是一种用于管理复制日志的共识算法。它生成的结果等同于 multi-Paxos 算法。为了增强可理解性,Raft 将共识的关键要素分离,如 Leader 选举、日志复制和安全性,并通过强制更高的一致性来减少需要考虑的状态数量。Raft 还包含一种新的机制,用于更改集群成员,该机制利用重叠多数派来确保安全性。

1. 引言

共识目的是允许一组机器作为一个连贯的整体协同工作,即便其中部分成员发生故障也能继续运作。以前是 Paxos 一家独大。现在提出落地的共识算法 Raft, 通过解耦合(Raft 将 Leader 选举、日志复制和安全性分开)和状态空间的减少(相比 Paxos,Raft 减少了服务器之间不一致情况下的不确定性)。

Feature:

  • 强 Leader(strong leader): Raft 采用比其他共识算法更强的领导机制。例如,日志条目仅从 Leader 流向其他服务器。这简化了对复制日志的管理,并使 Raft 更易于理解
  • Leader 选举(leader election):Raft 使用随机计时器来选举 Leader。这只在任何共识算法所需的心跳基础上增加了少量机制,却能简单而迅速地解决冲突。
  • 成员变更:Raft 更换集群服务器集合的机制采用了一种新的联合共识方法,在配置变更期间两个不同配置的多数派相互重叠。这允许集群在配置变更过程中继续正常操作。

本文的剩余部分将介绍复制状态机问题(第 2 节),讨论 Paxos 的优缺点(第 3 节),描述我们在提升可理解性方面的一般方法(第 4 节),Raft 共识算法(第 5-8 节),评估 Raft(第 9 节),并讨论相关工作(第 10 节)

2. 复制状态机

共识算法通常出现在**复制状态机(Replicated state machines, RSM)**的上下文中,而 RSM 又用于解决分布式系统中的多种容错问题。在这个方法中, 一组服务器上的状态机计算并维护相同的状态副本,即使某些服务器宕机也能继续运行。许多拥有单一集群 Leader 的大规模系统,如 GFS、HDFS 和 RAMCloud,通常使用一个独立的复制状态机来管理 Leader 选举并存储必须在 Leader 崩溃时仍需保存的配置信息。Google Chubby 和 ZooKeeper 就是复制状态机的示例。 【暗示了适合单一集群部署模式】

image-20241109154949022

复制状态机通常通过复制日志实现,如图 1 所示。每台服务器存储包含一系列命令的日志,状态机按顺序执行这些命令。每个日志包含相同顺序的相同命令,因此每个状态机处理相同的命令序列。由于状态机是确定性的,它们计算相同的状态和相同的输出序列。

共识算法通常具有以下特性:

  • 它们确保安全性——在任何情况下都不返回错误结果
  • 只要有多数服务器正常运行并可以互相通信以及与客户端通信,系统就是完全功能性的(即可用的)
  • 它们不依赖时间来确保一致性:故障时钟和极端消息延迟在最坏情况下只会影响系统的可用性。
  • 在通常情况下,一条命令在大多数集群响应 1 个 RTT 后即可完成;少数缓慢的服务器不会影响整体系统性能。

3. Paxos 有什么问题

Paxos 允许一组服务器就单个值达成一致,我们将其称为 single-decree Paxos("单一法令 Poxos?")。把很多次 single-decree Paxos 串联,形成一条 日志 (log),这就是 multi-Paxos。Paxos 确保了安全性和生命周期,并支持集群成员的更换。其正确性已经得到证明,且在常规情况下效率很高。

然而,Paxos 存在两个显著的缺陷。首先,Paxos 非常难以理解。第二个问题是,它未能为构建实用实现提供良好基础。原因之一是尚未有被广泛认可的 Multi-Paxos 算法。诸如 Chubby [4] 之类的系统实现了类似 Paxos 的算法,但大多数情况下其细节尚未公开。

此外,Paxos 的架构不适合构建实用系统;这是单决策分解的另一个后果。例如,选择一组日志条目后再将它们整合成一个顺序日志几乎没有什么好处,反而增加了复杂性。

4. 为了易于理解而设计

我们运用了两种普遍适用的技术。第一种技术是广为人知的“问题分解”方法;第二种减少需要考虑的状态数量;具体来说,不允许日志中出现空洞,Raft 还限制了日志之间可能出现的不一致情况。

5. Raft 共识算法

我们通过保持如下性质的不变性, 从而证明 Raft 是正确的

  • 选举安全性:在一个给定的任期内,最多只能选举出一个 Leader。§5.2
  • Leader 追加日志:Leader 从不覆盖或删除日志中的条目;它只会追加新的条目。§5.3
  • 日志匹配:如果两个日志在相同的索引和任期包含一个条目,那么在该索引之前的所有条目都必须相同。§5.3
  • Leader 完整性:如果一个日志条目在给定任期内被提交,那么该条目将会出现在所有更高编号任期的 Leader 日志中。§5.4
  • 状态机安全性:如果一个服务器已经将某个日志条目应用到其状态机的某个索引位置,那么其他任何服务器都不会对该索引应用不同的日志条目。§5.4.3

image-20241111123737381

Raft 通过选举一个领导节点来实现共识,并赋予管理复制日志的全部职责。Leader 从客户端接受日志条目,将他们复制到其他服务器,并告知其他服务器何时可以安全地将日志条目生效到状态机。Leader 的存在简化了复制日志的管理。

基于这种领导节点机制,Raft 将共识问题分解为三个相对独立的子问题:

  • 选主
  • 日志复制
  • 安全性

5.1 Raft 基础

一个 Raft 集群由多个服务器组成;通常为五个,这样可以容忍两个服务器出现故障。任何时刻,每台服务器都处于三种状态之一:领导者(leader)、追随者(follower)或候选者(Candidate)。在正常操作中,系统中只有一个 Leader,其他所有服务器都是 Follower。Follower 是被动的——它们不会主动发出请求,只是响应来自 Leader 和 Candidate 的请求。Leader 处理所有用户请求(如果用户联系到 Follower,Follower 会将其重定向到 Leader)。第三种状态是 Candidate,用于选主。图 4 显示了状态及其转换情况,这些转换将在下面讨论。

image-20241111125251772

  • ​ 图 4:服务器状态。follower 仅响应来自其他服务器的请求。如果 follower 没有接收到任何通信,它会变成 Candidate 并发起选举。获得来自全体集群多数服务器投票的 Candidate 将成为新的 Leader。Leader 通常会一直运行,直到发生故障

image-20241111125403887

Raft 将时间划分为任意长度的任期(term),如图 5 所示。任期用连续整数编号。每个任期开始时都会进行一次选举(election),一个或多个 Candidate 试图成为 Leader,如第 5.2 节所述。如果 Candidate 赢得选举,它将在该任期内担任 Leader。在某些情况下,选举会导致投票平局。在某些情况下,选举会导致投票分裂。在这种情况下,任期将结束且没有选出 Leader,很快将开始一个新的任期(并进行新的选举)。Raft 确保在一个给定的任期内最多只有一个 Leader。

在某些情况下,某个服务器可能没有观察到选举或整个任期。任期在 Raft 中充当了一个逻辑时钟,它们允许服务器检测过时的信息,例如过时的 Leader。每个服务器存储一个当前任期号,该任期号随时间单调递增。在服务器之间的通信中会交换当前任期号;如果某个服务器的当前任期小于另一个服务器的任期,它将更新为较大的那个任期号。如果某个 Candidate 或 Leader 发现其任期已经过时,它会立即回退到 Follower 状态。如果服务器收到一个过时的任期号请求,它将拒绝该请求。

Raft 服务器通过 RPC 进行通信,基础共识算法只需要两种类型的 RPC。RequestVote RPC 由 Candidate 在选举期间发起(见第 5.2 节),AppendEntries RPC 由 Leader 发起,用于复制日志条目并提供心跳信号(见第 5.3 节)。第 7 节增加了第三种 RPC,用于在服务器之间传输快照。如果服务器未能及时收到响应,它会重试 RPC,并且它们会并行发起 RPC 以获得最佳性能。

5.2 选主

Raft 使用心跳机制来触发 Leader 选举。当服务器启动时,它们首先处于 Follower 状态。只要服务器接收到来自 Leader 或 Candidate 的有效 RPC,它将保持在 Follower 状态。Leader 会定期发送心跳信号(没有日志条目的 AppendEntries RPC)给所有 Follower,以维持其权威。如果一个 Follower 在一定时间内(称为选举超时)没有接收到任何通信,它就假设没有有效的 Leader,并开始选举以选择一个新 Leader。

要开始选举,Follower 会增加当前的任期号,并转变为 Candidate 状态。它随后为自己投票,并并行向集群中的其他每个服务器发送 RequestVote RPC。Candidate 会保持在这一状态,直到发生以下三种情况之一:(a)它赢得选举,(b)另一个服务器成为 Leader,或者(c)一段时间过去了,未能选出 Leader。以下段落分别讨论这些结果。

Candidate 如果收到集群中多数服务器的选票,且这些选票属于同一任期,它就赢得了选举。每个服务器在一个任期内最多为一个 Candidate 投票,且按先到先得的原则(注意:第 5.4 节对选举有额外限制)。多数规则确保在特定任期内最多只有一个 Candidate 能够获胜(选举安全性属性)。一旦 Candidate 赢得选举,它就成为 Leader。随后它会发送心跳消息给所有其他服务器,确立自己的权威并防止新一轮选举。

在等待投票期间,Candidate 可能会收到来自其他服务器的 AppendEntries RPC,声称自己是 Leader。如果 Leader 的任期(RPC 中包含的)至少与 Candidate 当前的任期相同,那么 Candidate 会承认 Leader 是合法的,并转为 Follower 状态。如果 RPC 中的任期比 Candidate 当前的任期要小,Candidate 则会拒绝该 RPC,并继续保持 Candidate 状态。

第三种可能的结果是 Candidate 既没有赢得选举,也没有输掉选举:如果多个 Follower 同时变成 Candidate,投票可能会分裂,导致没有任何 Candidate 获得多数选票。当这种情况发生时,每个 Candidate 都会超时,并通过增加任期号来发起新一轮选举,并再次发起 RequestVote RPC。然而,如果没有额外的措施,选票分裂可能会无限期地重复。

Raft 使用随机化的选举超时来确保选票分裂很少发生,并且可以迅速解决。为了防止选举分裂,选举超时从一个固定的时间区间中随机选择(例如,150-300ms)。这样可以分散服务器,使得在大多数情况下,只有一个服务器会超时;它会赢得选举,并在其他服务器超时之前发送心跳信号。相同的机制也用于处理选举分裂。每个 Candidate 会在选举开始时重新设置其随机化选举超时,并等待该超时结束后才开始下一轮选举;这样减少了新一轮选举中出现选举分裂的可能性。第 9.3 节展示了这种方法如何快速选举出 Leader。

5.3 日志复制

一旦 Leader 选举完成,它开始处理客户端请求。每个客户端请求包含一个 RSM 的执行的命令。Leader 将命令追加到自己日志中,然后并行地向集群中的每个其他服务器发送 AppendEntries RPC 以复制该条目。当条目被安全地复制后,Leader 将执行该命令,并将执行结果返回给客户端。如果 Follower 崩溃或运行缓慢,或如果网络数据包丢失,Leader 会无限期地重试 AppendEntries RPC(即使它已经响应了客户端),直到所有 Follower 最终存储所有日志条目。

日志的组织方式如图 6 所示。每个日志条目存储一个状态机命令 和任期号。任期号用于检测日志之间的不一致性,并确保图 3 中的一些属性。每个日志条目还有一个整数索引,用于标识其在日志中的位置。

image-20241111131813108

  • 图 6:日志由条目组成,这些条目按顺序编号。每个条目包含它创建时的任期号(每个框中的数字)和一个状态机命令。当某个条目可以安全地应用到状态机时,该条目被视为已提交。

Leader 决定何时可以安全地将日志条目生效到状态机;这样的条目被称为已提交(commited)。Raft 保证已提交的条目是持久的,并且最终会被所有可用的状态机执行。日志条目一旦被复制到大多数服务器(例如,图 6 中的条目 7),则该条目及其前序条目都被认为已提交。Leader 会跟踪它知道的最高已提交索引,并将该索引包含在未来的 AppendEntries RPC 中(包括心跳),以便其他服务器最终得知。一旦 Follower 得知某个日志条目已提交,它会按照日志顺序执行该条目,最终生效到本地的状态机。

Raft 维护以下属性

  • 如果两个不同日志中的条目具有相同的索引和任期,则它们存储相同的命令。
  • 如果两个不同日志中的条目具有相同的索引和任期,则它们在所有前面的条目中完全一致。

第一个属性是因为 Leader 在给定任期内最多只能创建一个日志索引,并且日志条目从不更改其在日志中的位置。第二个属性通过 AppendEntries 执行的简单一致性检查来保证。当发送 AppendEntries RPC 时,Leader 会在其日志中包含一个条目的索引和任期,该条目紧随新条目之前。如果 Follower 的日志中没有找到具有相同索引和任期的条目,则会拒绝这些新条目。

在正常操作中,Leader 和 Follower 的日志保持一致。然而,Leader 的 Crash 可能导致日志不一致(旧的 Leader 可能没有完全复制其日志中的所有条目)。图 7 展示了 Follower 的日志可能与新 Leader 的日志不一致的情况。

当最上面的 Leader 上台时,、Fllower 的日志可能出现一下任何一种场景(a-f)。每个框代表一个日志条目;框中的数字是该条目的任期。

  1. 一个 Follower 可能缺少条目(a-b),或者
  2. 可能有额外的未提交条目(c-d),或者
  3. 两者都有(e-f)。

拿一个来说, 场景(f)可能发生在这样一种情况:该服务器在任期 2 时曾是 Leader,向日志中添加了几个条目,但在提交之前崩溃了;它快速重启,成为任期 3 的 Leader,并向日志中添加了更多条目;在任期 2 或任期 3 的任何条目都未提交之前,该服务器再次崩溃,并且在多个任期内保持宕机状态。

这些不一致性可能会在一系列的 Leader 和 Follower 崩溃后积累。日志中缺失和多余的条目可能跨越多个任期。

image-20250924232146831

在 Raft 中,Leader 通过强制 Follower 的日志与自己的日志一致来处理不一致性。为了使 Follower 的日志与自己的日志一致,Leader 必须找到与 Follower最近匹配的条目,并删除 Follower 日志中该点之后的所有条目,并将该点之后的所有 Leader 条目发送给 Follower。所有这些操作都通过 AppendEntries RPC 执行的一致性检查。Leader 会为每个 Follower 维护一个 nextIndex,它表示 Leader 将发送给该 Follower 的下一个日志条目的索引。当 Leader 首次成为领导时,它会将所有 nextIndex 值初始化为其日志中最后一个条目之后的索引(图 7 中的 11)。如果一个 Follower 的日志与 Leader 的日志不一致,AppendEntries 一致性检查将在下一个 AppendEntries RPC 中失败。在拒绝之后,Leader 会减少 nextIndex 并重试 AppendEntries RPC。最终,nextIndex 将达到一个点,在这个点上,Leader 和 Follower 的日志匹配。当发生这种情况时,AppendEntries 会成功,这会删除 Follower 日志中的冲突条目,并附加 Leader 日志中的条目(如果有的话)。一旦 AppendEntries 成功,Follower 的日志将与 Leader 的日志一致,并且在当前任期内将保持一致。【为什么不直接另外一个 RPC、一个 RTT 我就知道 Follower 的 NextIndex 了】

如果需要,可以对协议进行优化,以减少被拒绝的 AppendEntries RPC 的数量。例如,在拒绝 AppendEntries 请求时,Follower 可以包含冲突条目的任期和该任期的第一个索引。有了这些信息,Leader 可以减少 nextIndex,跳过该任期中的所有冲突条目;对于每个有冲突条目的任期,只需要一个 AppendEntries RPC,而不是每个条目一个 RPC。实际上,我们认为这种优化可能没有必要,因为失败发生的频率较低,并且出现许多不一致条目的情况也不太可能。

通过这个机制,Leader 在上任时不需要采取任何特殊措施来恢复日志一致性。它只需要开始正常操作,日志会自动在 AppendEntries 一致性检查的响应中趋于一致。Leader 永远不会覆盖或删除自己日志中的条目,只追加

这种日志复制机制展示了第 2 节中描述的理想共识属性:只要大多数服务器正常运行,Raft 就可以接受、复制并应用新的日志条目;在正常情况下,新的条目可以通过一次 RPC 回合复制到集群的大多数服务器;一个慢速的 Follower 不会影响性能。

5.4 安全性

前面描述了 Raft 如何选主和复制日志条目。然而不足以确保每个状态机按相同的顺序执行完全相同的命令。例如,某个 Follower 可能在 Leader 提交多个日志条目时不可用,之后它可能被选举为 Leader,并用新的条目覆盖这些条目;这样不同的状态机可能执行不同的命令序列。

本节通过对哪些服务器可以当选为 Leader 的限制,完善了 Raft 算法。这一限制确保了任何给定任期的 Leader 都包含了先前任期中已提交的所有条目(“Leader 完整性”属性)。

5.4.1 选举限制

在任何基于 Leader 的共识算法中,Leader 必须最终存储所有已提交的日志条目。在某些共识算法中,如 Viewstamped Replication [22],即使 Leader 最初不包含所有已提交的条目,也可以选举出 Leader。这些算法包含额外的机制,用于识别缺失的条目并在选举过程期间或之后将其传输到新 Leader。但这种方法会带来相当大的额外机制和复杂性【肯定有什么啥好处吧? 能感觉到的是选主速度加快】。Raft 使用了一种更简单的方法,保证每个新 Leader 从选举开始时就包含所有先前任期中已提交的条目,无需将这些条目传输给 Leader。这意味着日志条目只在一个方向上传输,从 Leader 到 Follower,Leader 从不覆盖其日志中已有的条目。

Raft 使用投票机制来防止 Candidate 在其日志不包含所有已提交条目的情况下当选为 Leader。RequestVote RPC 实现了这一限制:RPC 包含有关 Candidate 日志的信息,如果投票者的日志比 Candidate 的日志更加新,则会拒绝投票。

Raft 通过比较日志中最后条目的索引和任期来判断两个日志哪个更“最新”。如果两个日志的最后条目的任期不同,那么包含更晚任期的日志就是“更新”的。如果最后的条目在同一任期,则较长的日志更“更新”。

5.4.2 提交前任期的条目

如第 5.3 节所述,Leader 在将当前任期的条目复制到大多数服务器后知道该条目已提交。如果 Leader 在提交条目之前崩溃,未来的 Leader 会尝试完成条目的复制。然而,Leader 不能立即得出结论,即一旦某个来自前任期的条目存储在大多数服务器上,该条目就被提交。图 8 展示了一个情境,其中一个旧的日志条目已经存储在大多数服务器上,但仍然可能被未来的 Leader 覆盖。

image-20241111151515568

  • 图 8:一个时间序列展示了为什么 Leader 不能通过老任期日志来决定提交情况。在(a)中, S1 是 Leader,并且部分复制了索引 2 日志;在(b)中,S1 崩溃;S5 获得了 S3、S4 和自己的选票,并在日志索引 2 处接受了不同的条目。在(c)中,S5 崩溃;S1 重启,被选为 Leader 继续复制。此时,任期 2 中的日志被大多数服务器复制,但尚未提交。如果 S1 如(d)所示崩溃,S5 可以成为 Leader(获得 S2、S3 和 S4 的选票),并将该条目用其来自任期 3 的条目覆盖。然而,如果 S1 在崩溃前如(e)所示,在大多数服务器上复制了来自当前任期的条目,那么该条目将被提交(S5 无法赢得选举)。此时,日志中的所有前面的条目也将被提交。

为了消除像图 8 中的这种问题,Raft 从不通过统计 Majority 来决定是否 Commit 前任期的日志条目;只有来自 Leader 当前任期的日志条目才通过统计 Majority 来提交,因为日志匹配属性(Log Matching Property),所有先前的条目也会间接提交。【1、Leader 不能通过它本地日志来确定提交情况; 2、实际上只要某个任期的日志复制数达到了 Majority,它并不一定提交,能不能被提交只能通过它后面的日志被提交】

Raft 在提交规则中增加了这一复杂性,因为日志条目在 Leader 复制来自前任期的条目时会保留其原始的任期号。在其他共识算法中,如果新的 Leader 重新复制前一个“任期”的条目,必须使用其新的“任期号”进行复制。两个好处,第一,容易理解: Raft 的方法使得对日志条目进行推理变得更加容易,因为它们随着时间推移和跨日志保持相同的任期号。第二,传输量少:Raft 中的新 Leader 传输的来自前任期的日志条目比其他算法少(其他算法必须传输冗余的日志条目以重新编号它们后才能提交)。

5.4.3 安全性论证

我们用反证法证明, 假设任期 T 的 Leader(leaderTleader_T)提交了来自其任期的日志条目,但该条目没有被未来某个任期的 Leader 存储。考虑任期 U > T 的最小值,其中 Leader(leaderUleader_U)没有存储该条目。

image-20241111152446360

  • 图 9:如果 S1(任期 T 的 Leader)提交了来自其任期的新日志条目,并且 S5 被选为更晚任期 U 的 Leader,那么必须至少有一个服务器(S3)既接受了该日志条目,又为 S5 投了票。
  1. 该已提交的条目在 leaderUleader_U 的日志中必定不存在(Leader 永远不会删除或覆盖条目)。
  2. leaderTleader_T 将该条目复制到集群的大多数服务器,而 leaderUleader_U 从集群的大多数服务器中获得了选票。因此,至少有一台服务器(“投票者”)既接受了 leaderTleader_T 的条目,也投票给了 leaderUleader_U,如图 9 所示。投票者是达到矛盾的关键。
  3. 投票者必须在投票给 leaderUleader_U 之前接受了 leaderTleader_T 的已提交条目;否则,它将拒绝来自 leaderTleader_T 的 AppendEntries 请求(因为它的任期会大于 T)。
  4. 投票者在投票给 leaderUleader_U 时仍然存储该条目,因为每个中间的 Leader 都包含该条目(假设如此),Leader 永远不会删除条目,Follower 只会在条目与 Leader 冲突时删除条目。
  5. 投票者投票给了 leaderUleader_U,因此 leaderUleader_U 的日志必须至少和投票者的日志一样“最新”。这导致了两个矛盾。
  6. 首先,如果投票者和 leaderUleader_U 的最后一个日志任期相同,那么 leaderUleader_U 的日志必须至少与投票者的日志一样长,因此它的日志包含了投票者日志中的每个条目。这是一个矛盾,因为投票者包含了已提交的条目,而我们假设 leaderUleader_U 不包含它。
  7. 否则,leaderUleader_U 的最后一个日志任期必须大于投票者的任期。而且,它必须大于 T,因为投票者的最后一个日志任期至少是 T(它包含了来自任期 T 的已提交条目)。创建 leaderUleader_U 最后一个日志条目的较早的 Leader 必定包含了该已提交条目(假设如此)。然后,根据日志匹配属性,leaderUleader_U 的日志也必须包含该已提交条目,这是一个矛盾。
  8. 这完成了矛盾的证明。因此,所有大于 T 的任期的 Leader 必须包含任期 T 中已提交的所有条目。
  9. 日志匹配属性保证了未来的 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 无法继续执行。

Leader 选举是 Raft 中时序最为关键的部分。只要系统满足以下时序要求,Raft 就能够选举并维持一个稳定的 Leader:

broadcastTime ≪ electionTimeout ≪ MTBF

在这个不等式中,broadcastTime 是服务器向集群中每个服务器并行发送 RPC 并接收响应的平均时间;electionTimeout 是第 5.2 节中描述的选举超时;MTBF 是单个服务器的平均故障间隔时间。广播时间应该比选举超时小一个数量级,这样 Leader 就能可靠地发送心跳消息,防止 follower 开始选举;由于选举超时使用了随机化方法,这个不等式还使得选票分裂变得不太可能。选举超时应该比 MTBF 小几个数量级,以便系统能够稳定前进。当 Leader 崩溃时,系统会在大约选举超时的时间内不可用;我们希望这只占总体时间的一小部分。

广播时间和 MTBF 是底层系统的属性,而选举超时是我们需要选择的参数。Raft 的 RPC 通常要求接收方将信息持久化到稳定存储中,因此广播时间可能从 0.5 ms 到 20 ms 不等,具体取决于存储技术。因此,选举超时可能介于 10 ms 和 500 ms 之间。典型的服务器 MTBF 通常是几个月甚至更长,这完全满足时序要求。

6. 集群成员变更

到目前为止,我们假设集群配置(参与共识算法的服务器集)是固定的。实际上,偶尔需要更改配置,例如在服务器故障时替换服务器,或更改副本的数量。尽管可以通过将整个集群下线、更新配置文件然后重启集群来实现这一点,但这样会在变更期间使集群不可用。此外,如果涉及手动操作,也有可能出现操作员错误。为了避免这些问题,我们决定将配置变更自动化,并将其纳入 Raft 共识算法中。

为了使配置变更机制安全,过渡过程中必须不存在可能同时选举出两个 Leader 的情况。不幸的是,任何一种服务器从旧配置直接切换到新配置的方法都是不安全的。由于无法原子性地同时切换所有服务器,集群在过渡期间有可能分裂成两个独立的多数(参见图 10)。

image-20241111153549574

  • 图 10:直接从一个配置切换到另一个配置是不安全的,因为不同的服务器会在不同的时间切换。在这个例子中,集群从三个服务器扩展到五个服务器。不幸的是,在某些时刻,可能会为同一个任期选举出两个不同的 Leader,一个由旧配置(ColdC_{old})中的多数选出,另一个由新配置(CnewC_{new})中的多数选出。

为了确保安全,配置变更必须采用两阶段的方法。有多种方式可以实现这两个阶段。例如,一些系统(例如 [22])使用第一阶段禁用旧配置,使其无法处理客户端请求;然后,第二阶段启用新配置。在 Raft 中,集群首先切换到一个过渡配置,我们称之为联合共识(joint consensus);一旦联合共识被提交,系统就会过渡到新配置。联合共识结合了旧配置和新配置:

  • 日志条目被复制到两个配置中的所有服务器。
  • 任何来自旧配置或新配置的服务器都可以担任 Leader
  • 协议(用于选举和日志条目的提交)要求旧配置和新配置各自有独立的多数支持。

联合共识允许不同服务器在不同时间点之间过渡配置,而不会妥协安全性。此外,联合共识允许集群在配置变更期间继续处理客户端请求。

集群配置通过复制日志中的特殊条目进行存储和传递;图 11 展示了配置变更的过程。当 Leader 收到从 Cold 配置变更为 Cnew 配置的请求时,它会将联合共识配置(如图中所示的 Cold,new)作为日志条目存储,并使用先前描述的机制复制该条目。一旦某个服务器将新配置条目添加到其日志中,它将使用该配置进行所有未来的决策(服务器始终使用其日志中的最新配置,无论该条目是否已提交)。这意味着 Leader 将根据 Cold,new 规则来判断 Cold,new 日志条目是否已提交。如果 Leader 崩溃,可能会根据 Cold 或 Cold,new 规则选择新 Leader,具体取决于当选 Candidate 是否已经接收到了 Cold,new。在此期间,Cnew 无法单方面做出决策。

一旦 Cold,new 被提交,Cold 和 Cnew 都不能在没有对方批准的情况下做出决策,并且 Leader 完整性属性确保只有拥有 Cold,new 日志条目的服务器才能被选为 Leader。此时,Leader 可以安全地创建描述 Cnew 的日志条目并将其复制到集群。当新配置在 Cnew 规则下被提交时,旧配置将不再相关,且不在新配置中的服务器可以被关闭。如图 11 所示,在 Cold 和 Cnew 都不能单方面做出决策的时间段内,安全性得到了保证。

image-20241111154228374

  • 图 11:配置变更的时间线。虚线表示已经创建但尚未提交的配置条目,实线表示最新提交的配置条目。Leader 首先在其日志中创建 Cold,new 配置条目,并将其提交给 Cold,new(Cold 和 Cnew 的多数派)。然后,它创建 Cnew 条目,并将其提交给 Cnew 的多数派。在任何时刻,Cold 和 Cnew 都不能独立做出决策。

配置变更还需要解决另外三个问题。第一个问题是新服务器可能最初没有存储任何日志条目。如果它们在这种状态下被加入集群,可能需要相当长的时间才能赶上进度,在此期间可能无法提交新的日志条目。为了避免可用性中断,Raft 在配置变更之前引入了一个额外阶段,在此阶段中,新服务器作为非投票成员加入集群(Leader 会将日志条目复制给它们,但它们不计入多数派)。一旦新服务器赶上集群的其他部分,就可以按照上述描述进行重新配置。

第二个问题是集群 Leader 可能不属于新配置。在这种情况下,Leader 在提交 Cnew 日志条目后会下台(返回到 Follower 状态)。这意味着,在提交 Cnew 期间(即 Leader 正在提交 Cnew 时),Leader 会管理一个不包含自己的集群;它会复制日志条目,但不计入多数派。Leader 过渡发生在 Cnew 提交时,因为这是新配置可以独立运行的第一个时刻(此时可以从 Cnew 中选出 Leader)。在此之前,可能只有 Cold 中的服务器可以被选为 Leader。

第三个问题是被移除的服务器(不在 Cnew 中的服务器)可能会破坏集群。这些服务器将不会收到心跳信号,因此会超时并启动新的选举。它们随后会发送带有新任期号的 RequestVote RPC,这将导致当前 Leader 退回到 Follower 状态。最终会选出一个新的 Leader,但被移除的服务器将再次超时,流程会重复,导致可用性下降。为了防止这个问题,服务器会忽略它认为当前 Leader 存在的 RequestVote RPC。具体来说,如果服务器在听到当前 Leader 的心跳信号后的最短选举超时时间内收到 RequestVote RPC,它不会更新其任期号或授予投票。这不会影响正常的选举,其中每个服务器在开始选举之前都会等待至少一个最短的选举超时。但它有助于避免被移除服务器带来的干扰:如果 Leader 能够将心跳信号发送到集群中,则它不会被更大的任期号推翻。

7. 日志压缩

Raft 的日志在正常操作过程中会增长,以包含更多的客户端请求,但在实际系统中,日志不能无限增长。随着日志变得越来越长,它占用更多的空间,并且需要更多的时间来重放。这最终会导致可用性问题,因此需要某种机制来丢弃日志中已经过时的信息。

**快照(Snapshotting)**是压缩的最简单方法。在快照机制中,整个当前系统状态会被写入到稳定存储的快照中,然后将此时的整个日志丢弃。Chubby 和 ZooKeeper 等系统都使用了快照,以下部分将描述 Raft 中的快照机制。

增量压缩方法,例如日志清理(log cleaning)和日志结构化合并树( log-structured merge tree,LSM 树)[30, 5],也是可能的。这些方法一次只处理一部分数据,因此它们能够更加均匀地分配压缩负载。它们首先选择一个已积累了许多已删除和被覆盖的对象的数据区域,然后以更紧凑的方式重写该区域的活动对象,并释放该区域。与快照机制相比,这需要更复杂的额外机制,因为快照简化了问题,始终操作整个数据集。虽然日志清理需要对 Raft 进行修改,但状态机可以使用与快照相同的接口实现 LSM 树。

image-20241111154503545

  • 图 12: 一个服务器用新的快照替换它日志中已提交的条目(索引 1 到 5),该快照只存储当前的状态(例如此示例中的变量 x 和 y)。快照的最后包含索引和术语用于将快照定位到日志中的第 6 条目之前。

图 12 展示了 Raft 中快照的基本概念。每个服务器独立地创建快照,覆盖其日志中已提交的条目。大部分工作是由状态机将当前状态写入快照。Raft 在快照中还包含少量元数据:最后包含的索引是该快照替换的日志中最后一个条目的索引(即状态机已应用的最后一个条目),最后包含的任期是该条目的任期。这些信息被保留以支持 AppendEntries 一致性检查,因为接下来的日志条目需要一个先前的日志索引和任期。为了支持集群成员变化(第 6 节),快照还包括截至最后包含索引时日志中的最新配置。完成快照写入后,服务器可以删除所有通过最后包含索引的日志条目,以及任何先前的快照。

尽管服务器通常独立创建快照,但 Leader 必须偶尔向滞后的 Follower 发送快照。这发生在 Leader 已经丢弃了它需要发送给 Follower 的下一个日志条目时。幸运的是,在正常操作中,这种情况不太可能发生:一个已经跟上 Leader 进度的 Follower 应该已经有该条目。然而,异常慢的 Follower 或新加入集群的服务器(第 6 节)则不会。为了使这类 Follower 保持最新,Leader 将通过网络发送快照。

image-20241111154557970

Leader 使用一个新的 RPC,称为 InstallSnapshot,将快照发送给那些落后的 Follower;见图 13。当 Follower 接收到这个 RPC 的快照时,必须决定如何处理其现有的日志条目。通常,快照将包含一些尚未在接收者日志中的新信息。在这种情况下,Follower 丢弃整个日志;因为这些条目已经被快照取代,且可能存在与快照冲突的未提交条目。如果接收者收到的快照描述了其日志的前缀(由于重传或错误),则快照所覆盖的日志条目将被删除,但在快照之后的条目仍然有效,必须保留。

这种快照方法与 Raft 的强 Leader 原则有所不同,因为 Follower 可以在不知晓 Leader 的情况下进行快照。然而,我们认为这一改变是合理的。虽然 Leader 有助于避免在达成一致时发生冲突,但在进行快照时一致性已经达成,因此不会发生冲突。数据仍然只从 Leader 流向 Follower。

我们考虑了另一种基于 Leader 的方法,其中只有 Leader 创建快照,然后将快照发送给每个 Follower。然而,这有两个缺点。首先,将快照发送给每个 Follower 会浪费网络带宽并减慢快照过程。每个 Follower 已经拥有生成自己快照所需的信息,通常从本地状态生成快照比通过网络发送和接收快照要便宜得多。其次,Leader 的实现会更加复杂。例如,Leader 需要与复制新日志条目同时并行发送快照,以免阻塞新的客户端请求。

还有两个影响快照性能的问题。首先,服务器必须决定何时进行快照。如果服务器快照过于频繁,会浪费磁盘带宽和能源;如果快照过于不频繁,则可能会耗尽存储空间,并增加重启时重放日志所需的时间。一个简单的策略是,当日志达到固定大小(以字节为单位)时进行快照。如果此大小设置得远大于快照的预期大小,那么快照的磁盘带宽开销将很小。

第二个性能问题是写入快照可能需要相当长的时间,我们不希望这延迟正常操作。解决方法是使用写时复制技术,这样新更新就能被接受,而不会影响正在写入的快照。例如,使用函数式数据结构构建的状态机自然支持这一点。或者,操作系统的写时复制支持(例如,Linux 的 fork)可以用来创建整个状态机的内存快照(我们的实现使用了这种方法)。

8. 客户端交互

本节描述了客户端如何与 Raft 交互,包括客户端如何找到集群的 Leader,以及 Raft 如何支持线性化语义。这些问题适用于所有基于共识的系统,Raft 的解决方案与其他系统类似。

Raft 的客户端将所有请求发送给 Leader。当客户端首次启动时,它会连接到一个随机选择的服务器。如果客户端的第一个选择不是 Leader,该服务器将拒绝客户端的请求,并提供它所知道的最新 Leader 的信息(该服务器从 AppendEntries 请求得知的 Leader 的网络地址)。如果 Leader 崩溃,客户端请求将超时;然后,客户端会重新尝试与随机选择的服务器连接。【所有请求都要通过主,感觉性能不会太好,怎么优化】

Raft 的目标是实现线性化语义(每个操作似乎在某个时间点立即执行,且仅执行一次,在调用和响应之间的某个时刻)。然而,正如前面所描述,Raft 可能会多次执行一个命令:例如,如果 Leader 在提交日志条目后但在响应客户端之前崩溃,客户端将重新尝试该命令并选择新 Leader,导致该命令第二次执行。解决方案是客户端为每个命令分配一个序列号。然后,状态机跟踪每个客户端处理的最新序列号,并与相应的响应一起记录。如果收到一个序列号已执行的命令,它会立即响应而不重新执行请求。

只读操作可以在不向日志写入任何内容的情况下处理。然而,如果没有额外的措施,这可能会返回过时的数据,因为响应请求的 Leader 可能已经被一个新的 Leader 取代,而它对此一无所知。线性化读取必须不返回过时数据。Raft 需要采取两个额外的预防措施来保证这一点,而不使用日志。首先,Leader 必须拥有最新的已提交条目的信息。Leader 完整性属性保证 Leader 拥有所有已提交的条目,但在其任期开始时,它可能不知道哪些条目已提交。为此,它需要提交来自自己任期的一条空白的无操作(no-op)条目。Raft 通过让每个 Leader 在其任期开始时提交一条无操作条目来处理这一点。其次,在处理只读请求之前,Leader 必须检查它是否已经被废除(如果一个更近期的 Leader 被选举出来,可能存在它的信息过时的情况)。Raft 通过让 Leader 在响应只读请求之前与集群大多数服务器交换心跳消息来处理这一点。或者,Leader 可以依赖心跳机制提供一种租约(lease)形式,但这会依赖于时间的安全性(假设时钟偏差有界)。【租约就是在一定时间窗口内,避免并发决策,减少通信开销】

9 实现与评估

我们已经将 Raft 实现为一个复制的状态机,存储 RAMCloud 的配置信息,并协助 RAMCloud 协调器的故障转移。Raft 的实现包含大约 2000 行 C++代码,不包括测试、注释或空行。源代码是公开的 [23]。目前大约有 25 个独立的第三方开源 Raft 实现 [34],它们处于不同的开发阶段,基于本论文的草稿。此外,各种公司正在部署基于 Raft 的系统 [34]。

本节其余部分将通过三个标准来评估 Raft:可理解性、正确性和性能。

9.1 可理解性

为了衡量 Raft 相对于 Paxos 的可理解性,我们进行了一项实验研究,参与者为斯坦福大学的高级操作系统课程和加州大学伯克利分校的分布式计算课程的本科生和研究生。我们录制了 Raft 和 Paxos 的讲座视频,并设计了相应的测验。Raft 的讲座涵盖了本论文中的内容(除了日志压缩),而 Paxos 的讲座涵盖了足够的材料来创建一个等效的复制状态机,包括单条 Paxos、多条 Paxos、重新配置和一些实际应用中所需的优化(如 Leader 选举)。测验测试了算法的基本理解,并要求学生推理关于角落案例的知识。每个学生观看了一个视频,参加了相应的测验,之后观看了第二个视频并参加了第二个测验。约有一半的参与者先做 Paxos 部分,另一半先做 Raft 部分,以此来考虑个体差异和从第一部分学习中获得的经验。

我们尽量使 Paxos 和 Raft 之间的比较尽可能公平。实验有两个方面偏向 Paxos:43 名参与者中有 15 名报告曾有 Paxos 经验,而且 Paxos 视频比 Raft 视频多了 14%的时长。如表 1 所示,我们已采取措施来减少潜在的偏差。所有材料都可以供审查 [28, 31]。

平均而言,参与者在 Raft 测验中的得分比在 Paxos 测验中的得分高出 4.9 分(满分 60 分,Raft 的平均得分为 25.7,Paxos 的平均得分为 20.8);图 14 显示了他们的个别得分。配对 t 检验表明,Raft 得分的真实分布的均值至少比 Paxos 得分高 2.5 分,置信度为 95%。

我们还创建了一个线性回归模型,根据三因素预测新学生的测验得分:他们选择的测验、他们的 Paxos 经验程度以及他们学习算法的顺序。该模型预测选择测验会使 Raft 得分高出 12.5 分。这比实际观察到的 4.9 分的差异要大,因为许多实际学生拥有 Paxos 经验,这帮助了 Paxos,而对 Raft 的帮助则较小。奇怪的是,该模型还预测,已参加 Paxos 测验的学生在 Raft 测验中的得分低了 6.3 分;尽管我们不清楚原因,但这似乎具有统计显著性。

我们还在测验后对参与者进行了调查,了解他们认为哪个算法更容易实现或解释;这些结果如图 15 所示。绝大多数参与者认为 Raft 更容易实现和解释(41 人中有 33 人回答“Raft”)。然而,这些自我报告的感受可能不如参与者的测验成绩可靠,参与者可能会受到我们假设 Raft 更易理解的偏见影响。

关于 Raft 用户研究的详细讨论可在[31]中查看。

9.2 性能

Raft 的性能与其他共识算法(如 Paxos)相似。对于性能而言,最重要的场景是已经建立的 Leader 在复制新的日志条目时。Raft 通过最少的消息数来实现这一点(即从 Leader 到集群一半节点的单次往返)。此外,Raft 的性能还有进一步提升的空间。例如,它容易支持批处理和流水线请求,以提高吞吐量并降低延迟。许多优化方案已经在文献中提出,其他算法的许多优化也可以应用于 Raft,但我们将这些留待未来的工作中进行探讨。

我们使用 Raft 实现来测量 Raft 的 Leader 选举算法的性能,并回答两个问题:第一,选举过程是否快速收敛?第二,Leader 崩溃后可以实现的最小停机时间是多少?

为了测量 Leader 选举,我们反复使一个包含五个服务器的集群中的 Leader 崩溃,并记录检测崩溃并选举新 Leader 所需的时间(见图 16)。为了生成最坏情况,测试中的每个服务器日志长度不同,因此有些 Candidate 不符合成为 Leader 的条件。此外,为了鼓励分裂选票,我们的测试脚本在 Leader 终止之前触发了同步的心跳 RPC 广播(这近似于 Leader 在崩溃前复制新的日志条目时的行为)。

图 16 中的上图显示,选举超时的少量随机化足以避免选举中的分裂选票。在没有随机化的情况下,由于许多分裂选票,Leader 选举在我们的测试中始终超过了 10 秒。只添加 5ms 的随机化就能显著改善,导致中位停机时间为 287ms。增加更多随机化进一步改善了最坏情况的表现:在 50ms 的随机化下,最坏情况下的完成时间(经过 1000 次试验)为 513ms。

图 16 中的下图显示,通过减少选举超时可以缩短停机时间。在 12-24ms 的选举超时下,选举 Leader 平均只需要 35ms(最长的试验用了 152ms)。然而,如果进一步降低超时,则会违反 Raft 的时间要求:Leader 很难在其他服务器开始新一轮选举之前广播心跳,这可能导致不必要的 Leader 更换,降低系统整体可用性。我们建议使用保守的选举超时,如 150-300ms;这样的超时不太可能导致不必要的 Leader 更换,并且仍能提供良好的可用性。

image-20241111154755907

  • 图 16:检测并替换崩溃 Leader 的时间。 上方图表显示了选举超时中的随机性变化,底部图表则展示了最小选举超时的变化。每条线代表 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 使用基于 Leader 的方法,与 Raft 有许多相似之处。

Raft 和 Paxos 的最大区别在于 Raft 的强领导性:Raft 将 Leader 选举作为共识协议的核心部分,并尽可能将所有功能集中在 Leader 中。这种方法导致了更简单的算法,更容易理解。例如,在 Paxos 中,Leader 选举是与基本共识协议正交的:它仅作为一种性能优化,并不直接要求达成共识。然而,这也带来了额外的机制:Paxos 包含了一个用于基本共识的两阶段协议,以及一个单独的 Leader 选举机制。相比之下,Raft 将 Leader 选举直接整合进共识算法,并作为共识的两个阶段之一。这减少了比 Paxos 更复杂的机制。

像 Raft 一样,VR 和 ZooKeeper 都是基于 Leader 的,因此在许多方面与 Paxos 相比具有 Raft 的优势。然而,Raft 比 VR 或 ZooKeeper 的机制更少,因为它最大限度地减少了非 Leader 中的功能。例如,Raft 中的日志条目仅向一个方向流动:通过 AppendEntries RPC 从 Leader 向外流动。而在 VR 中,日志条目在两个方向上流动(Leader 可以在选举过程中接收日志条目);这导致了额外的机制和复杂性。ZooKeeper 的已发布描述也将日志条目传输到 Leader 并从 Leader 传输,但实现显然更像 Raft [35]。

Raft 拥有比我们所知的其他共识日志复制算法更少的消息类型。例如,我们统计了 VR 和 ZooKeeper 用于基本共识和成员变更的消息类型(不包括日志压缩和客户端交互,因为这些与算法几乎独立)。VR 和 ZooKeeper 各自定义了 10 种不同的消息类型,而 Raft 只有 4 种消息类型(两种 RPC 请求及其响应)。Raft 的消息比其他算法更简洁,但它们整体上更简单。此外,VR 和 ZooKeeper 的描述中涉及在 Leader 变化时传输整个日志;为了使这些机制变得实用,还需要定义更多的消息类型来优化这些机制。

Raft 的强领导性方法简化了算法,但也排除了某些性能优化。例如,平等 Paxos(EPaxos)在某些条件下可以通过无 Leader 的方法实现更高的性能 [27]。EPaxos 利用状态机命令的可交换性。只要其他并发提议的命令与之可交换,任何服务器都可以通过一次通信轮次来提交命令。然而,如果并发提议的命令彼此不交换,EPaxos 就需要额外的一轮通信。由于任何服务器都可以提交命令,EPaxos 在服务器之间良好地平衡负载,并能够在广域网环境中比 Raft 实现更低的延迟。但它也为 Paxos 增加了显著的复杂性。

在其他工作中,已经提出或实现了几种集群成员变更的方法,包括 Lamport 最初的提案 [15]、VR [22] 和 SMART [24]。我们为 Raft 选择了联合共识方法,因为它利用了共识协议的其余部分,因此对成员变更所需的额外机制非常少。Lamport 的基于 α 的方法对 Raft 来说不可行,因为它假设无需 Leader 即可达成共识。与 VR 和 SMART 相比,Raft 的重新配置算法有一个优势,那就是成员变更可以在不限制正常请求处理的情况下进行;相比之下,VR 在配置变更期间会停止所有正常处理,而 SMART 对待未完成请求的数量有类似于 α 的限制。Raft 的方法也比 VR 或 SMART 更少的机制。

11 结论

算法通常是以正确性、效率和/或简洁性为主要目标设计的。尽管这些目标都值得追求,我们认为可理解性同样重要。在开发人员将算法转化为实际实现之前,其他目标无法实现,而实际实现不可避免地会偏离和扩展原始形式。除非开发人员对算法有深入的理解,并能够建立起相关的直觉,否则他们很难在实现中保留算法的期望特性。

本文解决了分布式共识的问题,Paxos 是一个广泛接受但难以理解的算法,多年来一直困扰着学生和开发人员。我们开发了一种新的算法,Raft,我们已经证明它比 Paxos 更易于理解。我们还认为 Raft 为系统构建提供了更好的基础。将可理解性作为主要设计目标改变了我们设计 Raft 的方式;随着设计的进展,我们发现自己反复使用了一些技术,比如分解问题和简化状态空间。这些技术不仅提高了 Raft 的可理解性,还使我们更容易验证其正确性。


FAQ(Raft 答疑整理)

整理自 raft-faq.txt(part1,选举/日志/网络)与 raft2-faq.txt(part2,配置变更/快照/只读)。

用途与定位

  • Raft 用来做什么? 构建容错的配置服务(追踪谁负责什么、选 primary 防脑裂);Chubby/ZooKeeper/etcd 用 Raft/Paxos;也用于复制数据/管理分片分配。
  • 为简单牺牲了什么? 全部操作要落盘、每个 follower 同时只能有一个 AppendEntries 在飞、快照写全量状态、恢复副本要收完整快照、单线程执行限制多核。
  • 为什么学 Raft 不学裸 Paxos? 有一篇清晰且足够详细、描述完整复制服务的论文;而"如何基于 Paxos 建完整复制服务器系统"缺少令人满意的论文。
  • Raft 与 VMware FT 关系? Raft 无单点;VMware FT 的 test-and-set 服务器是瓶颈,故 Raft 更容错。但 VMware FT 复制任意 VM,Raft 是需按应用定制的库。
  • 拜占庭条件会破坏 Raft 吗? 会。Raft 假设非拜占庭(fail-stop,要么守协议要么停);恶意/有 bug 的节点会返回错误结果。生产中用防火墙 + 消息签名防外部攻击。

选举与多数派

  • 网络分区会出现两个 leader 吗? 不会。新 leader 需 RequestVote 拿到多数票,少数派分区选不出,老 leader 在少数派也凑不齐多数。
  • "多数"指活的还是全部? 永远是全部服务器(含死的)的多数。5 台坏 2,候选人仍需 3 票——保证任意两多数派相交、防脑裂。
  • 选举超时太短会怎样? 影响活性不影响安全:太短→AppendEntries 还没到就反复选举、浪费时间;太长→leader 挂后停顿久。
  • 为什么随机化选举超时? 减少多个节点同时成为候选人、分裂选票导致谁都拿不到多数。
  • votedFor 检查的作用? 保证每个 term 每个 follower 只投一票,防分票。
  • leader 除了崩溃还会下台吗? 会:CPU 慢/网络断/丢包,别人收不到心跳就发起选举,把它换掉。
  • 选举期间服务不可用严重吗? 客户端可见停顿约 0.1 秒级,且故障罕见,对多数应用影响小。

日志、提交与回滚

  • leader 复制前崩溃,请求会丢吗? 会(未提交项不跨 leader 保留)。但客户端没收到回复就会超时重发,系统需防重复请求。
  • follower 何时把日志项交给状态机? 仅当 leader 通过 AppendEntries 的 leaderCommit 表明已提交后,follower 才执行(送上 applyCh)。
  • leader 该等 AppendEntries 回复吗? 不该串行等。并发发给各 follower,收到回复就计数,达多数(含自己)才标记提交。
  • 日志为何 1-indexed? 概念上第 0 项是 dummy(index=0,term=0),使第一个 AppendEntries 的 PrevLogIndex=0 是合法下标。
  • 快速回滚(§5.3)是什么? follower 返回冲突项的 term 和该 term 首项 index,leader 据此成批回退 nextIndex,比一次退一格快。
  • 为什么新 leader 任期开始要提交一个 no-op? 新 leader 无法确定已有日志项是否已提交;提交一个本任期新项可保证其前面所有项都已提交。

配置变更与快照

  • C_old / C_new 是什么? 旧/新集群的服务器集合(地址/名字)。
  • 联合共识 C_{old,new} 怎么过渡?为何 leader 要下台? 过渡期 leader 需同时拿到新旧两个多数,防两套配置各行其是;提交 C_new 后不在新配置里的 leader 必须下台(不再参与)。
  • 快照里存的是谁的状态? 用 Raft 的服务(如 KV 模块)保存自己的状态表,不是客户端应用。
  • 快照覆盖的日志前缀删掉不会丢操作吗? 不会,快照已含所有被删操作的效果。
  • InstallSnapshot 带宽大吗? 大。leader 应保留足够日志,使常见故障只需 AppendEntries 而非传整快照。
  • 写快照比选举超时还久怎么办? 后台写、不阻塞;让快照频率低于最坏写时间。
  • copy-on-write 怎么帮快照? fork 后内存页标记写时复制,只有真写时才复制,避免整份内存拷贝。

只读与杂项

  • 只读操作为何能不走日志? 读远多于写,绕过日志更省时省存。
  • leader 处理只读前为何要和多数交换心跳? 发(通常空的)AppendEntries 给各 peer,多数回复后才答读——确认自己仍是 leader、未被分区出局。
  • 半数以上服务器死了会怎样? 服务无法推进、反复尝试选举;待足够多带持久状态的服务器恢复,重新选出 leader 继续。
  • §5.4.3 是完整证明吗? 不是,完整证明见 Ongaro 博士论文与 Verdi 验证项目。