前言
上一篇我们从集合交集定律推导出了多数派机制,并在此基础上推演了 Basic Paxos 和 Multi-Paxos 的 Leader 优化思想。但我们也提到,Paxos 的原论文只给出了核心思想,留下了大量工程空白:Leader 怎么选?日志怎么复制?故障怎么恢复?
这些空白导致了一个尴尬的现状------每个团队实现的"Paxos"各不相同,相互之间难以对齐,甚至出现了"世界上只有一种共识算法,就是 Paxos,但没人知道它到底是什么"的调侃。
2014 年,Diego Ongaro 和 John Ousterhout 发表了 Raft 算法,设计的首要目标不是性能,而是可理解性(Understandability)。Raft 把共识问题拆解成三个相对独立的子问题:
- 领导者选举(Leader Election):谁来当 Leader?
- 日志复制(Log Replication):Leader 怎么把数据同步到所有节点?
- 安全性(Safety):怎么保证换 Leader 后数据不丢不乱?
本篇将逐一展开这三个子问题。
一、Raft 的设计哲学
强 Leader 模型
在 Basic Paxos 中,任何节点都可以发起提案,这种"人人平等"的设计带来了灵活性,但也带来了活锁和实现复杂度。
Raft 做了一个大胆的简化:一切以 Leader 为中心。 集群中的节点分为 Leader、Follower、Candidate 三种角色(后面会详细介绍),核心规则是:
- 客户端的所有写请求只能发给 Leader。
- 日志只能从 Leader 流向 Follower,单向复制,绝不反向。
- Follower 和 Candidate 收到客户端请求时,直接转发给 Leader。
这种"独裁"模型的好处很明显:所有决策由一个节点统一做,不存在并发冲突,实现起来也简单得多。
状态机复制(Replicated State Machine)
💡 什么是状态机? 状态机是一个抽象模型:给定一个初始状态,按顺序输入一系列命令,每条命令都会确定性地将状态从一个值转变为另一个值。相同的初始状态 + 相同的命令序列 = 相同的最终状态。数据库、KV 存储、配置中心本质上都是状态机。
Raft 的目标是让一组服务器表现得像一台可靠的服务器。实现方式是状态机复制:
- Client 发送命令给 Leader。
- Leader 把命令追加到自己的日志中。
- Leader 将日志复制给所有 Follower。
- 多数派 Follower 确认写入后,Leader 提交该日志,将命令应用到自己的状态机,并回复 Client。
- Leader 通过后续心跳将提交进度同步给 Follower,Follower 将已提交的日志应用到各自的状态机。
最终,所有节点持有相同的日志、按相同的顺序执行,状态必然一致:
ini
Client 命令 ──> 日志(有序序列)──> 状态机(执行命令)──> 一致的状态
节点1: [set x=1] [set y=2] [del x] ──> 状态机 ──> {y=2}
节点2: [set x=1] [set y=2] [del x] ──> 状态机 ──> {y=2}
节点3: [set x=1] [set y=2] [del x] ──> 状态机 ──> {y=2}
↑
相同的日志 = 相同的状态
所以 Raft 的核心任务就是:维护一个一致的分布式日志。
三种角色
Raft 中的每个节点在任意时刻处于三种状态之一:
- Leader(领导者) :处理所有客户端请求,负责日志复制。整个集群同一时刻最多只有一个
Leader。 - Follower(跟随者) :被动角色,只响应
Leader和Candidate的请求,不主动发起任何操作。 - Candidate(候选人) :
Follower在选举超时后转变为Candidate,发起投票竞选Leader。
三者之间的转换关系:
所有节点启动时都是 Follower。只有在超时未收到 Leader 心跳时,才会转为 Candidate 发起选举。
💡 选举超时(Election Timeout):每个节点内部维护一个随机计时器(通常 150ms~300ms)。Follower 每次收到 Leader 心跳就重置计时器;如果计时器到期仍未收到心跳,就认为 Leader 已失联,转为 Candidate 发起选举。Candidate 在选举期间如果超时未拿到多数派投票,也会触发同一计时器重新选举。
二、领导者选举(Leader Election)
任期(Term):Raft 的逻辑时钟
分布式系统中没有全局物理时钟,Raft 用**任期(Term)**来充当逻辑时钟。
- 每个任期由一个递增的整数标识:Term 1, Term 2, Term 3...
- 每个任期最多只有一个 Leader(也可能选举失败,没有 Leader)。
- 任期是 Raft 判断信息新旧的唯一标准------如果一个节点收到了更高任期的消息,说明自己的信息已经过时,必须立刻"让位"。
任期的规则很简单:
- 节点发起选举时,将自己的 Term +1。
- 节点在通信中发现对方的 Term 比自己大 → 立刻更新自己的 Term,并退回 Follower 状态。
- 节点收到 Term 比自己小的请求 → 直接拒绝。
选举流程
当一个 Follower 在**选举超时(Election Timeout)**时间内没有收到 Leader 的心跳,它就认为 Leader 挂了,发起选举:
- 自增当前 Term。
- 转变为 Candidate 状态。
- 给自己投一票。
- 向所有其他节点发送
RequestVoteRPC。
Term: 1 → 2
给自己投一票 rect rgb(240, 248, 255) F1->>F2: RequestVote(Term=2, CandidateId=A) F1->>F3: RequestVote(Term=2, CandidateId=A) F2-->>F1: 投票给 A ✅ F3-->>F1: 投票给 A ✅ end Note over F1: 获得 3/3 票(含自己)
当选 Leader 🎉 rect rgb(240, 255, 240) F1->>F2: AppendEntries(心跳) F1->>F3: AppendEntries(心跳) end Note over F1,F3: A 开始以 Leader 身份服务
定期发送心跳维持权威
每个节点在同一个 Term 内只能投一票 ,先到先得。这保证了同一任期内最多只有一个节点能获得多数派投票,即最多一个 Leader。
投票三结局
Candidate 发起选举后,有三种可能的结局:
结局一:赢得选举 → 获得多数派投票,成为 Leader,立即开始发送心跳。
结局二:别人赢了 → 在等待投票的过程中,收到了来自新 Leader 的 AppendEntries 心跳(且 Term ≥ 自己的 Term)。说明已经有人当选了,自己乖乖退回 Follower。
结局三:没人赢(选票瓜分) → 多个 Candidate 同时发起选举,把选票分散了,谁都没拿到多数派。超时后重新选举。
第三种情况如果不处理,可能会反复发生------多个节点总是同时超时、同时发起选举、同时瓜分选票。
随机超时:打破对称
Raft 用一个极其简单的方式解决选票瓜分:随机化选举超时时间。
每个节点的选举超时从一个固定区间(例如 150ms~300ms)中随机选取。这样,大多数情况下只有一个节点最先超时并发起选举,在其他节点超时之前就已经赢得了多数派投票。
css
节点A 超时:|████████████████░░░░░░░░░░| 187ms → 率先发起选举
节点B 超时:|████████████████████████░░| 263ms → 还没超时就收到A的投票请求
节点C 超时:|██████████████████████████| 291ms → 还没超时就收到A的心跳
论文中的实验数据表明,150ms~300ms 的随机超时区间在大多数网络环境下,选举通常在一轮内完成。
三、日志复制(Log Replication)
Leader 选出来了,接下来的核心任务是:把客户端的命令安全地复制到所有节点。
日志的结构
每条日志(Log Entry)包含三个要素:
- 索引(Index):日志在序列中的位置,从 1 开始递增。
- 任期(Term):创建该日志时 Leader 的任期号。
- 命令(Command):客户端请求的具体操作。
ini
日志序列示意:
Index: 1 2 3 4 5
Term: [1] [1] [2] [2] [2]
Cmd: [set x=1] [set y=2] [del x] [set z=3] [set x=5]
复制流程
当 Leader 收到客户端的写请求时:
提交日志,执行命令 L-->>C: 返回结果: OK rect rgb(240, 255, 240) Note over L,F2: 4. 下一次心跳通知 Follower 提交 L->>F1: AppendEntries(commitIndex 更新) L->>F2: AppendEntries(commitIndex 更新) Note over F1: 执行 set x=42 Note over F2: 执行 set x=42 end
关键点:
- Leader 先写本地日志,然后并行发送给所有 Follower,不需要等所有人都回复。
- 只要多数派(含 Leader 自己)确认写入,Leader 就认为日志已提交(Committed),可以执行命令并回复客户端。
- Follower 通过后续的 AppendEntries 得知 Leader 的 commitIndex 更新后,在本地执行已提交的日志。
⚠️注意,并非每个 Follower 都会立刻成功------有的节点可能因为日志不一致而拒绝,有的节点可能已经宕机。但这不会阻塞客户端请求:Leader 只需要多数派确认就够了,不一致的节点会在后续的 AppendEntries 中被逐步修复(下文"一致性检查"会详述)。
那如果连多数派都凑不够呢? :比如 5 节点挂了 3 个(注意,宕机不等于退出集群,多数派仍按总数 5 计算,需要 3 票 )。此时日志无法提交,Leader 不会回复客户端,请求会一直挂起,直到客户端自身超时放弃。Raft 不会主动返回错误------宁可不响应也不给错误的结果。这正是第三篇中 CAP 定理的体现:节点故障超过半数时,Raft 牺牲可用性来保证一致性。
AppendEntries RPC:不只是"追加"
AppendEntries 是 Raft 中最核心的 RPC,它同时承担三个职责:
- 日志复制:携带新的日志条目让 Follower 追加。
- 心跳维持:即使没有新日志,Leader 也定期发送空的 AppendEntries 来维持自己的权威,防止 Follower 超时发起选举。
- 提交通知 :通过
leaderCommit字段告诉 Follower "哪些日志已经可以执行了"。
一致性检查
Leader 发送 AppendEntries 时,会附带前一条日志的 Index 和 Term (称为 **prevLogIndex **和 prevLogTerm)。
Follower 收到后会检查:我在 prevLogIndex 位置的日志,Term 是否等于 prevLogTerm?
- 匹配 → 说明到这个位置为止,我和 Leader 的日志是一致的,可以安全追加新日志。
- 不匹配 → 拒绝,Leader 会将
prevLogIndex回退一位重试。
ini
场景:Leader 发送 AppendEntries,prevLogIndex=4, prevLogTerm=2
Leader 日志: [1|1] [2|1] [3|2] [4|2] [5|2] ← 要追加 index=5
Follower A: [1|1] [2|1] [3|2] [4|2] ← index=4 的 Term=2 ✅ 匹配,追加
Follower B: [1|1] [2|1] [3|1] ← index=4 不存在 ❌ 拒绝
Follower C: [1|1] [2|1] [3|2] [4|3] ← index=4 的 Term=3≠2 ❌ 拒绝
这个机制通过逐步回退,最终一定能找到 Leader 和 Follower 日志一致的那个点,然后从那个点开始覆盖 Follower 的日志。
Leader 的强制覆盖
当 Follower 的日志和 Leader 不一致时,Raft 的处理原则很霸道:以 Leader 为准,强制覆盖 Follower 的冲突日志。
这背后的逻辑是:Leader 的日志一定包含所有已提交的条目(下一节安全性中会严格论证),所以用 Leader 的日志覆盖 Follower 不会丢失任何已提交的数据。
来看一个具体的修复过程:
ini
初始状态(Leader 任期为 3):
Leader: [1|1] [2|1] [3|2] [4|2] [5|3]
Follower: [1|1] [2|1] [3|2] [4|3] [5|3] [6|3]
↑
从这里开始分叉(index=4 的 Term 不同)
修复过程:
第1次: Leader 发送 prevLogIndex=4, prevLogTerm=2
Follower 检查 index=4 → Term=3 ≠ 2 → 拒绝 ❌
第2次: Leader 回退, 发送 prevLogIndex=3, prevLogTerm=2
Follower 检查 index=3 → Term=2 = 2 → 匹配 ✅
第3次: Leader 从 index=4 开始发送自己的日志
Follower 删除 index≥4 的旧日志,接受 Leader 的版本
修复后:
Leader: [1|1] [2|1] [3|2] [4|2] [5|3]
Follower: [1|1] [2|1] [3|2] [4|2] [5|3] ← 与 Leader 一致 ✅
注意 Follower 原来有 6 条日志,修复后只有 5 条------多出来的、未提交的日志被丢弃了。这是安全的,因为这些日志从未被多数派确认过,不算"已提交"。
四、安全性(Safety)
选举和日志复制只是 Raft 的"骨架"。如果没有额外的安全性约束,上述机制其实存在漏洞。本节回答两个关键问题:谁能当 Leader?什么时候才能提交日志?
选举限制:不是谁都能当 Leader
假设没有任何限制,任意 Follower 都能当选 Leader。考虑这个场景:
less
5 节点集群,当前 Leader 是 A(Term=2):
节点A (Leader): [1|1] [2|1] [3|2] [4|2] [5|2] ← index 5 已提交(多数派确认)
节点B: [1|1] [2|1] [3|2] [4|2] [5|2]
节点C: [1|1] [2|1] [3|2] [4|2] [5|2]
节点D: [1|1] [2|1] ← 落后很多
节点E: [1|1] [2|1] ← 落后很多
如果 A、B、C 同时宕机,D 当选新 Leader:
- D 只有 2 条日志,index 3~5 的已提交数据全部丢失!
Raft 的解决方案:Candidate 在 RequestVote 中携带自己最后一条日志的 Index 和 Term。投票者只会把票投给日志"不比自己旧"的 Candidate。
"不比自己旧"的比较规则:
- 先比 Term:最后一条日志的 Term 越大越新。
- Term 相同则比 Index:Index 越大越新。
ini
投票判断示例:
Candidate 最后日志: (Index=5, Term=2)
Voter 最后日志: (Index=3, Term=3)
比较: Candidate Term=2 < Voter Term=3 → Candidate 更旧 → 拒绝投票 ❌
---
Candidate 最后日志: (Index=5, Term=2)
Voter 最后日志: (Index=3, Term=2)
比较: Term 相同,Candidate Index=5 > Voter Index=3 → Candidate 更新 → 投票 ✅
这条规则保证了新 Leader 在所有 Candidate 中日志是最新的,也为上一节"Leader 强制覆盖"提供了安全性基础------Leader 日志最全,覆盖别人不会丢数据。
不过,选举比较的是节点实际持有的最后一条日志,不区分它是否已提交。这意味着一条从未被多数派确认的高 Term 日志,也能让节点在比较中胜出。所以光有选举限制还不够,我们还需要对"提交"本身做约束。
提交限制:当前任期提交原则
来看一个经典的反例:
ini
5 节点集群,时间线如下:
T1: 节点S1 是 Leader(Term=2),将 index=2 的日志(Term=2)复制给了 S2
S1: [1|1] [2|2]
S2: [1|1] [2|2]
S3: [1|1]
S4: [1|1]
S5: [1|1]
# s5 此前已通过心跳同步到 Term=2
T2: S1 宕机,S5 发起选举(Term=2+1=3),当选 Leader(Term=3),收到新日志但还没复制就宕机了
S5: [1|1] [2|3]
T3: S1 恢复,重新当选 Leader(Term=4),继续复制 index=2(Term=2)给 S3
S1: [1|1] [2|2] ← 此时 index=2 已经在 S1、S2、S3 上,构成多数派
S2: [1|1] [2|2]
S3: [1|1] [2|2]
S4: [1|1]
S5: [1|1] [2|3]
⚠️ 如果此时 S1 提交 index=2(Term=2),然后 S1 再次宕机------
S5 可能当选(S5 最后日志的 Term=3 > S2、S3 最后日志的 Term=2,日志更"新")
S5 会用 [2|3] 覆盖掉已经"提交"的 [2|2]
💥 已提交的数据被覆盖了!
问题出在哪?S1 在 Term=4 时提交了一条旧任期(Term=2)的日志 。虽然这条日志已经被多数派写入,但它的 Term 太旧了,无法阻止一个持有更高 Term 日志的节点(S5)当选并覆盖它。
Raft 的解决方案:Leader 选举成功后不直接提交旧任期的日志,而是先写入一条当前任期的日志(可以是客户端的新请求,也可以是一条空的 no-op 日志)。
旧任期的日志怎么办?它们会在当前任期的日志被提交时间接提交------因为日志是有序的,提交了 index=3(Term=4),意味着 index≤3 的日志都已提交。
ini
正确做法:
T3: S1 当选 Leader(Term=4),先写入一条当前任期的日志
S1: [1|1] [2|2] [3|4] ← 新日志 Term=4
将 index=3(Term=4)复制到多数派后提交
此时 index=2(Term=2)也被间接提交
即使 S1 再宕机,S5 也无法当选了:
S2、S3 持有 Term=4 的日志,比 S5 的 Term=3 更新
S5 拿不到多数派投票 ✅
安全性保证总结
-
选举限制:确保了选出来的 Leader 日志够全------日志落后的节点拿不到多数派选票,当不了 Leader。
-
提交限制:确保了提交的日志 Term 够高------只提交当前任期的日志,旧任期日志间接提交,不给未提交的高 Term 日志可乘之机。
选出来的够全,提交的够高,二者合力保证了 Raft 的核心安全性------Leader 完备性(Leader Completeness Property):
如果一条日志在某个任期内被提交,那么所有更高任期的 Leader 一定包含这条日志。
换句话说:已提交的数据,无论换多少任 Leader,都不会丢。
选出来的 Leader 日志够全"] --> C["🔒 Leader 完备性
新 Leader 一定包含所有已提交日志"] B["📝 提交限制
提交的日志 Term 够高"] --> C C --> D["✅ 强制覆盖安全
被覆盖的一定是未提交数据"] C --> E["✅ 客户端承诺
回复 OK 的写入永不丢失"] style A fill:#4ecdc4 style B fill:#4ecdc4 style C fill:#ff6b6b,color:#fff style D fill:#ffe66d style E fill:#ffe66d
五、成员变更(Membership Change)
生产环境中,集群节点不可能一成不变------要扩容、要缩容、要替换故障机器。但在一个正在运行共识协议的集群中"换人",远比想象中危险。
直接切换的危险
假设一个 3 节点集群要扩容到 5 节点。如果所有节点在同一时刻直接从旧配置切换到新配置,理论上没问题。但分布式系统中不存在"同一时刻"------不同节点切换的时间点不同,就会出现两个多数派共存的窗口:
css
旧配置:{A, B, C} 多数派 = 2
新配置:{A, B, C, D, E} 多数派 = 3
如果 A、B 已切换到新配置,C、D、E 还在旧配置(或刚加入):
旧配置视角:{A, B, C} 中 C 还认旧配置,C 自己就能和某个节点凑成多数派(2/3)
新配置视角:{A, B, ...} 已切换,A、B 加上 D 也能凑成多数派(3/5)
💥 两个多数派可能选出两个 Leader!
单节点变更(Single-server Changes)
Raft 推荐的方案是最保守也最安全的:每次只增加或移除一个节点。
为什么一次只变一个就安全?因为旧配置和新配置的多数派一定有交集:
ini
旧配置 3 节点,加 1 个变 4 节点:
旧多数派 = 2,新多数派 = 3
2 + 3 = 5 > 4 → 必有交集 ✅
旧配置 4 节点,加 1 个变 5 节点:
旧多数派 = 3,新多数派 = 3
3 + 3 = 6 > 5 → 必有交集 ✅
有交集就意味着:不可能同时存在两个网络分区的多数派,也就不可能选出两个 Leader。这和 Quorum 的集合交集定律是同一个道理。
具体流程:
- Leader 收到成员变更请求(比如"添加节点 D")。
- Leader 将新配置作为一条特殊的日志条目追加并复制。
- 这条日志一旦写入,节点立即使用新配置,不需要等到提交。
- 新配置日志被多数派确认后提交,变更完成。
如果要从 3 节点扩到 5 节点,就分两步:先 3→4,等稳定后再 4→5。
联合共识(Joint Consensus)
单节点变更简单安全,但每次只能加减一个,大规模变更需要多轮操作。Raft 还给出了联合共识方案,允许一次性从旧配置切换到任意新配置:
- Leader 先进入过渡配置 C_old,new :任何决策都需要同时获得旧配置的多数派和新配置的多数派。
- 过渡配置提交后,Leader 再切换到新配置 C_new。
- C_new 提交后,变更完成,旧配置中不在新配置里的节点可以下线。
旧配置"] -->|"Leader 提交过渡配置"| B["C_old,new
联合共识"] B -->|"Leader 提交新配置"| C["C_new
新配置"] style A fill:#4ecdc4 style B fill:#ffe66d style C fill:#4ecdc4
过渡阶段需要"双多数派"确认,保证了在任何时刻都不会出现两个独立的多数派。代价是实现复杂度较高,实际工程中大多选择单节点变更。
六、日志压缩(Log Compaction)
问题:日志不能无限增长
Raft 的所有数据都以日志形式存储。随着系统运行,日志会不断增长:
- 磁盘空间被持续消耗。
- 节点重启时需要重放所有日志才能恢复状态,耗时越来越长。
- 新节点加入时需要同步全量日志,网络开销巨大。
Snapshot 机制
Raft 的解决方案是快照(Snapshot):定期将当前状态机的完整状态保存为一个快照文件,然后丢弃快照之前的所有日志。
ini
快照前:
日志: [1|1] [2|1] [3|2] [4|2] [5|3] [6|3] [7|3] [8|4]
状态机: {x=5, y=2, z=3}
执行快照(截止到 index=6):
快照文件: { lastIndex=6, lastTerm=3, state={x=5, y=2, z=3} }
日志: [7|3] [8|4] ← index≤6 的日志全部丢弃
快照中需要记录:
- lastIncludedIndex:快照包含的最后一条日志的 Index。
- lastIncludedTerm:该日志的 Term。
- 状态机数据:当前的完整状态。
每个节点独立决定何时做快照,不需要 Leader 协调。常见策略是日志大小超过阈值时触发。
InstallSnapshot RPC
当 Follower 落后太多,Leader 需要发送的日志已经被快照覆盖(被丢弃了)时,Leader 无法通过正常的 AppendEntries 同步数据。此时 Leader 会通过 InstallSnapshot RPC 直接把快照发给 Follower:
ini
场景:Follower 落后太多
Leader 日志: [快照: 截止index=100] [101|5] [102|5] [103|5]
Follower 日志: [1|1] [2|1] [3|2] ← 远远落后
Leader 无法发送 index=4~100 的日志(已被快照覆盖)
→ 直接发送 InstallSnapshot
Follower 收到后:
1. 丢弃自己的全部日志
2. 用快照恢复状态机
3. 从 index=101 开始正常接收 AppendEntries
这个机制也让新节点加入集群变得简单------直接接收当前快照,不需要从第一条日志开始同步。
总结
本篇完整介绍了 Raft 算法:
- 领导者选举:通过任期(Term)实现逻辑时钟,通过随机超时打破选票瓜分,保证每个任期最多一个 Leader。
- 日志复制 :Leader 将日志并行复制到 Follower,多数派确认后提交。通过
prevLogIndex/prevLogTerm做一致性检查,不一致时逐步回退并强制覆盖。 - 安全性:选举限制保证新 Leader 日志最全,提交限制避免旧任期日志被错误覆盖。两者共同保证已提交的数据永远不会丢失。
- 成员变更:单节点变更利用多数派交集保证安全,联合共识支持一次性大规模变更。
- 日志压缩:Snapshot 机制定期保存状态机快照,丢弃旧日志,解决日志无限增长和节点恢复慢的问题。
Raft 的成功在于它把 Multi-Paxos 的思想拆解成了可独立理解的模块,每个模块的规则都足够清晰。这也是为什么 Nacos、etcd、Consul等主流系统都选择了 Raft 而非直接实现 Paxos。
下篇预告:
Raft 是共识算法领域的"现代标准",但它不是唯一的选择。在 Raft 之前,ZooKeeper 的 ZAB 协议就已经在 Hadoop 生态中大规模落地。ZAB 和 Raft 同为强 Leader 模型,但在选举策略和数据恢复上走了不同的路。
下一篇,我们将深入 ZAB 协议,看看它和 Raft 之间的殊途同归与关键差异。
思考题
- 为什么 Raft 要求每个节点在同一个 Term 内只能投一票?如果允许投多票会怎样?
参考答案
会导致同一任期内出现两个 Leader(脑裂)。
具体场景(5 节点):
- 节点 A 和 B 同时发起选举(Term=2)
- 如果允许投多票:节点 C 先投给 A,又投给 B
- A 获得 {A, C, D} 三票 → 当选 Leader
- B 获得 {B, C, E} 三票 → 也当选 Leader
- 同一任期两个 Leader,各自接受写请求,数据分裂
总结:每个 Term 只投一票,保证了多数派的排他性------同一任期内不可能有两组多数派分别选出不同的 Leader。这和 Paxos 中 Acceptor 承诺不再接受更小编号的道理一样。
- 如果 Leader 刚提交日志就宕机了,还没来得及通知任何 Follower 更新 commitIndex,这条日志会丢吗?
参考答案
不会丢。
- "提交"的含义:日志被多数派写入,不是 Leader 单方面决定的。Leader 宕机时,多数派节点上已经持久化了这条日志。
- 新 Leader 选举:由于选举限制,只有拥有最新日志的节点才能当选新 Leader。新 Leader 的日志中一定包含这条已提交的日志。
- 恢复过程:新 Leader 上任后,通过正常的日志复制流程,会把这条日志同步到所有节点,并最终更新 commitIndex。
总结:commitIndex 的通知丢了不要紧,只要日志本身已经被多数派持久化,它就不会丢。新 Leader 一定会"继承"并"重播"它。
- 为什么 Leader 不能直接提交旧任期的日志?只看"多数派已写入"不够吗?
参考答案
不够,因为"多数派写入"不能阻止更高 Term 的节点覆盖它。
核心问题:
- 旧任期的日志虽然被多数派写入了,但选举比较的是"最后一条日志的 Term"
- 另一个节点如果持有更高 Term 的日志(即使只有一条),在日志比较中它就更"新"
- 如果这个节点联合其他没有旧日志的节点形成多数派,它就能当选 Leader
- 当选后它会用自己的日志覆盖掉那些已被"提交"的旧 Term 日志
解法:Leader 只提交当前任期的日志。当前任期的日志被多数派写入后,持有它的节点在选举比较中一定胜出(Term 最大),从而阻止了没有这条日志的节点当选。旧任期的日志随着当前任期日志的提交被间接提交,安全性得到保证。
- 假设集群中有 5 个节点,Leader 发送了一条日志但只有 1 个 Follower 确认了(加上 Leader 自己是 2/5),此时 Leader 宕机。这条日志会被提交吗?
参考答案
不确定------取决于新 Leader 是谁。
- 如果新 Leader 有这条日志(即那个已确认的 Follower 当选):新 Leader 会继续复制这条日志,最终可能被提交。
- 如果新 Leader 没有这条日志(另外 3 个节点之一当选):这条日志会被新 Leader 的日志覆盖,永远不会被提交。
关键点:只有被多数派写入的日志才算"已提交"。2/5 没有达到多数派(需要 3/5),所以这条日志处于"未提交"状态,命运取决于后续选举结果。这不是 bug,而是 Raft 的设计保证:未被多数派确认的数据,本来就不应该被承诺给客户端。