Raft协议#
一句话答案#
Raft 将分布式共识分解为 Leader 选举、日志复制、安全性三个子问题,通过”多数派同意即提交 + term 递增防脑裂”保证一致性,比 Paxos 更易理解和实现。
核心要点
Raft 三大子问题#
1. Leader Election —— 谁来当老大?
2. Log Replication —— 老大怎么让大家保持一致?
3. Safety —— 怎么保证已提交的数据不丢?plaintext1. Leader 选举#
节点三种角色:
- Leader:处理所有写请求,向 Follower 复制日志
- Follower:接受 Leader 的日志复制,响应读请求(部分实现)
- Candidate:正在竞选 Leader 的节点
选举流程:
1. Follower 在 election timeout(随机 150ms~300ms)内没收到 Leader 心跳
2. 转为 Candidate,term+1,投自己一票
3. 向所有节点发送 RequestVote RPC
4. 收到多数票 → 成为 Leader,立即发送心跳宣告
├── 同一 term 内每个节点只投一票(先到先得)
└── 投票条件:Candidate 的日志至少和自己一样新(term 更大,或 term 相同但 logIndex 更大)
5. 如果收到 term ≥ 自己的 Leader 的 AppendEntries → 承认对方,退回 Follower
6. 如果本轮既没当选也没发现 Leader(平票)→ 等待新的随机超时后 term+1 重新发起plaintext为什么用随机超时? 避免多个节点同时发起选举导致反复平票(split vote)。
2. 日志复制#
流程:
1. 客户端发送写请求到 Leader
2. Leader 将命令追加到自己的日志(uncommitted)
3. Leader 并行发送 AppendEntries RPC 给所有 Follower
4. Follower 验证日志一致性(prevLogIndex + prevLogTerm 匹配)
├── 匹配 → 追加日志,返回 success
└── 不匹配 → 返回 fail,Leader 递减 nextIndex 重试
5. Leader 收到多数节点(含自己)的 success → 提交日志(committed,推进 commitIndex,前提见下方「只能提交当前任期日志」)
6. Leader 返回客户端成功
7. 下一次心跳/AppendEntries 通知 Follower 提交已 committed 的日志plaintext日志匹配属性(Log Matching Property):
- 如果两个节点的日志在相同 index 有相同 term → 该 index 及之前的所有日志完全相同
- 这保证了 Leader 可以通过逐步回退 nextIndex 来对齐 Follower 的日志
3. 安全性保证#
三个关键保证:
| 保证 | 含义 |
|---|---|
| Election Safety | 同一 term 内最多一个 Leader |
| Leader Completeness | 已提交的日志一定存在于后续所有 Leader 中 |
| State Machine Safety | 所有节点按相同顺序执行相同的已提交日志 |
Leader Completeness 怎么保证?
- 投票时检查 Candidate 日志是否”足够新”
- Candidate 的最后一条日志 term 更大,或 term 相同 index 更大
- → 只有包含全部已提交日志的节点才可能当选
只能通过当前任期的日志来提交(论文 §5.4.2 / Figure 8):
- Leader 不能仅因为「旧任期的某条日志已复制到多数派」就认定它已提交——它仍可能被后来当选的 Leader 覆盖
- Leader 只对当前 term 的日志按「多数派复制」判定提交;当前 term 的日志一旦提交,之前的旧日志按日志匹配属性被间接一并提交
- 工程上新 Leader 上任后会立即追加一条当前 term 的空日志(no-op),尽快把之前的日志一起提交
脑裂防护#
场景:网络分区导致 Leader A 和 Follower B/C 分到不同网络
分区 1: [A(Leader, term=3)]
分区 2: [B, C] → B 超时发起选举,term=4,C 投票 → B 成为新 Leader
此时两个 "Leader":A(term=3) 和 B(term=4)
A 无法获得多数确认(只有自己)→ 任何写入都不会被提交
B 有多数节点 → 正常工作
网络恢复后:
A 收到 term=4 的消息 → 发现自己 term 过期 → 自动降级为 Follower
A 的未提交日志被 B 的日志覆盖
关键:term 递增 + 多数派 = 自动解决脑裂,不会出现两个有效 LeaderplaintextPreVote(预投票,Ongaro 博士论文 §9.6): 被隔离的少数派节点会不断超时、term 不断 +1,网络恢复后用更大的 term 把正常 Leader 逼下台,造成不必要的重新选举。PreVote 让 Candidate 先发一轮「如果我参选你会投我吗」(不增加自己的 term),确认能拿到多数派才真正 term+1 发起选举。etcd/raft 通过 PreVote 配置开启。
日志压缩(Snapshot)#
- 日志无限增长 → 内存和恢复时间不可控
- 定期做快照:将状态机当前状态保存为 Snapshot
- Snapshot 之前的日志可以丢弃
- 新节点加入时先传 Snapshot,再增量同步
ZAB(ZooKeeper)vs Raft 对比#
| 维度 | ZAB | Raft |
|---|---|---|
| 设计目标 | ZooKeeper 专用的原子广播 | 通用分布式共识 |
| Leader 选举 | 选最大 ZXID 的节点 | 随机超时 + 多数投票 |
| 任期标识 | epoch | term |
| 模式 | 恢复模式 / 广播模式 | 无显式模式,选举和复制无缝衔接 |
| 日志对齐 | 恢复阶段有 DIFF/TRUNC/SNAP | AppendEntries 逐步回退对齐 |
| 工程实现 | etcd 不用、ZooKeeper 用 | etcd / Consul / TiKV 都用 Raft |
本质相同: 都是 Leader-based、多数派确认、心跳检测。Raft 更强调可理解性。
Raft vs Paxos#
| 维度 | Paxos | Raft |
|---|---|---|
| 可理解性 | 极难(论文公认晦涩) | 易理解(论文以可理解性为目标) |
| Leader | Multi-Paxos 需要 Leader,Basic Paxos 不需要 | 强制 Leader |
| 工程实现 | 很少有完全按论文实现的 | 大量工程实现(etcd, Consul) |
面试回答(2分钟版)
Raft 把分布式共识拆成三个子问题。Leader 选举:每个节点有随机超时,超时后转为 Candidate,term+1 发起投票,多数票当选。随机超时避免 split vote。日志复制:Leader 收到写请求后追加日志,并行发 AppendEntries 给 Follower,多数确认后提交。Follower 日志不一致时通过回退 nextIndex 对齐。安全性:投票时检查 Candidate 日志是否足够新,保证已提交日志一定在新 Leader 中。脑裂防护靠 term 递增——网络分区时旧 Leader 无法提交(没有多数派),新 Leader term 更大,恢复后旧 Leader 自动降级。和 ZAB 本质相同(Leader-based + 多数派),Raft 更通用、更易理解,etcd 和 Consul 都用 Raft。
追问与易错
追问方向:
- “Raft 脑裂怎么解决?”→ term 递增 + 多数派,旧 Leader 无法提交,恢复后自动降级
- “日志复制和提交的区别?”→ 复制是日志写到 Follower 的日志里(持久化);提交是 Leader 确认当前任期的该条日志已被多数派持久化、推进 commitIndex;提交后各节点再按顺序**应用(apply)**到状态机——提交 ≠ 应用
- “Raft 和 Paxos 优缺点?”→ Raft 易理解但强制 Leader(单点瓶颈),Paxos 理论更优但工程难实现
- “Leader 宕机后数据会丢吗?”→ 已提交的不会丢(多数节点有),未提交的可能丢
易错点:
- ❌ “两个 Leader 会脑裂”——term 大的有效,小的自动降级,不会同时有两个有效 Leader
- ❌ “Leader 写完就算提交”——需要多数节点确认
- ❌ “旧任期日志只要复制到多数派就算提交”——Leader 只能按多数派规则提交当前任期的日志,旧日志随之间接提交(论文 Figure 8)
- ❌ “Raft 保证线性一致性读”——默认读 Leader 但可能读到旧数据(Leader 可能已过期),需要 ReadIndex 或 Lease Read 优化