Archipelago

Lec 6Paxos

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

摘要

问题的背景: 容错地"达成一致"——要用复制做高可用、表现得像单台机一样强一致。难点是脑裂:主备在网络分区时,两边都以为对方挂了、都自封 primary,破坏一致性。根因——机器无法区分"对方崩了"与"网络断了",只能观察到"请求没回应"。在 ~1989 年前,自动容错切换被认为不可能(只能人工介入,而人又成了单点)。

Lec 6 Paxos

问题的背景: 容错地"达成一致"——要用复制做高可用、表现得像单台机一样强一致。难点是脑裂:主备在网络分区时,两边都以为对方挂了、都自封 primary,破坏一致性。根因——机器无法区分"对方崩了"与"网络断了",只能观察到"请求没回应"。在 ~1989 年前,自动容错切换被认为不可能(只能人工介入,而人又成了单点)。

Paxos 协议

Paxos 如何防止脑裂?

对于 Paxos 来说,脑裂(split brain)意味着不同服务器最终“达成一致”的值不同。Paxos 中所有需要等待**多数派(majority)**响应的地方,正是防止脑裂的关键,

一、多数派原则

定义

多数派(也称法定多数, quorum)的两个数学性质用奇数台(3/5/7),任何操作要多数同意才推进。① 任意两个多数派必相交(于是状态变更不会丢失);② 只有一个分区能含多数派(少数派分区无法推进 → 防脑裂)。容错算术:2f+1 台容忍 f 台故障(3 容 1、5 容 2)。

三、单值 Paxos(single-decree)

角色:

  • proposer(任意服务器,在尚未达成一致时可提议;选唯一且单调递增的提案号 n)、
  • acceptor(应答提议、状态须持久化跨重启)、
  • learner(学习最终值)。

acceptor 须持久化的状态:

  • n_p(见过的最高 prepare 号)、
  • n_a, v_a(已接受的最高 accept 号与值)。

两阶段协议

Phase 1(prepare/promise):proposer 选 n,发 prepare(n)。acceptor 收到:若 n > n_p 则 n_p=n、回 prepare_ok(n_a, v_a),否则拒。proposer 收到多数 prepare_ok 后,采用其中 n_a 最高的那个 v_a 作为要提的值 v′(若都没有已接受值,才用自己的值)。

Phase 2(accept/accepted):proposer 发 accept(n, v′)。acceptor 收到:若 n ≥ n_p 则 n_p=n; n_a=n; v_a=v′、回 accept_ok,否则拒。proposer 收到多数 accept_ok 后,值被选定(chosen),广播 decided(v′)。

四、三条安全规则(为什么正确)

核心保证:一旦某值被 chosen,之后任何选定都等于它(系统不会"改主意")。

靠三条规则:

  • proposer 必须采用见到的最高 n_a 的 v_a:若先前已有值可能被选定,多数派的 prepare_ok 里一定有人报告它 → 新提案被迫沿用它,保住已选值。

  • accept 处理要求 n ≥ n_p:防止旧提案号盖掉新的(否则 A→B 的安全违例会发生)。

  • accept 时也更新 n_p = n:acceptor 可能没见过 prepare(n) 就收到 accept(n,v),不更新 n_p 会让更老的提案"复活"。

原则:只要有证据表明"可能已经达成一致",就必须当作真的达成了来行事(保守性保证安全)。崩溃恢复也要靠多数派相交 + 持久化 n_p/n_a/v_a。

五、活性:决斗提议者与 leader

dueling proposers(决斗提议者)

两个 proposer 交替用更高的 n 互相打断对方的 accept,谁都凑不齐多数 → 活锁。Paxos 安全永远成立,但活性是"弱"的:需要选出单一 leader(唯一活跃 proposer)来打破对称——leader 选举本身又可用 Paxos,或用"最小 IP 当 leader + 其余随机延迟"等办法。Paxos 本身不要求 leader(§3 是可选实现策略)。

六、多值 Paxos:复制状态机(RSM)

真实系统要的是一连串一致(一个日志),不是单个值:客户端把操作发给任一副本 → 副本为它选一个日志槽位 → 对该槽位跑一遍 Paxos → 全体在该位置同意同一操作 → 提交后各副本按日志顺序执行。日志的作用:管并发(后面操作等前面提交)、让掉队副本靠重放追平、便于快照。

