Skip to content

Raft 协议与 Paxos 核心区别:为什么 Raft 更易实现?Leader 选举过程详解

问题

Raft 协议的核心机制是什么?和 Paxos 相比,它为什么被认为是"更易实现的一致性算法"?

从 Paxos 到 Raft:共识算法的工程化革命

Paxos 在理论上完美,但工程上出了名的难实现。Lamport 的原始论文抽象到大部分人读三遍都写不出正确代码。Google 的 Chubby 实现 Paxos 时,工程师花了大量时间在活锁、Multi-Paxos 的日志顺序、Leader 选主等细节上,最终做出来的东西本质上更像 Raft 了。

Raft 的出现不是为了发明新的共识模型,而是把 Paxos 的"数学证明"翻译成"工程蓝图"。Diego Ongaro 的博士论文动机很单纯:设计一个能让本科生上完课就能写出来的共识算法。

Raft 的核心思路是分而治之:把共识问题分解为三个彼此独立、可单独理解和实现的子问题:

  1. Leader 选举(选主)
  2. 日志复制(主从同步)
  3. 安全性(保证一致性)

这种分解本身就是 Paxos 做不到的——Paxos 把这三个问题揉在一起,Proposer/Acceptor/Learner 三个角色可以任意组合,灵活性高但实现复杂度爆炸。实际生产里没人用 Classic Paxos,大家都在用变种:Multi-Paxos、Fast Paxos、Cheap Paxos,每个变种实现细节都不相同,社区共识成本极高。

面试重点:Paxos 和 Raft 的关系

面试官问"Paxos 和 Raft 的区别",真正想听的是:你理解共识算法本质上是"让一群不可靠的节点对一个值达成一致",而 Raft 通过 Strong Leader 模型把问题简化了,不是创新了共识理论,是创新了工程实现方式。

Leader 选举:Raft 最核心的机制

三种角色状态

Raft 的每个节点在任意时刻处于三种状态之一:

           +-----------+
           | Follower  |<----- 默认状态,被动接收 Leader 的 AppendEntries
           +-----------+
                |
         选举超时,转为 Candidate
                |
                v
           +-----------+
     +---->| Candidate |<---- 发起选举,请求投票
     |     +-----------+
     |           |
     |     获得多数派票数
     |           |
     |           v
     |     +-----------+
     |     |  Leader   |-----> 发送心跳(AppendEntries RPC)维持权威
     +-----+-----------+
       发现更高 Term 的 Leader,退回 Follower

选举流程

  1. Follower 启动选举超时计时器(Election Timeout,随机 150-300ms)。为什么是 150-300ms?这个范围不是随便选的。心跳间隔通常是 50-100ms(etcd 默认 50ms),如果心跳间隔 50ms,选举超时 150ms 保证了节点至少错过 1-3 个心跳包才会触发选举,避免网络抖动导致频繁选主。
  2. 超时未收到 Leader 的心跳,Follower 转为 Candidate
  3. Candidate 自增当前 Term(任期号),给自己投票,然后向所有其他节点发送 RequestVote RPC
  4. 每个节点在同一个 Term 内只能投一票,先到先得
  5. 获得多数派(> N/2) 票的节点成为 Leader。3 节点集群需要 2 票,5 节点需要 3 票
  6. 如果投票被瓜分(Split Vote),没有节点获得多数票,则本轮选举失败,各节点随机等待后重试

关键设计点:随机超时时间 是 Raft 避免选举冲突的关键。如果所有节点的超时时间一样,每次选举都会出现 Split Vote,永远选不出 Leader。随机化保证了绝大多数情况下,至少有一个节点先超时并发起选举,在其他人反应过来之前拿到多数票。

选举的日志安全性

Raft 要求只有拥有最新日志的节点才能成为 Leader。在 RequestVote 请求中,Candidate 需要带上自己最后一条日志的 (term, index)。接收节点比较:

  • 如果 Candidate 的 lastTerm 比自己的小,拒绝投票
  • 如果 lastTerm 相等但 lastIndex 比自己的小,拒绝投票

这个机制保证了新 Leader 必然拥有所有已提交的日志,不需要像 Paxos 那样在选举后做日志补救。这也是 Raft 安全性证明的最关键一环——Leader Completeness 属性。

面试追问:为什么 Raft 必须是 Strong Leader?

面试官可能问:"为什么 Raft 是 Strong Leader 模型,而 Paxos 允许多个 Proposer?"

回答要点:

  • Paxos 允许任意节点发起提案,好处是灵活,坏处是活锁 —— 多个 Proposer 互相抢占,导致迟迟无法达成一致
  • Raft 的 Strong Leader 消除了活锁,代价是写请求必须经过 Leader,多了一个网络跳转
  • 这个代价在工程上完全可接受,因为共识算法本身就不是为了低延迟设计的,一致性才是核心目标
  • 读请求可以通过 ReadIndex 或 Lease Read 在 Follower 上处理,不一定经过 Leader

