Raft 算法详解
Paxos 太难了。Lamport 本人都承认,那篇用寓言体写的论文「很难理解」。2014 年,Diego Ongaro 和 John Ousterhout 发表论文《In Search of an Understandable Consensus Algorithm (Extended Version)》,提出了 Raft——一个刻意设计成更容易理解的共识算法。
论文的标题本身就是宣言:Raft 的首要设计目标,不是性能最优,不是表达能力最强,而是让工程师真正能搞懂它。
这个目标实现了吗?相当程度上是的。Raft 已经成为 etcd、Consul、TikV、CockroachDB 等主流分布式系统的共识引擎。理解 Raft,几乎是现代后端工程师的必修课。
设计哲学:问题分解
Raft 的核心思想是把共识问题拆解为三个相对独立的子问题:
为什么 Raft 要分解问题?
这种分解方式比 Paxos 的「单一协议」更符合人类直觉。Paxos 试图用一套优雅的数学框架解决所有问题,Raft 则选择「分而治之」——用三个更具体的子问题,换取实现的可理解性。
- Leader 选举:如何在集群中选出一个 Leader?
- 日志复制:Leader 如何把客户端请求复制到所有 Follower?
- 安全性:如何保证 Raft 不会丢失数据或产生不一致?
这种分解方式比 Paxos 的「单一协议」更符合人类直觉。Paxos 试图用一套优雅的数学框架解决所有问题,Raft 则选择「分而治之」——用三个更具体的子问题,换取实现的可理解性。
Leader 选举
Raft 把时间划分为任期(Term),每个任期以一次选举开始。
关键机制
选举超时
Follower 在心跳超时后转为 Candidate。Raft 的关键设计:选举超时是随机化的(通常是 150~300ms 之间的随机值)。
为什么随机化很重要?
如果所有 Follower 的超时时间相同,选举时会产生平票僵局。随机化确保大多数情况下只有一个 Candidate 会在其他节点超时前发起选举并获胜,快速收敛。
Leader 选举机制总结
Leader 选举的核心要点:
- 任期(Term):全局递增的时间单位,每次选举开始时 +1
- 随机超时:150~300ms 之间的随机值,避免平票僵局
- 投票规则:每个任期只能投一票,候选人日志必须比本地日志「更新」
- 多数派原则:获得多数派投票的 Candidate 成为 Leader :::
日志复制
Leader 接收客户端请求后,将命令追加到本地日志,然后并行发送给所有 Follower。只有当多数派节点都已存储该日志条目时,Leader 才提交该条目并执行命令。
AppendEntries 的一致性检查
Follower 收到 AppendEntries 后,会进行一致性检查:验证 prevLogIndex 和 prevLogTerm 是否匹配。
:::tip 日志复制的核心原则:过半提交
Leader 接收客户端请求后,将命令追加到本地日志,然后并行发送给所有 Follower。只有当多数派节点都已存储该日志条目时,Leader 才提交该条目并执行命令。这个「过半」原则保证了 Raft 的 Leader Completeness Property。
日志不一致时的处理:如果 Follower 的日志与 Leader 不一致,Raft 的解决方案是「Leader 强制 Follower 复制自己的日志」。Leader 维护每个 Follower 的 nextIndex,如果 Follower 拒绝,Leader 递减 nextIndex 并重试——最终达到一致。
安全性保证
Raft 的安全性通过几个关键不变式保证:
Raft 安全性两大不变式:
- Leader Completeness Property:如果某个日志条目在某个任期被提交了,那么后续所有 Leader 都必须包含这个日志条目
- Log Matching Property:如果两个节点的日志在某个 index 之前都匹配,则 index 之前的所有日志都匹配 :::
不变式 1:Leader 不会覆盖已提交日志
这个不变式的证明依赖于:只有包含最新日志的 Candidate 才能赢得选举。
不变式 2:日志匹配特性
AppendEntries 的一致性检查天然保证了这一点。
简化代码:日志匹配检查
成员变更
最危险的分布式操作之一:如何安全地往集群中增加或移除节点?
:::danger 直接添加节点可能引发脑裂:假设原来 3 节点集群,需要扩展到 5 节点。如果新节点刚加入时旧 Leader 宕机,新旧节点可能分别选出不同的 Leader——两个 Leader 都认为自己拥有多数派。
安全的成员变更原则:
- 一次只变更一个节点:避免多节点同时变更带来的复杂性
- 新节点先以 Learner 身份加入:不参与投票,只同步日志,避免拖慢选举
- 移除节点前先转移 Leadership:如果移除的是 Leader,先让它把 Leadership 转给其他节点 :::
Joint Consensus(联合共识)
Raft 采用两阶段成员变更(这里简化描述核心思路):
第一阶段:集群使用 C_old ∪ C_new 配置,所有操作需要两边的多数派都同意。 第二阶段:切换到 C_new,只需要新配置的多数派同意。
:::details 点击展开:更安全的成员变更流程
实际实现中,Raft 成员变更有几个关键点:
- 一次只变更一个节点:避免多节点同时变更带来的复杂性
- 新节点先以 Learner 身份加入:不参与投票,只同步日志,避免拖慢选举
- 移除节点前先转移 Leadership:如果移除的是 Leader,先让它把 Leadership 转给其他节点
与 Paxos 的核心对比
权衡矩阵
术语表
延伸思考
Raft 强 Leader 特性的双刃剑
Raft 的强 Leader 特性既是优势也是劣势。在跨数据中心部署时,所有请求都经过 Leader 意味着跨机房延迟是不可避免的。如果业务对延迟敏感,可以考虑:
- 读本地化:允许 Follower 提供「可能过期但最终一致」的读
- 读选举:从 Candidate 中读(需额外验证)
- 分片 Raft:每个分片独立选主,分散热点
但这些优化都引入了新的复杂性。回到核心问题:Raft 的可理解性是以牺牲某些灵活性为代价的——这个 trade-off 在大多数场景下是值得的。