与 Raft 的关系:Raft 是为可实现性而简化的 Paxos 后继——保留同样的安全保证,但逻辑更清晰(强 leader + 日志连续性)。本课实验用 Raft 而非裸 Paxos。

FAQ(Paxos 答疑整理)

来自 paxos-faq.txt。

  • Paxos 如何防脑裂? 多个 proposer 都要拿到多数回应,而多数必相交 → 相交处的服务器会告诉落败者它输了;分区时只有含多数的那侧能达成一致。
  • 怎么保证提案号唯一? 在号的低位嵌唯一标识(如 IP);各 proposer 记自己用过的最高号并保证新号更大;也可"时间高位+随机低位"(随机位不足会撞)。
  • §2.4 的 distinguished proposer 是什么? 鼓励同时只有一个活跃提议以加快达成——如每个 proposer 发 prepare 前随机等一会儿打破对称,让一个先完成。
  • 算法怎么终止? Paxos 没有形式化终止点;无故障时,proposer 看到所有服务器的 accept 回复即可安全断定已达成、停止提议。
  • Paxos 的用例? 提升容错的复制服务:Chubby、Spanner、Megastore、Spinnaker、ZooKeeper 等。
  • Paxos 多快? 受消息往返与磁盘写限制——至少两轮消息(数据中心内数百微秒)+ 两次磁盘写(数百微秒到数十毫秒),未优化时较慢。
  • 第 9 页说的 leader 是什么?Paxos 有 leader 吗? Paxos 本身不要求 leader;§3 是可选实现策略,leader 可由 Paxos 自己选或用"最小 IP + 随机延迟",主要为避免同时竞争多轮。

论文阅读: Paxos make simple

1. 引言

Paxos 算法用于实现容错分布式系统,一直被认为难以理解。但事实上,它是分布式算法中最简单和最直观的一种。其核心是一个共识算法——对单个值达成一致(单值 Paxos,single-decree)。最后一节解释了完整的 Paxos 算法(多值 Paxos),它是通过将共识算法应用于状态机方法而得到的。

2. 共识算法

2.1 本质问题

假设有一组可以发起提议(propose)的进程。共识算法确保如果有一个值被选出,那么进程应该能够得知该值。共识算法的安全性要求就是:

  1. 只有被提出的值才能被选择
  2. 只能选择一个值
  3. 一个未被选择的值,任何进程都不会认为它被选择了

简而言之, 目标是确保最终会选择某个提出的值,并且如果某个值已经被选择,进程最终能够得知该值。

Paxos 共识算法中有三个角色, 一个进程可能扮演多个角色

  • 提议者(proposers):
  • 接受者(acceptors):
  • 学习者(learners)

角色之间的通信可能不可靠,可能时间特别长;可能发生重启等

2.2 选择一个值

选择一个值最简单的方法就是, acceptor 选择它接收到的第一个提议值。虽然这种方法简单,但它并不理想,因为 acceptor 的失败会使得共识的达成陷入死局(达不成多数派)

因此,我们尝试另一种选择值的方法。一个 proposer 将提议值发送给一组 acceptors。当足够多的 acceptors 接受了该值时,该值被选择。

在没有失败或消息丢失的情况下,我们希望即使只有一个 proposer 提出了一个值,仍然能选择一个值。这提出了以下要求:

[!IMPORTANT]

P1. 接受者必须接受它接收到的第一个提议。

但是这个要求会引出一个问题。不同的 proposers 可能会在几乎相同的时间提出多个值,导致每个 acceptor 都接受了一个值,但没有单一的值被大多数接受者接受。单个 acceptor 的失败可能导致无法知道哪个值被选择。

结合两个要求——P1 和前面提出的,大多数 acceptors 接收时才能选择的要求,说明,必须允许一个 acceptor 接受多个提议。我们可以为每个提议分配一个编码来跟踪,一个接受者可能接受的不同提议,因此一个提议是<pid, val>的所以目前我们只假设这一点。一个值被选择,当且仅当某个提议(以及其值)被大多数 acceptors 接受时。此时,我们称该提议(以及其值)已被选择