日志复制:Raft 的写路径

Leader 选出后,所有写请求走 Leader。下面是完整的时序过程:

Client     Leader          Follower1       Follower2
  |          |               |               |
  | 写入请求  |               |               |
  |--------->|               |               |
  |          | 追加到本地日志 |               |
  |          | AppendEntries |               |
  |          |-------------->|               |
  |          | AppendEntries |               |
  |          |---------------------------->| |
  |          |               |               |
  |          |   多数派回复   |               |
  |          |<--------------|               |
  |          |<-----------------------------| |
  |          | Commit 日志   |               |
  |          | 应用到状态机  |               |
  |          | 写入回复      |               |
  |<---------|               |               |
  |          | 下次心跳通知   |               |
  |          | 日志已提交     |               |
  |          |-------------->|               |
  |          |---------------------------->| |

流程细节

  1. Leader 接受客户端请求,追加日志条目到本地
  2. Leader 并行发送 AppendEntries RPC 给所有 Follower
  3. Follower 收到后检查日志一致性(prevLogIndex/prevLogTerm 匹配),追加到本地日志,回复成功
  4. Leader 收到多数派回复后,将日志标记为 Committed,应用到状态机
  5. Leader 在后续的 AppendEntries(心跳)中通知 Follower 该日志已提交

日志一致性保证

Raft 通过两个不变量保证日志不会乱:

  • Log Matching Property:如果两个节点上某个日志条目的 (term, index) 相同,那么该条目之前的所有日志也相同。这个性质是通过 AppendEntries 的 prevLogIndex/prevLogTerm 检查保证的:如果 Follower 发现 prevLogIndex 处的日志 term 不匹配,就拒绝追加,Leader 回退后重试。
  • Leader 不会覆盖自己已提交的日志:新 Leader 选举出来后,对于自己 Term 的日志,只有等到提交后才算数。

Raft 与 Paxos 的核心区别

维度PaxosRaft
提案顺序无顺序,Multi-Paxos 需要额外机制保证顺序日志天然有序(按日志索引)
提议者任意节点都可提议,导致活锁(Liveness 问题)只有 Leader 提议,无活锁
选举与复制选举和提案解耦,实现时可混合选举和日志复制绑定,逻辑清晰
可理解性抽象,背后是数学证明,实现难度高具体,背后是工程分解,本科生可码
实现复杂度高,Paxos、Multi-Paxos、Fast Paxos 实现差异大低,标准实现参考 etcd/raft,业界通用
角色数量3 个(Proposer、Acceptor、Learner)3 个状态(Follower、Candidate、Leader)
日志管理需要额外组件保证顺序和提交日志索引天然有序,提交点明确
成员变更不讨论,留给实现者处理有标准的 Joint Consensus 方案
读优化无标准方案ReadIndex、Lease Read、Follower Read
生产实现Google Chubby、Polaris(改造成本高)etcd、Consul、TiKV、Kafka(Raft 模式)

为什么 Raft 在实际生产中被广泛采用?

Paxos 在理论上优雅,但 Raft 在工程上胜出,根本原因在于:

  1. 可调试性:Raft 的日志复制机制意味着你可以通过查看 Leader 的日志索引和 Follower 的匹配索引来定位问题,而 Paxos 的日志是分散的,调试困难。
  2. 选主确定性:Raft 的选举规则明确(最新日志优先),Paxos 的选主依赖于 Leader 租约和超时,实现差异大。
  3. 社区共识:etcd 的 Raft 实现(go.etcd.io/raft/v3)是公认的参考实现,有问题可以查标准代码,而 Paxos 的每个实现都是独立的。

安全性:Raft 如何保证不丢数据

Raft 的安全性保证可以归纳为一条核心规则

一个日志条目一旦被提交(Commit),它就不会被任何后续的 Leader 覆盖或删除。

具体机制:

  • Leader 只能追加日志,不能修改或删除已存在的日志
  • 新 Leader 选举时,只有拥有最新日志的节点才能当选
  • Leader 在提交自己 Term 的日志时,会隐式提交之前所有 Term 的日志
  • 如果 Follower 的日志与 Leader 不一致(旧 Leader 未提交的日志),Leader 强制 Follower 覆盖

安全性证明的核心:Commit 不回归

Raft 的安全性证明依赖于一个关键观察:如果一条日志在 term T 被提交,那么所有后续 term 的 Leader 都会拥有这条日志

证明思路:

  1. 日志在 term T 被提交,意味着它被多数派节点复制了
  2. 后续的任何 Leader 选举,Candidate 必须获得多数派支持
  3. 多数派和多数派必然有交集,所以新 Leader 的日志至少和已提交的日志一样新(通过 RequestVote 的日志比较)
  4. 因此,已提交的日志永远不会丢失

