Raft 协议通俗指南
一看就懂的分布式一致性协议
目录
1. 什么是分布式一致性问题?
1.1 一个简单的故事
想象你和朋友要在微信群决定周末去哪里吃饭:
你:「去吃火锅吧!」
朋友 A:「好啊,我同意」
朋友 B:「不行,我想吃日料」
朋友 C:「我觉得烧烤也不错」
问题来了:
- 谁来做最终决定?
- 如何确保大家达成一致?
- 如果有人网络断了怎么办?1.2 分布式系统的困境
java
/**
* 分布式系统面临的问题:
*
* 1. 网络不可靠
* - 消息可能延迟
* - 消息可能丢失
* - 网络可能分区
*
* 2. 节点不可靠
* - 节点可能崩溃
* - 节点可能重启
* - 节点可能永久故障
*
* 3. 时钟不同步
* - 不同节点时间不一致
* - 难以判断事件顺序
*
* CAP 定理:
* - Consistency (一致性): 所有节点看到相同数据
* - Availability (可用性): 每个请求都能得到响应
* - Partition Tolerance (分区容错): 网络分区时系统仍能运行
*
* 只能同时满足两个!
*/1.3 一致性的目标
┌─────────────────────────────────────────────────────────┐
│ 一致性的目标 │
├─────────────────────────────────────────────────────────┤
│ │
│ 所有节点在任何时刻看到相同的数据 │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Node A │ │ Node B │ │ Node C │ │
│ │ X = 5 │ │ X = 5 │ │ X = 5 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 写入 X = 10 后: │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Node A │ │ Node B │ │ Node C │ │
│ │ X = 10 │ │ X = 10 │ │ X = 10 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 而不是: │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Node A │ │ Node B │ │ Node C │ │
│ │ X = 10 │ │ X = 5 │ │ X = 10 │ ← 不一致 │
│ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────────────────┘2. 为什么需要 Raft?
2.1 之前的一致性协议问题
java
/**
* Paxos - 经典但复杂
*
* 问题:
* 1. 极其难懂
* - "Paxos made simple" 论文还是很难懂
* - 多种变体,容易实现错误
* 2. 缺乏实践指导
* - 只有理论,缺少工程细节
* 3. 难以实现
* - 很多实际实现都有 bug
*
* Raft 的设计目标:
* 1. 可理解性优先
* - 必须容易理解
* 2. 实用性
* - 提供完整的实现指导
* 3. 性能
* - 与 Paxos 性能相当
*/2.2 Raft 的设计哲学
┌────────────────────────────────────────────────────────┐
│ Raft 设计哲学 │
├────────────────────────────────────────────────────────┤
│ │
│ 1. 分解问题 │
│ ├── 领导者选举(Leader Election) │
│ ├── 日志复制(Log Replication) │
│ └── 安全性(Safety) │
│ │
│ 2. 减少状态 │
│ ├── 强制领导者(Strong Leader) │
│ └── 简化状态机 │
│ │
│ 3. 约束随机性 │
│ ├── 随机超时 │
│ └── 可预测的行为 │
│ │
└────────────────────────────────────────────────────────┘3. Raft 核心思想
3.1 强领导者模型
java
/**
* Raft 的核心:强领导者
*
* 与其他协议不同:
* - Multi-Paxos:任何节点都可以接受提案
* - Raft:只有 Leader 可以处理客户端请求
*
* 优势:
* 1. 简化日志管理
* - 日志只能从 Leader 流向 Follower
* - 不会出现日志分叉
*
* 2. 简化客户端交互
* - 客户端只需与 Leader 通信
* - 不需要关心其他节点
*
* 3. 简化一致性保证
* - Leader 保证日志不会冲突
*/3.2 整体架构
4. 角色与状态
4.1 三种角色
java
/**
* Raft 节点的三种状态:
*
* 1. Follower(跟随者)
* - 被动接收 Leader 的日志和心跳
* - 不主动发起任何请求
* - 如果长时间没收到心跳,转换为 Candidate
*
* 2. Candidate(候选人)
* - 候选人,竞选 Leader
* - 发起投票请求
* - 获得多数票成为 Leader
*
* 3. Leader(领导者)
* - 处理所有客户端请求
* - 负责日志复制
* - 定期发送心跳维持统治
*/4.2 状态转换
4.3 状态详解
┌────────────────────────────────────────────────────────────┐
│ Follower(跟随者) │
├────────────────────────────────────────────────────────────┤
│ │
│ 职责: │
│ - 接收 Leader 的日志复制请求 │
│ - 接收 Leader 的心跳 │
│ - 响应 Candidate 的投票请求 │
│ - 如果选举超时,转换为 Candidate │
│ │
│ 特点: │
│ - 被动,不主动发送请求(除了投票响应) │
│ - 随时准备转换角色 │
│ │
└────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────┐
│ Candidate(候选人) │
├────────────────────────────────────────────────────────────┤
│ │
│ 职责: │
│ - 发起投票请求 │
│ - 等待其他节点响应 │
│ - 统计投票结果 │
│ │
│ 特点: │
│ - 任期(Term)递增 │
│ - 首先投票给自己 │
│ - 向所有节点请求投票 │
│ - 选举超时后重新开始 │
│ │
└────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────┐
│ Leader(领导者) │
├────────────────────────────────────────────────────────────┤
│ │
│ 职责: │
│ - 处理所有客户端请求 │
│ - 将日志复制到所有 Follower │
│ - 定期发送心跳 │
│ - 告知 Follower 已提交的日志索引 │
│ │
│ 特点: │
│ - 强领导者,唯一处理客户端请求 │
│ - 心跳间隔远短于选举超时 │
│ - 保证日志不会冲突 │
│ │
└────────────────────────────────────────────────────────────┘5. 领导者选举
5.1 选举的触发条件
java
/**
* 何时触发选举?
*
* 正常情况:
* - 集群启动时,所有节点都是 Follower
* - 没有收到 Leader 心跳,触发选举
*
* Leader 故障:
* - Leader 崩溃
* - Follower 收不到心跳
* - 选举超时,转为 Candidate
*
* 网络分区:
* - Leader 被隔离
* - 剩余节点重新选举
* - 可能产生多个 Leader(脑裂)
* - 但只有拥有多数派的 Leader 能提交日志
*/5.2 Term (任期) 的概念
┌────────────────────────────────────────────────────────┐
│ Term(任期) │
├────────────────────────────────────────────────────────┤
│ │
│ Term 是单调递增的整数,充当逻辑时钟 │
│ │
│ Term 1 │
│ ┌─────────┐ │
│ │ Leader │ Node A │
│ └─────────┘ │
│ │ │
│ │ 崩溃 │
│ ↓ │
│ Term 2 │
│ ┌─────────┐ │
│ │ Leader │ Node B │
│ └─────────┘ │
│ │
│ 作用: │
│ 1. 识别过时的 Leader │
│ 2. 防止陈旧日志覆盖新日志 │
│ 3. 保证最多一个有效 Leader │
│ │
└────────────────────────────────────────────────────────┘5.3 选举完整流程
5.4 投票规则
java
/**
* Follower 如何决定投票给谁?
*
* 投票条件(必须同时满足):
*
* 1. Candidate 的 Term >= 自己的 Term
* - 如果 Term 更大,更新自己的 Term
* - 如果 Term 相同,检查日志
*
* 2. Candidate 的日志至少和自己一样新
* - 比较 Term:最后一条日志的 Term
* - 比较 Index:日志条目的索引
* - Term 更大 OR(Term 相同 AND Index 更大)
*
* 重要:
* - 每个 Term 最多投一票
* - 先投先得原则
* - 投票后重置选举超时
*/5.5 为什么需要随机超时?
┌────────────────────────────────────────────────────────┐
│ 随机选举超时的作用 │
├────────────────────────────────────────────────────────┤
│ │
│ 如果没有随机超时: │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Node A │ │ Node B │ │ Node C │ │
│ │超时:5s │ │超时:5s │ │超时:5s │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ │ │
│ └─────────────┴─────────────┘ │
│ 同时超时! │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 都转为 Candidate,都投票给自己 │
│ 结果:没人获得多数,选举失败! │
│ │
├────────────────────────────────────────────────────────┤
│ │
│ 使用随机超时(150ms - 300ms): │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Node A │ │ Node B │ │ Node C │ │
│ │超时:180ms│ │超时:250ms│ │超时:200ms│ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ │
│ ▼ │ │
│ 先超时,转为 Candidate │ │
│ │ │ │
│ │ 请求投票 ───────────────┘ │
│ ▼ │
│ 获得多数票,成为 Leader │
│ │
│ 发送心跳阻止其他节点超时 │
│ │
└────────────────────────────────────────────────────────┘5.6 处理分裂投票
┌────────────────────────────────────────────────────────┐
│ 分裂投票(Split Vote) │
├────────────────────────────────────────────────────────┤
│ │
│ 可能发生的情况: │
│ │
│ Node A 和 Node B 同时超时 │
│ │
│ Node A 投票给自己 │
│ Node B 投票给自己 │
│ Node C 投票给 Node A(随机) │
│ Node D 投票给 Node B(随机) │
│ Node E 投票给 Node A(随机) │
│ │
│ 结果: │
│ - Node A:3 票(A, C, E)← 赢得选举 │
│ - Node B:2 票(B, D) │
│ │
│ 如果票数相同: │
│ - 都没获得多数 │
│ - 选举超时后重新开始 │
│ - 由于随机性,下次通常能选出 Leader │
│ │
└────────────────────────────────────────────────────────┘6. 日志复制
6.1 日志的结构
java
/**
* Raft 日志的格式:
*
* 每条日志包含:
* 1. Index:日志条目的位置(单调递增)
* 2. Term:写入时的 Leader 任期
* 3. Command:客户端的命令
*
* 示例:
*
* Index | Term | Command
* ------|------|------------------------
* 1 | 1 | x = 5
* 2 | 1 | x = 10
* 3 | 2 | x = 15 ← Leader 切换
* 4 | 2 | y = 20
* 5 | 2 | z = 25
*
* 特性:
* - 不同节点的日志如果 Index 和 Term 相同,则该条日志相同
* - 单调递增的 Index
* - 单调递增的 Term(按 Index)
*/6.2 日志复制流程
6.3 Commit Index (提交索引)
java
/**
* 什么是 Commit Index?
*
* Commit Index:
* - 已知被复制到多数节点的最大日志索引
* - 只有 Commit Index 之前的日志才被提交
* - 提交的日志才能应用到状态机
*
* 更新时机:
* - Leader 收到 Follower 对某条日志的确认
* - 该日志被复制到多数节点
* - Leader 更新 commitIndex
*
* 传播:
* - Leader 通过 AppendEntries RPC
* - 告知 Follower 当前的 commitIndex
* - Follower 也更新自己的 commitIndex
*/6.4 日志一致性检查
java
/**
* AppendEntries RPC 的一致性检查:
*
* 请求参数:
* - prevLogIndex:前一条日志的索引
* - prevLogTerm:前一条日志的任期
* - entries[]:新的日志条目
*
* Follower 检查:
* 1. 查找本地日志中 index = prevLogIndex 的条目
* 2. 检查该条目的 term 是否等于 prevLogTerm
* 3. 如果匹配,追加新日志
* 4. 如果不匹配,拒绝请求
*
* Leader 的处理:
* - 收到拒绝后,递减 prevLogIndex
* - 重试 AppendEntries
* - 最终找到匹配点
*/6.5 日志冲突解决
┌────────────────────────────────────────────────────────┐
│ 日志冲突示例 │
├────────────────────────────────────────────────────────┤
│ │
│ Leader(Term 3): │
│ ┌───┬───┬───┬───┬───┬───┐ │
│ │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ ← Index │
│ ├───┼───┼───┼───┼───┼───┤ │
│ │ 1 │ 1 │ 2 │ 3 │ 3 │ 3 │ ← Term │
│ ├───┼───┼───┼───┼───┼───┤ │
│ │ A │ B │ C │ D │ E │ F │ ← Command │
│ └───┴───┴───┴───┴───┴───┘ │
│ ↑ │
│ Follower: ← prevLogIndex=5, Term=3 │
│ ┌───┬───┬───┬───┬───┬───┐ │
│ │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ │
│ ├───┼───┼───┼───┼───┼───┤ │
│ │ 1 │ 1 │ 2 │ 2 │ 2 │ 2 │ ← Term 不同! │
│ ├───┼───┼───┼───┼───┼───┤ │
│ │ A │ B │ C │ X │ Y │ Z │ ← 冲突 │
│ └───┴───┴───┴───┴───┴───┘ │
│ ↑ │
│ 匹配点:prevLogIndex=2, Term=1 │
│ │
│ 解决过程: │
│ 1. Leader 发送 prevLogIndex=5, Term=3 │
│ 2. Follower 检查:Index 5 的 Term 是 2,不是 3 │
│ 3. Follower 拒绝 │
│ 4. Leader 递减到 prevLogIndex=4, Term=3 │
│ 5. Follower 仍然拒绝(Term 是 2) │
│ 6. Leader 递减到 prevLogIndex=3, Term=2 │
│ 7. Follower 检查:Index 3 的 Term 是 2,匹配! │
│ 8. Follower 删除 Index 3 之后的所有日志 │
│ 9. Follower 追加 Leader 的新日志 │
│ │
│ 最终 Follower 日志与 Leader 一致 │
│ │
└────────────────────────────────────────────────────────┘7. 安全性保证
7.1 选举安全性
java
/**
* 选举安全性(Election Safety):
*
* 定理:在一个任期内,最多有一个 Leader 被选出
*
* 证明:
* 1. 一个 Candidate 要成为 Leader,必须获得多数票
* 2. 多数票集合必然有交集
* 3. 两个 Candidate 不可能同时获得多数票
* 4. 因此每个 Term 最多一个 Leader
*
* 意义:
* - 保证日志不会出现分支
* - 保证状态机一致性
*/7.2 Leader 完整性
java
/**
* Leader 完整性(Leader Completeness):
*
* 定理:如果一条日志在某个 Term 被提交,
* 那么这条日志将在所有未来的 Term 中存在
*
* 证明:
* 1. 日志被提交 = 被复制到多数节点
* 2. 未来的 Leader 必须获得多数票
* 3. 多数集合有交集
* 4. 新 Leader 必然拥有所有已提交的日志
*
* 意义:
* - 已提交的日志不会丢失
* - 保证数据持久性
*/7.3 日志匹配性
java
/**
* 日志匹配性(Log Matching Property):
*
* 定理:如果两个日志包含相同(Index, Term)的条目,
* 那么这个条目之前的所有条目都相同
*
* 证明:
* 1. 日志只追加,不修改
* 2. AppendEntries 的一致性检查
* 3. 如果冲突,Follower 会截断
* 4. 最终收敛到一致状态
*
* 意义:
* - 保证日志最终一致
* - 简化日志复制
*/7.4 为什么新 Leader 必须拥有最新日志?
java
/**
* 投票限制:只有日志最新的节点才能成为 Leader
*
* 为什么?
*
* 反例:如果允许旧日志节点成为 Leader
*
* Term 2:
* Leader A 的日志:[1, 2, 3, 4, 5]
* Follower B 的日志:[1, 2, 3] ← 落后
*
* 如果 B 在 Term 3 成为 Leader:
* B 可能提交日志:[1, 2, 3, 6] ← 覆盖了 4, 5!
*
* 这违反了 Leader 完整性!
*
* 解决:投票时检查日志新旧
* - 比较 Term:最后一条日志的 Term
* - 比较 Index:日志索引
* - 必须至少和自己一样新
*/8. 实战案例
8.1 Kafka 的 KRaft 实现
java
/**
* Kafka 如何使用 Raft?
*
* __cluster_metadata Topic:
* - 相当于 Raft 的日志
* - 存储集群元数据
* - 只在 Controller 节点
*
* Controller 角色:
* - Leader:处理元数据变更
* - Follower:同步元数据
*
* 元数据变更流程:
* 1. Controller Leader 接收请求(如 CreateTopic)
* 2. 生成记录,追加到 __cluster_metadata
* 3. 通过 Raft 协议复制到其他 Controller
* 4. 多数确认后提交
* 5. 应用到状态机(内存元数据)
* 6. 发布快照到所有 Broker
*
* Kafka 特点:
* - 使用 Snapshot 加快启动
* - 批量处理提高效率
* - MetadataCache 作为状态机
*/8.2 Etcd 的 Raft 实现
java
/**
* Etcd 如何使用 Raft?
*
* etcd 是一个分布式 KV 存储:
*
* 架构:
* - Raft 层:复制日志,保证一致性
* - 存储层:boltdb,持久化日志
* - API 层:gRPC,客户端接口
*
* Key-Value 存储:
* - Put/Delete 操作作为 Raft 日志
* - 提交后应用到 boltdb
* - 提供 ReadIndex 保证线性一致性读
*
* 特点:
* - WAL(Write-Ahead Log)持久化
* - Snapshot 压缩日志
* - 变更 Watch 机制
*/8.3 Consul 的 Raft 实现
java
/**
* Consul 如何使用 Raft?
*
* Consul 使用 Raft 管理:
* - 服务注册信息
* - 健康检查状态
* - KV 存储
*
* 特点:
* - 多数据中心
* - 每个 Datacenter 一个 Raft 集群
* - WAN gossip 跨数据中心同步
*
* 一致性模式:
* - consistent:强一致性读
* - default:最终一致性读
* - stale:允许读 stale 数据
*/8.4 Raft 参数调优
java
/**
* Raft 关键参数:
*
* 1. 选举超时(Election Timeout)
* - 典型值:150ms - 300ms
* - 影响:Leader 故障检测速度
* - 调优:网络延迟高时增大
*
* 2. 心跳间隔(Heartbeat Interval)
* - 典型值:50ms - 100ms
* - 影响:阻止不必要选举
* - 调优:远小于选举超时
*
* 3. 日志批量大小
* - 典型值:几 KB 到几 MB
* - 影响:吞吐量 vs 延迟
* - 调优:高吞吐场景增大
*
* 4. 快照阈值
* - 典型值:几千到几万条
* - 影响:内存占用 vs 恢复速度
* - 调优:日志增长快时降低
*/9. 总结
9.1 Raft 的优势
┌────────────────────────────────────────────────────────┐
│ Raft 的优势 │
├────────────────────────────────────────────────────────┤
│ │
│ 1. 可理解性 │
│ ├── 分解为独立子问题 │
│ ├── 强领导者简化设计 │
│ └── 状态转换清晰 │
│ │
│ 2. 实用性 │
│ ├── 提供完整实现指导 │
│ ├── 考虑工程细节 │
│ └── 广泛应用于生产 │
│ │
│ 3. 正确性 │
│ ├── 严格证明 │
│ ├── 避免常见错误 │
│ └── 社区验证 │
│ │
│ 4. 性能 │
│ ├── 与 Paxos 相当 │
│ ├── 批量处理优化 │
│ └── 流水线复制 │
│ │
└────────────────────────────────────────────────────────┘9.2 学习路径
第一步: 理解基本概念
├── 分布式一致性问题
├── CAP 定理
└── Raft 的设计目标
第二步: 理解核心机制
├── 领导者选举
├── 日志复制
└── 安全性保证
第三步: 实践
├── 阅读 Raft 论文
├── 实现 Raft 算法
└── 使用 Raft 系统 (etcd, Kafka)
第四步: 深入
├── 性能优化
├── 生产实践
└── 变体和扩展9.3 资源推荐
java
/**
* 学习资源:
*
* 1. 论文
* - "In Search of an Understandable Consensus Algorithm"
* - Diego Ongaro & John Ousterhout
*
* 2. 可视化
* - http://thesecretlivesofdata.com/raft/
* - Raft 动画演示
*
* 3. 实现
* - etcd/raft(Go)
* - hashicorp/raft(Go)
* - Apache Ratis(Java)
*
* 4. 教程
* - The Raft Consensus Algorithm(MIT 6.824)
* - Raft 用户指南
*/附录: 常见问题
Q1: Raft 和 Paxos 的区别?
java
/**
* Raft vs Paxos:
*
* 可理解性:
* - Raft:专为可理解性设计
* - Paxos:理论复杂,实现困难
*
* 领导者:
* - Raft:强领导者,只有 Leader 处理请求
* - Paxos:Basic Paxos 无 Leader,Multi-Paxos 有 Leader
*
* 日志复制:
* - Raft:只有 Leader 追加日志
* - Paxos:任何节点都可以提议
*
* 使用场景:
* - Raft:etcd, Consul, Kafka(KRaft),TiKV
* - Paxos:Google Spanner,Chubby
*/Q2: 为什么需要多数派?
java
/**
* 为什么需要多数派(Quorum)?
*
* 原因:保证只有一个 Leader
*
* 反例:简单多数
*
* 假设 4 个节点,只要 2 个同意就可以:
* - 节点 A、B 选举出 Leader 1
* - 节点 C、D 选举出 Leader 2
* - 两个 Leader!(脑裂)
*
* 使用多数派(N/2 + 1):
* - 4 个节点需要 3 个同意
* - A、B 选举 Leader 1(2 票,不够)
* - 需要至少拉到 C 或 D
* - C、D 就无法再选举出另一个 Leader
*
* 结论:多数派保证唯一性
*/Q3: Raft 如何处理网络分区?
java
/**
* 网络分区场景:
*
* 集群:5 个节点(A, B, C, D, E)
* 分区前:A 是 Leader
*
* 网络分区:
* - 分区 1:A, B(2 个节点)
* - 分区 2:C, D, E(3 个节点)
*
* 分区 1:
* - A 收不到多数节点的心跳响应
* - A 成为 Follower(Term 递增)
* - 没有新 Leader(票数不够)
*
* 分区 2:
* - C, D, E 触发选举
* - C 成为 Leader(Term 2)
* - 有 3 票,超过多数
*
* 恢复后:
* - A 发现 Term 2 > Term 1
* - A 成为 Follower
* - C 保持 Leader
* - A 截断日志,从 C 同步
*
* 结果:最多一个 Leader 能提交日志
*/作者: Claude Code
最后更新: 2026-03-01
反馈: 如有疑问或建议,欢迎提 Issue