我们可以允许多个提议被选择,但我们必须保证所有被选择的提议具有相同的值。通过对提议编号进行归纳,我们只需要保证:

[!IMPORTANT]

P2. 如果某个提议的值为 v 被选择,那么每个编号更高的被选择提议的值都必须是 v。

由于编号是完全有序的,条件 P2 保证了关键的安全性属性:只有一个值会被选择。

为了被选择,一个提议必须至少被一个 acceptor 接受。因此,我们可以通过满足以下条件来保证 P2:

[!IMPORTANT]

P2aP2^a. 如果某个提议的值为 v 被选择,那么每个编号更高的由任何接受者接受的提议都必须具有值 v。

我们仍然保持 P1,以确保某个提议会被选择。由于通信是异步的,某个提议可能被选择,而某个特定的 acceptor c 从未收到任何提议。假设一个新的 proposer“醒来”并发出了一个更高编号的、不同值的提议。P1 要求 c 接受这个提议,这会违反 P2aP2^a。

同时保持 P1 和 P2aP2^a 需要强化 P2aP2^a,具体来说,要求:

[!IMPORTANT]

P2bP2^b: 如果某个提议的值为 v 被选择,那么每个编号更高的由任何提议者发出的提议都必须具有值 v。

由于一个提议必须由提议者发出,才能被 acceptor 接受,P2bP2^b 隐含了 P2aP2^a,而 P2aP2^a 又隐含了 P2。

为了理解如何满足 P2bP2^b,我们考虑如何证明它成立。我们假设某个编号为 m、值为 v 的提议被选择,并证明任何编号为 n > m 的提议也必须具有值 v。为了简化证明,我们使用对 n 的归纳法,这样我们可以在额外假设所有编号在 m 到 n-1 之间的提议值均为 v 的情况下,证明编号为 n 的提议值也为 v。为了使编号为 m 的提议被选择,必须有一个包含大多数 acceptor 的集合 C,确保集合 C 中的每个 acceptor 都接受了该提议。将这一点与归纳假设结合,假设 m 被选择意味着:

[!IMPORTANT]

P2cP2^c: 对于任何值 v 和编号 n,如果发出一个编号为 n、值为 v 的提议,则存在一个包含大多数 acceptors 的集合 S,满足以下条件之一:

  1. S 中没有 acceptor 接受过任何编号小于 n 的提议;或者
  2. v 是所有编号小于 n 的提议中,S 中 acceptor 所接受的最高编号提议的值。

因此,我们可以通过保持 P2cP2^c 的不变性来满足 P2bP2^b

为了保持其不变性,想要发出编号为 n 的提议的 proposer 必须了解每个 acceptor 在某个大多数 acceptors 这集合中已经接受或者将要接受的编号小于 n 的最高编号提议(如果有的话)。了解已经接受的相对容易,但是预测未来接受的情况就比较苦难。为了避免预测,proposer 通过提取一个承诺来控制它,即请求 acceptor 承诺不再接受任何编号小于 n 的提议。这个过程引出了以下提议发出算法:

  1. proposer 选择一个新的提议编号 n,并向一些acceptors 集合中的每个成员发送请求,请求其回复:

    (a) 一个承诺,永远不会再接受任何编号小于 n 的提议; (b) 如果有的话,回复它已接受的编号小于 n 的最高编号提议。

    我将这样的请求称为编号为 n 的“准备请求”。

  2. 如果 proposer 从大多数 acceptors 那里收到了请求的响应,那么它可以发出编号为 n、值为 v 的提议,其中 v 是所有响应中提到的提议的最高编号提议的值,或者如果响应者报告没有提议,则选择提议者自己选择的值。proposer 通过向某些 acceptors 集合发送请求,要求接受提议来发出提议(这个接受者集合不必与最初回应请求的接受者集合相同)。我们称这种请求为“接受请求”。

这描述了 proposer 的算法。那么,接受者该如何操作呢?它可以从提议者接收两种类型的请求:准备请求和接受请求。acceptor 可以忽略任何请求而不会影响安全性。因此,我们只需要说明在何时可以响应请求。它可以始终响应准备请求。它可以响应接受请求,并接受提议,当且仅当它没有承诺不接受该提议。换句话说:

[!IMPORTANT]