这个证明比 Paxos 的证明直观得多,也是 Raft 易理解性的核心原因。

工程实现中的常见坑

脑裂(Split-brain)

网络分区时,旧 Leader 在少数派分区,新 Leader 在多数派分区产生。旧 Leader 的写请求会被拒绝(因为 Term 更小),不会产生脑裂写。但旧 Leader 的读请求会返回过期数据——这就是为什么 Etcd 默认的 Leader Read 模式需要配合 ReadIndex 才能保证线性一致读。

真实案例:某公司 3 节点 etcd 集群,网络交换机故障导致节点 1 和节点 2 之间 300ms 延迟,触发了选举。节点 1 作为旧 Leader 在少数派分区,仍然响应了 2000 个读请求,返回了过期数据,导致下游服务写入了错误的配置。事后排查发现,他们没开 ReadIndex 模式,用的是默认的 Leader Read。

日志不一致

Leader 刚当选时,如果 Follower 的日志和 Leader 不一致(比如旧 Leader 宕机前只将日志复制到部分 Follower),Leader 会强制 Follower 覆盖不一致的日志。Raft 通过逐轮匹配(AppendEntries 的 prevLogIndex/prevLogTerm 检查)找到第一个不一致的位置,然后从那里开始覆盖。

Follower 日志: [1,1] [1,2] [1,3] [2,4] [2,5]   ← 旧 Leader 写的未提交日志
Leader 日志:   [1,1] [1,2] [1,3] [1,4] [1,5]   ← 提交了前 3 条,第 4、5 条是新的

Leader 通过 prevLogIndex/prevLogTerm 逐轮检查:
- prevLogIndex=5, prevLogTerm=? → Follower 回复失败(term 不匹配)
- prevLogIndex=4, prevLogTerm=? → Follower 回复失败
- prevLogIndex=3, prevLogTerm=1 → Follower 接受(匹配)
- Leader 从 index=4 开始覆盖 Follower 的日志

注意:Follower 的 [2,4] [2,5] 是旧 Leader 的未提交日志,覆盖是安全的。这是 Raft 的一个重要设计决策:未提交的日志可以被覆盖,已提交的日志永远不会被覆盖。

集群成员变更

Raft 的联合共识(Joint Consensus)是成员变更的标准方案:

  1. 将集群配置从 C_old 切换到 C_old + C_new(联合共识)
  2. 联合共识期间的决策需要两个多数派都同意
  3. 再切换到 C_new
  4. 如果过程中出现故障,回退到上一个稳定配置

这种两阶段过渡保证了成员变更期间集群不中断服务。

为什么不能一次切换? 如果直接从一个 3 节点集群切换到 5 节点集群,中间状态可能产生两个互不重叠的多数派,导致脑裂。Joint Consensus 通过要求两个配置的多数派都同意,避免了这个问题。

面试高频:Raft 的选举超时参数怎么配?

场景心跳间隔选举超时(随机范围)理由
同机房低延迟50ms300-500ms网络稳定,心跳快,避免无谓选举
跨机房100ms500-1000ms延迟大,需要容忍网络抖动
异地容灾200ms1000-2000ms极端延迟,防止频繁选主

etcd 默认心跳 50ms,选举超时 200ms(实际实现是心跳间隔的 4-10 倍)。如果业务对写入延迟敏感,可以适当降低心跳间隔,但不要低于 10ms,否则 Leader 的心跳包可能会耗尽网络带宽。

总结

  • Raft 把 Paxos 的"任意 Proposer 都可发起提案"简化为"选主后只有 Leader 发起提案",消除了活锁问题
  • Raft 把"无顺序的命名决策"简化为"有序的日志条目",降低了日志管理的复杂度
  • Raft 把"任意 Acceptor 任意接受"简化为"日志复制到多数派",使实现更直观
  • Paxos 是理论模型,Raft 是工程实现——面试时知道 Paxos 原理就行,真正能实现的是 Raft
  • 面试重点:选举流程、日志复制、安全性证明思路、脑裂场景、成员变更的 Joint Consensus

如果要在生产环境选一个共识算法实现,etcd 的 Raft 实现(go.etcd.io/raft/v3)是经过大规模验证的参考实现,代码结构清晰,适合学习和二次开发。TiKV、Consul、Kafka 的 Raft 模式也都是基于类似的实现。

参考

  • Ongaro, D., & Ousterhout, J. (2014). In Search of an Understandable Consensus Algorithm(Raft 论文原文,必读)
  • Etcd 官方文档. etcd/raft 库使用指南
  • Diego Ongaro. Raft 博士论文(对 Raft 安全性证明的完整阐述)
  • Raft 官网. raft.github.io(有交互式可视化演示,适合面试前复习)

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。