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 这么难?

  1. 论文写法:隐喻太多,正文太抽象
  2. 边界情况复杂:多个 Proposer 并发时的处理
  3. 工程实现难:从算法到代码,有巨大鸿沟
  4. 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

维度PaxosRaft
可理解性
领导者隐式,可多个显式,只有一个
日志顺序不保证严格顺序
实现难度
正确性已证明已证明
性能理论上可更高足够好

实际选择:大多数新系统选 Raft。

谁在用 Raft?

系统用途说明
etcdKubernetes 配置存储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:理想的破灭

下一篇:最终一致性:不强求,但终会一致

本系列:

  1. 单机到分布式:一致性为何变难
  2. 2PC 与 CAP:理想的破灭
  3. Paxos 与 Raft:让多数人达成共识(本篇)
  4. 最终一致性:不强求,但终会一致
  5. CRDT:无需协调的合并魔法
  6. 现代方案:从 Spanner 到 TiDB
  7. 实战篇:方案选型与落地