P1aP1^a: 接受者可以接受编号为 n 的提议,当且仅当它没有回应过编号大于 n 的准备请求。

注意,P1aP1^a 包含了 P1。

现在我们有了一个完整的值选择算法,能够满足所要求的安全性属性——假设提议编号是唯一的。最终算法通过一个小的优化来完成。

假设一个 acceptor 接收到编号为 n 的准备请求,但它已经响应了编号大于 n 的准备请求,从而承诺不接受任何为 n 的新提议,因此它不会接受,也没有理由回应这个准备请求。同时,acceptor 也会忽略已经接受过的提议的准备请求

通过这个优化,acceptor 只需要记住它曾经接受的最高编号的提议以及它回应过的编号最高的准备请求。由于P2cP2^c必须保持不变性,无论何时发生故障,acceptor 必须记住这些信息。注意,proposer 可以随时放弃一个提议并忘记他——只要它不再发出相同编号提议。

将提议者和接受者的操作结合起来,就可以理清算法的两个阶段:

阶段 1:

  1. proposer 选择一个编号为 n 的提议,并向大多数 acceptor 发送编号 n 的准备请求
  2. 如果接受者接收到了编号为 n 的准备请求,并且该编号大于它已经响应过的任何准备请求编号,则它会回应请求,并承诺不再接受任何小于 n 的提议,并返回它已经接受的最高编号提议(如果有的话)。

阶段 2:

  1. 如果 proposer 从大多数 acceptors 那里接收到了编号为 n 的准备请求的响应,那么他会向这些 acceptor 发送编号为 n 且值为 v 的接受请求,其中 v 是所有响应中提到的最高编号提议的值,如果没有报告提议,则选择 proposer 自己选择的值。
  2. 如果 acceptor 接收到编号为 n的接受请求,他会接受该提议,除非它已经回应过一个大于 n 的准备请求

proposer 可以进行多个提议,只要它遵循每个提议的算法。它可以在协议的任何时候放弃一个提议。(即使该提议的请求和/或响应在提议被放弃很久后才到达目的地,正确性仍然得以保持。)如果某个 proposer 开始尝试发出一个更高编号的提议,通常最好放弃当前的提议。因此,如果某个接受者因为已经接收到一个编号更高的准备请求而忽略了准备请求或接受请求,它应该通知 proposer,提议者应该放弃该提议。这是一个性能优化,不会影响正确性。

2.3 学习已选择的值

为了得知某个值已经被选中,learner 必须确认某个提议已经被大多数接受者接受。一个显而易见的算法是,每当接受者接受一个提议时,它就回应所有 learner,向它们发送该提议。这可以让 learner 尽早得知已选择的值,但它要求每个接受者都需要回应每个 learner——回应的数量是接受者数量与 learner 数量的乘积。

假设没有拜占庭故障,可以让一个 learner 从另一个 learner 那里得知某个值是否已经被选中。我们可以让接受者将它们的接受情况反馈给一个特定的 learner,这个 learner 再将结果通知其他 learner。该方法需要额外的轮次来让所有 learner 发现已选择的值,且其可靠性较低,因为特定的 learner 可能会失败。然而,它只需要的回应数量是接受者数量与 learner 数量的和。

更一般地,接受者可以将其接受的信息反馈给一组特定的 learner,每个 learner 可以将已选中的值通知所有其他 learner。使用更多的 learner 集可以提高可靠性,但会增加通信复杂性。

由于消息丢失的存在,可能会出现值已经被选中,但没有任何 learner 得知的情况。learner 可以向接受者询问它们已经接受的提议,但若某个接受者失败,可能就无法确认是否有多数接受者接受了某个特定的提议。在这种情况下,learner 只能在新的提议被选中时得知已选值。如果 learner 需要知道某个值是否已经被选中,可以让提议者发起一个提议,使用上述算法来完成。

2.4 进度

我们可以构造一个场景,在这个场景中,两个 proposer 每个都不断发出一个递增编号的提议,但没有任何提议被选中。proposer p 完成了编号为 n1n_1 的提议的阶段 1。然后,另一个 proposer q 完成了编号为 n2n_2 > n1n_1 的提议的阶段 1。由于接受者已经承诺不接受任何编号小于 n2n_2 的提议,proposer p 的阶段 2 接受请求会被忽略。于是 proposer p 开始并完成编号为 n3 > n2n_2 的新提议的阶段 1,这使得 proposer q 的阶段 2 接受请求也被忽略。如此继续下去。

