基本介绍
Paxos 算法是由计算机科学家 Leslie Lamport 提出的一种基于消息传递的分布式一致性算法,这项开创性工作使他获得了2013年图灵奖。该算法解决了分布式系统中如何就某个值(决议)达成一致的问题,即使在部分节点失效或网络不稳定的情况下也能保证系统一致性。
发展历史
Paxos 算法最早由 Lamport 于 1998 年在《The Part-Time Parliament》论文中首次公开。论文采用了一种独特的叙述方式,使用希腊的一个名为 Paxos 的小岛作为比喻,详细描述了 Paxos 小岛中通过议会决议的流程,并以此命名这个算法。
在2001年,Lamport 意识到学术同行们难以理解他这种幽默的表达方式,于是重新发表了更加简洁直接的算法描述版本《Paxos Made Simple》。
行业影响与应用
自 Paxos 问世以来,它就在分布式一致性算法领域占据主导地位。该算法在工业界得到了广泛应用:
- Google 系统应用:Chubby(分布式锁服务)、Megastore(高可用性存储系统)、Spanner(全球分布式数据库)
- 开源系统应用:ZooKeeper(分布式协调服务)、MySQL 5.7 的 Group Replication
算法特点
Paxos 算法虽然强大,但以难以理解和实现著称。其难点主要体现在:
- 算法描述抽象,需要深入理解分布式系统原理
- 实现时需要处理各种边界条件和异常情况
- 性能优化需要考虑实际网络环境
解决了什么问题
问题背景
在现代分布式系统中,数据通常采用多副本机制存储以提高可用性和容错性。典型场景包括:分布式数据库(如 Google Spanner)、分布式存储系统(如 HDFS)、分布式计算框架等。
具体挑战
- 网络分区:节点间通信可能中断
- 消息延迟:操作指令到达不同节点的时间不一致
- 节点故障:部分节点可能暂时或永久失效
- 并发冲突:多个客户端同时发起修改请求
解决方案细节
Paxos 通过以下机制保证一致性:
- 提案阶段(Prepare):节点提议值并收集多数派响应
- 接受阶段(Accept):确定最终值并获得多数派确认
- 学习阶段(Learn):将确定的值传播给所有副本
Paxos 能在半数以上节点正常工作的情况下,保证系统对外展现一致的状态,即使面对网络问题和节点故障。
相关概念
提案(Proposal)机制
提案是 Paxos 算法的核心概念,它是一个包含两个关键组成部分的信息包:
- 提案编码(Proposal ID):一个全局唯一的标识符,通常由提案编号(通常是递增的数字)和提案者的唯一标识(如服务器 ID)组成。例如:<5, ServerA> 表示由 ServerA 发起的第 5 号提案。
- 提议的值(Value):这是希望分布式系统达成一致的最终数据值。
Paxos 算法中的角色分工
Paxos 算法定义了四个关键角色,各司其职:
1. Client(客户端)
- 是系统外部请求的发起方
- 向分布式系统发出操作请求(如读写请求)
- 等待系统响应和确认
2. Proposer(提案发起者)
- 核心职责:接收客户端请求并转化为提案,推动提案的接受过程,处理提案冲突时的协调工作
- 工作流程:
- 准备阶段:向 Acceptors 发送准备请求
- 提议阶段:收到多数 Acceptors 响应后发送正式提案
3. Acceptor(决策者)
- 关键功能:接收并处理 Proposer 的请求,决定是否接受某个提案,保证安全性约束
- 决策规则:只接受编号高于已承诺编号的提案,必须记住已接受的最高编号提案
- 重要特性:通常需要多数派(quorum)Acceptors 接受才能选定提案
4. Learner(学习者)
- 主要任务:观察并学习被选定的 Value,将最终决议传播到整个系统
- 实现方式:可以监听 Acceptors 的决定,或者由 Proposer 通知选定结果
这些角色在实际系统中通常由服务器节点兼任,例如一个节点可能同时扮演 Proposer 和 Acceptor 的角色。
推导过程
最简单的方案:只有一个 Acceptor
在 Paxos 算法的实现中,最简单的方案是只使用一个 Acceptor(可以有多个 Proposer)。在这种配置下,系统的工作流程如下:
- 任何 Proposer 都可以向唯一的 Acceptor 提交提案
- Acceptor 遵循”先到先接受”的原则,即接受它收到的第一个提案
- 一旦 Acceptor 接受某个提案,该提案就被选定
- 该提案中包含的 Value 就成为整个系统最终选定的值
这种方案的优势在于实现简单,可以确保系统最终会选定一个值。但是,这个方案存在严重的问题:
- 单点故障风险:如果这个唯一的 Acceptor 宕机,整个系统将完全停止工作
- 性能瓶颈:所有请求都必须经过单个 Acceptor 处理
- 缺乏容错能力:无法应对网络分区等异常情况
因此,在实际生产环境中,必须使用多个 Acceptor 来构建可靠的分布式系统。
多个 Acceptor
如何保证多个 Proposer 和多个 Acceptor 的情况下选定一个 Value 呢?
P1:一个 Acceptor 必须接受它收到的第一个提案。
但是,如果每个 Proposer 分别提出不同的 Value,发送给不同的 Acceptor。根据刚才的约束,Acceptor 分别接受自己收到的第一个提案,就导致不同的 Value 被选定,出现了不一致。
因此,我们需要加一个规定:一个提案被选定需要被半数以上的 Acceptor 接受。
根据上面的内容,我们现在虽然允许多个提案被选定,但必须保证所有被选定的提案都具有相同 Value 值,否则又会出现不一致。
P2:如果某个 Value 为 v 的提案被选定了,那么每个编号更高的被选定提案的 Value 也必须是 v。
一个提案只有被 Acceptor 接受才可能被选定,因此我们可以把 P2 约束改写成 Acceptor 接受的提案的约束 P2a。
P2a:如果某个 Value 为 v 的提案被选定了,那么每个编号更高的被 Acceptor 接受的提案 Value 必须也是 v。
只要满足了 P2a,就能满足 P2。
但是,考虑如下的情况:假设总的有 5 个 Acceptor,Proposer 提出【M1,V1】的提案,Acceptor25(半数以上)接受了提案,于是对于 Acceptor25 和 Proposer2 来讲,它们都认为 V1 被选定。Acceptor1 刚从宕机状态恢复过来(之前 Acceptor1 没有收到任何提案),此时 Proposer1 向 Acceptor1 发送了【M2, V2】的提案(V2 不等 V1 且 M2 大于 M1),对于 Acceptor1 来讲,这是它收到的第一个提案。根据 P1(一个 Acceptor 必须接受它收到的第一个提案),Acceptor1 必须接受该提案,同时 Acceptor1 认为 V2 被选定。
这就出现了如下的两个问题:
- Acceptor1 认为 V2 被选定,Acceptor2~5 和 Proposer2 认为 V1 被选定,出现了不一致
- V1 被选定了,但是编号更高的被 Acceptor1 接受的提案【M2,V2】的 value 为 V2,且 V2 != V1。这就跟 P2a 矛盾了
所以我们需要对 P2a 约束进行强化!
P2a 是对 Acceptor 接受的提案约束,但其实提案是 Proposer 提出来的,所以我们可以对 Proposer 提出的提案进行约束,得到 P2b:
P2b:如果某个 Value 为 v 的提案被选定了,那么之后任何 Proposer 提出的编号更高的提案 value 也必须是 v。
有 P2b 可以推出 P2a 进而推出 P2。那么,如何确保在某个 Value 为 v 的提案之后,Proposer 提出的编号更高的提案的 Value 都是 v 呢?
只要满足 P2c 即可:
P2c:对于任意的 Mn 和 Vn,如果提案 [Mn,Vn] 提出,那么肯定存在一个由半数以上的 Acceptor 组成的集合 S,满足以下两个条件中的任意一个:
- 要么 S 中的每个 Acceptor 都没有接受过编号小于 Mn 的提案
- 要么 S 中所有 Acceptor 批准的所有编号小于 Mn 的提案中,编号最大的那个提案的 Value 值为 Vn
从上面的内容,可以看出,从 P1 到 P2c 的过程其实是对一系列条件的逐步加强,如果需要证明这些条件可以保证一致性,那么就可以进行反向推导:P2c => P2b => P2a => P2,然后通过 P2 和 P1 来保证一致性。