Paxos 是分布式共识的开山之作,但它难懂到作者自己都承认。Raft 的目标很简单:让工程师能看懂。它成功了。
前情回顾
在前两篇中,我们看到:
- 单机到分布式:网络不可靠、时钟不一致、节点会挂
- 2PC 的问题:协调者单点故障,一挂全挂
- CAP 定理:网络分区时,一致性和可用性只能二选一
2PC 的核心问题是:依赖单个协调者。如果协调者挂了,没人知道该提交还是回滚。
有没有办法让一群节点自己达成共识?即使部分节点挂了,剩下的节点也能继续工作?
共识问题:投票选餐厅
假设你们组 5 个人要选中午吃什么:
投票情况:
小明:川菜
小红:川菜
小李:粤菜
小张:(没来)
小王:(没来)
已投票 3 人,川菜 2 票,粤菜 1 票
问:现在能决定吃川菜吗?
能。 因为即使小张和小王都投粤菜,川菜也是 2:2,不会输。
核心思想:只要超过半数同意,结果就确定了。剩下的人无论怎么投,都改变不了结果。
这就是分布式共识的核心:多数派决定。
共识算法要解决什么?
让一群节点对某个值达成一致意见,满足:
| 要求 | 含义 |
|---|---|
| 一致性 | 所有节点最终同意同一个值 |
| 有效性 | 被同意的值必须是某个节点提出的(不能凭空出现) |
| 终止性 | 只要多数节点存活,最终能达成共识 |
注意:允许部分节点挂掉,只要多数节点存活就行。
Paxos:天书级别的开山之作
1989 年,Leslie Lamport 写出了 Paxos 算法。这是第一个被证明正确的分布式共识算法。
Paxos 的故事
Lamport 用一个虚构的希腊岛屿「Paxos」来讲这个算法,把节点比作岛上的议员。
论文在 1990 年提交后被拒了,因为审稿人觉得「故事太多,不像正经论文」。
直到 1998 年,这篇论文才正式发表。后来 Lamport 又写了一个「正经版本」(Paxos Made Simple, 2001),结果大家还是看不懂。
Lamport 自己说:
「Paxos 算法很简单,但大家就是觉得难。」
事实是:Paxos 真的很难懂。
Paxos 的核心思想(简化版)
Paxos 有三种角色:
| 角色 | 职责 | 类比 |
|---|---|---|
| Proposer | 提出提案 | 提议吃什么的人 |
| Acceptor | 接受或拒绝提案 | 投票的人 |
| Learner | 学习最终结果 | 被通知结果的人 |
两阶段流程:
阶段 1:Prepare(准备)
Proposer Acceptor 1,2,3
│ │
│── Prepare(n=1) ──────────►│
│ │
│◄── Promise(n=1, 无旧值) ───│
│ │
│ 收到多数派的 Promise
│ 进入阶段 2
阶段 2:Accept(接受)
Proposer Acceptor 1,2,3
│ │
│── Accept(n=1, v="川菜") ──►│
│ │
│◄── Accepted ──────────────│
│ │
│ 收到多数派的 Accepted
│ 共识达成!值 = "川菜"
为什么 Paxos 这么难?
- 论文写法:隐喻太多,正文太抽象
- 边界情况复杂:多个 Proposer 并发时的处理
- 工程实现难:从算法到代码,有巨大鸿沟
- Multi-Paxos:实际使用需要扩展,但 Lamport 没详细说
结果:每个实现 Paxos 的团队,都觉得自己在重新发明轮子。
Raft:为可理解性而生
2014 年,斯坦福的 Diego Ongaro 和 John Ousterhout 发表了 Raft 算法。
论文标题:《In Search of an Understandable Consensus Algorithm》
目标很明确:让工程师能看懂。
Raft 的核心设计
Raft 把共识问题拆成三个独立的子问题:
| 子问题 | 描述 | Paxos 对比 |
|---|---|---|
| Leader 选举 | 选出一个领导者 | Paxos 没有明确领导者 |
| 日志复制 | Leader 把操作同步给 Follower | 类似 Multi-Paxos |
| 安全性 | 保证已提交的日志不会丢失 | Paxos 的核心,但更易理解 |
核心简化:在任意时刻,只有一个 Leader。所有写操作都通过 Leader。
Raft 的三种角色
┌─────────────┐
│ Leader │ 只有一个,负责处理所有写请求
└─────────────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Follower │ │ Follower │ │ Follower │ 跟随 Leader,复制日志
└──────────┘ └──────────┘ └──────────┘
(还有 Candidate 角色,选举时出现)
子问题一:Leader 选举
正常情况:
Leader Follower A,B,C
│ │
│── 心跳 ─────────────────►│
│ │
│ (每隔 100ms 发一次) │
│ │
│── 心跳 ─────────────────►│
│ │
Leader 挂了:
Leader (挂了) Follower A,B,C
X │
│ Follower A: 好久没收到心跳了
│ (超时时间 150-300ms 随机)
│
│ Follower A: 我来竞选 Leader!
│
▼
Candidate A Follower B,C
│ │
│ 我现在是任期 2 │
│ 请投票给我 │
│ │
│── 请求投票 ─────────────►│
│ │
│◄── 投给你了 ──────────────│
│ │
│ 收到多数票 │
│ (自己1票 + B和C的票) │
│ 我是新 Leader 了! │
关键点:
- 超时时间随机:避免多个节点同时发起选举
- 任期递增:每次选举任期 +1,用于判断新旧
- 一个任期只投一票:防止一个任期选出多个 Leader
子问题二:日志复制
选出 Leader 后,客户端的写请求这样处理:
客户端 Leader Follower A,B
│ │ │
│── 写入 x=1 ──►│ │
│ │ │
│ │── 日志: x=1 ───►│
│ │ │
│ │◄── 收到 ────────│ (多数派确认)
│ │ │
│ │ 写入本地 │
│ │ 标记为已提交 │
│ │ │
│◄── 成功 ──────│ │
│ │ │
│ │── 提交通知 ────►│ (告诉 Follower 可以提交了)
关键点:
- 先复制后提交:日志先发给 Follower,多数派确认后才提交
- 顺序保证:日志按索引顺序复制,不会跳跃
- Leader 绝对权威:Follower 的日志与 Leader 冲突时,以 Leader 为准
子问题三:安全性
问题:如果旧 Leader 挂了,新 Leader 会不会丢失已提交的日志?
Raft 的保证:只有包含所有已提交日志的节点,才能成为新 Leader。
场景:Leader 提交了日志 [1,2,3],然后挂了
节点 A: [1,2,3] ← 日志完整
节点 B: [1,2] ← 少了日志 3
节点 C: [1,2,3] ← 日志完整
选举时:
- B 发起选举,A 和 C 不会投给 B(B 的日志太旧)
- A 或 C 发起选举,能获得多数票成为 Leader
- 日志 3 不会丢失
投票规则:只投给日志「至少和我一样新」的候选人。
Raft vs Paxos
| 维度 | Paxos | Raft |
|---|---|---|
| 可理解性 | 难 | 易 |
| 领导者 | 隐式,可多个 | 显式,只有一个 |
| 日志顺序 | 不保证 | 严格顺序 |
| 实现难度 | 高 | 低 |
| 正确性 | 已证明 | 已证明 |
| 性能 | 理论上可更高 | 足够好 |
实际选择:大多数新系统选 Raft。
谁在用 Raft?
| 系统 | 用途 | 说明 |
|---|---|---|
| etcd | Kubernetes 配置存储 | Raft 的标杆实现 |
| Consul | 服务发现 | HashiCorp 出品 |
| TiKV | 分布式 KV 存储 | TiDB 的存储层 |
| CockroachDB | 分布式数据库 | 每个 Range 一个 Raft 组 |
一个完整的例子
5 节点集群,处理一次写请求:
初始状态:
节点 1 (Leader, 任期 5)
节点 2,3,4,5 (Follower)
客户端请求:SET x = 100
步骤 1:客户端发送请求到 Leader
客户端 ──► 节点 1: SET x = 100
步骤 2:Leader 追加日志,发送给 Follower
节点 1 ──► 节点 2,3,4,5: AppendEntries(日志: SET x=100)
步骤 3:Follower 追加日志,响应 Leader
节点 2,3 ──► 节点 1: 成功
节点 4,5: (网络慢,还没响应)
步骤 4:Leader 收到多数派确认 (3/5),提交日志
节点 1: 标记日志为已提交,应用到状态机
步骤 5:响应客户端
节点 1 ──► 客户端: 成功
步骤 6:后续心跳通知 Follower 提交
节点 1 ──► 节点 2,3,4,5: 心跳(commitIndex=新值)
Follower 收到后提交日志
关键:只要 3/5 节点确认,就能提交。节点 4,5 慢一点没关系,最终会追上。
脑裂问题:Raft 怎么解决?
回忆上一篇的脑裂场景:网络分区导致出现两个 Leader。
Raft 的解决方案:
网络分区前:
节点 1 (Leader, 任期 5)
节点 2,3,4,5 (Follower)
网络分区后:
[分区 A] [分区 B]
节点 1 (旧 Leader) 节点 3,4,5
节点 2 │
│ 超时,发起选举
│ 选出新 Leader (任期 6)
▼
节点 3 (新 Leader, 任期 6)
节点 4,5 (Follower)
此时:
节点 1 以为自己还是 Leader (任期 5)
节点 3 是新 Leader (任期 6)
但节点 1 无法提交任何日志:
节点 1 (旧 Leader) 节点 2
│ │
│── 写 x=1 ────────────►│
│ │
│ 只有 2/5 节点 │
│ 不够多数派 │
│ 无法提交! │
网络恢复后:
节点 1 收到任期 6 的消息
│
│ 任期 6 > 我的任期 5
│ 我不再是 Leader 了
│ 转为 Follower
│ 回滚未提交的日志
│
▼
只剩一个 Leader (节点 3)
核心机制:
- 多数派:少数派分区无法提交
- 任期:新任期的 Leader 会「废黜」旧 Leader
- 日志一致性:以新 Leader 的日志为准
常见问题
Q:Raft 能保证多少节点挂了还能工作?
A:少于一半。
| 集群大小 | 可容忍故障数 | 说明 |
|---|---|---|
| 3 节点 | 1 | 最小生产配置 |
| 5 节点 | 2 | 常见配置 |
| 7 节点 | 3 | 大规模场景 |
公式:N 个节点可容忍 (N-1)/2 个故障。
Q:为什么通常用奇数个节点?
A:偶数没有额外收益。
- 4 节点和 3 节点都只能容忍 1 个故障
- 6 节点和 5 节点都只能容忍 2 个故障
- 多一个节点只增加成本,不增加容错
Q:Raft 的性能瓶颈在哪?
A:Leader 是瓶颈。
所有写请求都经过 Leader,Leader 的网络带宽和 CPU 决定了上限。
解决方案:
- Multi-Raft:数据分片,每个分片一个 Raft 组,不同组有不同 Leader
- Follower Read:读请求可以走 Follower(牺牲一点一致性)
总结
Paxos 的贡献:
- 第一个被证明正确的分布式共识算法
- 奠定了「多数派」思想的理论基础
- 影响了所有后来的共识算法
Raft 的贡献:
- 把共识问题拆解成可理解的子问题
- 让工程师能正确实现共识算法
- 成为事实上的工业标准
核心思想:
只要多数派同意,决定就不会被推翻。
少数派可能落后,但最终会追上。
这是强一致性的基础。
但是,强一致性是有代价的:
- 每次写入都要等多数派确认
- 网络分区时,少数派无法服务
- 跨地域部署时,延迟很高
有些场景下,我们不需要这么强的一致性。「最终能一致」就够了。
下一篇,我们来看 最终一致性——一种更「宽容」的一致性模型。
上一篇:2PC 与 CAP:理想的破灭
下一篇:最终一致性:不强求,但终会一致
本系列:
- 单机到分布式:一致性为何变难
- 2PC 与 CAP:理想的破灭
- Paxos 与 Raft:让多数人达成共识(本篇)
- 最终一致性:不强求,但终会一致
- CRDT:无需协调的合并魔法
- 现代方案:从 Spanner 到 TiDB
- 实战篇:方案选型与落地