为了保证进度,必须选定一个特定的 proposer,作为唯一的 proposer 进行提议。如果该 proposer 能够与大多数接受者成功通信,并且它使用的提议编号大于之前所有使用的编号,那么它就能成功发出一个被接受的提议。通过放弃一个提议并在得知更高编号的请求后重新尝试,最终该 proposer 会选择一个足够大的提议编号。

只要系统中的足够部分(如 proposer、接受者和通信网络)工作正常,就可以通过选举一个特定的 proposer 来实现活跃性。Fischer、Lynch 和 Patterson 的著名结果 [1] 表明,可靠的 proposer 选举算法必须使用随机性或真实时间——例如,使用超时。然而,无论选举的成功与否,安全性都能够得到保证。

2.5 实现

Paxos 算法 [5] 假设一个由进程组成的网络。在它的共识算法中,每个进程都扮演 proposer、acceptor 和 learner 的角色。该算法选择一个 Leader,Leader 扮演着特定 proposer 和特定 learner 的角色。Paxos 共识算法正是上述描述的算法,其中请求和响应作为普通消息发送。(响应消息被标记上相应的提议编号,以防止混淆。)在故障发生时,稳定存储用于维护 acceptor 必须记住的信息。acceptor 在实际发送响应之前,会将它的预期响应记录到稳定存储中。

剩下的就是描述如何保证不会有两个 proposer 发出相同编号的提议。不同的 proposer 从不重叠的编号集选择它们的编号,因此两个不同的 proposer 永远不会发出相同编号的提议。每个 proposer 会在稳定存储中记住它已经尝试过的最大提议编号,并在阶段 1 中开始一个比它已使用过的编号更大的提议编号。

3. 简单的分布式系统实现方式

一种实现分布式系统的简单方法是将其设计为一组客户端,客户端向中央服务器发出命令。服务器可以被描述为一个确定性状态机,它按照某种顺序执行客户端命令。状态机有一个当前状态,它通过接受一个命令作为输入,产生一个输出并生成新的状态。

如果实现只使用单一中心服务器,那么服务器失败时系统也会失败。因此,我们改为使用一组服务器,每个服务器独立地实现状态机。因为状态机是确定性的,如果它们执行相同的命令序列,所有服务器会产生相同的状态和输出。客户端可以向任何服务器发出命令,并使用服务器生成的输出。

为了保证所有服务器执行相同的状态机命令序列,我们实现了多个 Paxos 共识算法实例,每个实例选择的值就是该状态机命令序列中的第 i 个命令。每个服务器在每个算法实例中扮演所有角色(proposer、acceptor 和 learner)。此时假设服务器集是固定的,因此所有实例的共识算法使用相同的代理集。

在正常操作中,选举一个服务器作为 Leader,Leader 作为特定 proposer(唯一发出提议的服务器)执行所有 Paxos 共识算法实例。客户端将命令发送给 Leader,Leader 决定每个命令应该在序列中的哪个位置。如果 Leader 决定某个客户端命令应该是第 135 个命令,它会尝试将该命令选为第 135 个实例的值。通常它会成功,但有时也可能失败,原因可能是因为故障,或者是另一个服务器也认为自己是 Leader 并且对第 135 个命令有不同的看法。但共识算法保证,最多只能选择一个命令作为第 135 个命令。

这种方法高效的关键在于,Paxos 共识算法中的提议值直到第二阶段才被选定。回想一下,在完成 proposer 算法的第一阶段之后,提议值要么已经确定,要么 proposer 可以自由选择任何值。

我将现在描述 Paxos 状态机在正常操作中的实现方式。稍后我将讨论可能出现的问题。我考虑的是在前一个 Leader 刚刚失败并选举出新 Leader 时会发生什么情况。(系统启动是一个特殊情况,在此期间尚未提出任何命令。)

新 Leader 作为所有共识算法实例中的 learner,应该知道大部分已经被选定的命令。假设它知道命令 1 ~ 134、138 ~ 139——即共识算法中实例 1134、138 和 139 中选定的值。(稍后我们会看到命令序列中如何产生这种间隙。)然后,它执行实例 135137 以及所有大于 139 的实例的第 1 阶段。(我将在下面描述如何完成此操作。)假设这些执行的结果决定了实例 135 和 140 中提议的值,但在所有其他实例中,提议的值没有约束。Leader 随后执行实例 135 和 140 的第 2 阶段,从而选定命令 135 和 140。

Leader 以及任何其他学习到 Leader 所知道的所有命令的服务器,现在可以执行命令 1135。然而,它不能执行命令 138 ~ 140,因为命令 136 和 137 尚未被选定。Leader 可以将客户端请求的下两个命令作为命令 136 和 137 提出。相反,我们让它立即填补这个间隙,通过提议命令 136 和 137 为特殊的“无操作(no-op)”命令(即不会改变状态的命令)。它通过执行实例 136 和 137 的第 2 阶段来实现这一点。一旦这些无操作命令被选定,命令 138140 就可以被执行。

现在,命令 1 ~ 140 已经被选定。Leader 也已经完成了共识算法中所有大于 140 的实例的第 1 阶段,并且它可以在这些实例的第 2 阶段提出任何值。它将命令 141 分配给客户请求的下一个命令,并将其作为第 141 实例中的值在第 2 阶段提出。它将接收到的下一个客户命令作为命令 142 提出,以此类推。

Leader 可以在没有得知命令 141 是否已被选定之前就提出命令 142。可能会出现所有提议命令 141 的消息都丢失的情况,而命令 142 被选定,在任何其他服务器了解 Leader 提议的命令 141 之前。在 Leader 未收到预期的第 2 阶段消息响应时,Leader 将重新发送这些消息。如果一切顺利,提议的命令将被选定。然而,它也可能在此之前 crash,导致命令序列中出现间隙。一般来说,假设 Leader 能够提前 α 个命令——即它可以在命令 1i 已选定后,提出命令 i + 1i + α。这时,可能会产生多达 α - 1 个命令的间隙。

一个新选出的 Leader 执行共识算法中的无限多个实例的第 1 阶段——在上述场景中,是实例 135~137 以及所有大于 139 的实例。通过对所有实例使用相同的提议号,它可以通过向其他服务器发送一条简短的消息来完成这一操作。在第 1 阶段中,只有在 acceptor 已经收到某个 proposer 的第 2 阶段消息时,才会回应一个比简单“OK”更长的消息(在这个场景中,仅有实例 135 和 140 是这种情况)。因此,作为 acceptor 的服务器可以通过一条简短的消息对所有实例作出回应。因此,执行这些无限多个实例的第 1 阶段并不会造成问题。

由于 Leader 失败并选举出新 Leader 的事件应该是少数事件,因此执行状态机命令的有效成本——即就命令/值达成共识的成本——仅仅是执行共识算法第 2 阶段的成本。可以证明,共识算法的第 2 阶段是任何在存在故障的情况下达成协议的算法中,代价最低的阶段[2]。因此,Paxos 算法基本上是最优的。

上述关于系统正常操作的讨论假设总是只有一个 Leader,除了当前 Leader 失败与新 Leader 选举之间的短暂期间。在不正常情况下,Leader 选举可能会失败。如果没有服务器充当 Leader,则不会提出新的命令。如果多个服务器认为自己是 Leader,那么它们都可能在同一实例中提出不同的值,这可能导致没有值被选定。然而,安全性仍然得到保证——不同的服务器永远不会对作为第 i 个状态机命令所选定的值产生分歧。选举一个单一的 Leader 仅仅是为了确保进展。

如果服务器集可以改变,则必须有某种方法来确定哪些服务器执行哪些实例的共识算法。最简单的做法是通过状态机本身来完成。当前的服务器集可以作为状态的一部分,并通过普通的状态机命令进行更改。我们可以允许 Leader 提前 α 个命令,通过让执行第 i + α 个共识算法实例的服务器集由执行第 i 个状态机命令后的状态来指定。这样可以实现一个简单的、任意复杂度的重新配置算法的实现。

论文阅读:Paxos make live

Google 用来工程化的 Paxos 的例子

参考资料