Raft 深水区探秘_经典极端场景与 Raft 的完美解法
导语: 在前面的文章中, 我们看到 Raft 是如何在 Leader 宕机时力挽狂澜的. 但是, 现实世界往往比单纯的宕机要复杂得多: 网络可能会被切断一半、节点可能会不断重启、甚至会出现跨越好几个任期的"幽灵日志". 今天, 我们将走进 Raft 的"深水区", 看看 Raft 协议是如何通过严密的数学逻辑, 将这些足以摧毁系统的致命异常一一化解的.
一、 场景一: 网络分区与"任期怪兽"(幽灵节点)
这是分布式系统中最常见的灾难: 网络分区(Network Partition / 脑裂).
1. 灾难现场: 被孤立的节点疯狂自嗨
假设我们有一个 5 个节点的集群(A, B, C, D, E), A 是 Leader, 当前的 Term(任期)是 1. 突然, 机房网络交换机故障, 整个网络被劈成了两半:
- **大区: ** A, B, C 互相连通.
- **小区: ** D, E 互相连通.
**系统状态演变: **
- 大区继续工作: A 依然能和 B、C 通信, 3 个节点构成了多数派(Majority), 所以 A 继续当 Leader, 正常处理客户端的读写请求.
- 小区陷入疯狂: D 和 E 收不到 A 的心跳. D 的选举定时器超时, 变成了 Candidate, 将自己的 Term 加 1(变成 2), 向 E 拉票.
- 死循环发生: D 只能拿到自己和 E 的 2 张票, 达不到多数派(3 张). 于是 D 再次超时, Term 变成 3... 变成 4...
- 几个小时后, 网络故障排除了. 此时, D 的 Term 已经增加到了 10000.
2. 致命威胁: 王者归来还是搅局者?
网络恢复的瞬间, D(Term 10000)重新连上了大区. Raft 有一条铁律: "见大即怂". Leader A 收到 D 的消息, 一看对方的 Term 是 10000, 远大于自己的 Term 1, A 会立刻认为自己过时了, 直接退位变成 Follower!
这就导致了一个极其恶心的现象: 一个落后了几个小时、毫无用处的"幽灵节点", 仅仅因为被关了禁闭导致 Term 虚高, 一回来就干翻了稳定工作的 Leader, 迫使整个集群停顿并重新选举.
3. Raft 工程演进的解法: 预投票机制(Pre-Vote)
为了解决这个刺头, 后来的 Raft 工业实现(如 etcd)引入了论文中的一个关键优化: Pre-Vote(预投票)阶段.
有了 Pre-Vote, D 节点在超时后, 不能立刻增加自己的 Term. 它必须先发起一轮"模拟选举":
- D 问大家: "如果我参选, 你们会投给我吗?"(此时 D 的 Term 不变).
- 网络恢复后, D 向 A、B、C 发送 Pre-Vote.
- A、B、C 发现自己一直在正常接收 Leader 的心跳, 活得好好的, 于是果断拒绝 D.
- D 发现自己连模拟选举都拿不到多数票, 于是乖乖退回 Follower 状态, 悄悄通过 Leader 的
AppendEntries同步丢失的数据.
完美解决! 稳定运行的 Leader 得到了保护, 幽灵节点无法再扰乱朝纲.
二、 场景二: "幽灵复现"与跨任期提交陷阱(Raft 最难懂的细节)
如果说上一个场景是工程优化, 那这个场景就是 Raft 协议核心的理论底座. 它对应了 Raft 论文中最晦涩难懂的 Figure 8.
1. 灾难现场: 已提交的日志竟然被覆盖了?
请仔细看下面的状态推演图. 我们要弄清楚: Leader 能不能把前任遗留下来的日志, 通过"凑齐多数派"的方式直接标记为已提交?
图解: 幽灵复现的致命死局(注意看日志的 Term)(a) S1 是 Leader(Term 2). 它把日志写入了 S1 和 S2. 然后 S1 宕机.
S1: [Term 2]
S2: [Term 2]
S3: [](b) S5 被 S3, S4, S5 选为 Leader(Term 3). S5 自己写了一条 Term 3 的日志, 没来得及复制, S5 宕机.
S1: [Term 2]
S2: [Term 2]
S5: [Term 3](c) 关键时刻!S1 重新启动, 并被 S1, S2, S3 选为 Leader(Term 4)!
S1 发现本地还有一条没提交的 [Term 2] 日志, 于是把它复制给了 S3.
S1: [Term 2]
S2: [Term 2]
S3: [Term 2] <-- 此时 [Term 2] 已经在多数派节点上了!❓ 疑问: S1 现在能把 [Term 2] 的日志标记为 "Committed (已提交)" 吗?
2. 致命的逻辑漏洞
直觉上, 既然 [Term 2] 已经复制到了多数派(S1, S2, S3), 它应该被安全提交了, 对吧? 错!极其危险!
假设 S1 此时把 [Term 2] 提交了, 然后 S1 又双叒叕宕机了. 这时候, S5 重新启动了. 它可以凭借自己身上的 [Term 3] 日志去向 S2, S3, S4 拉票. 因为 S5 的日志 Term (3) 比 S2 和 S3 上的 [Term 2] 都要大! 于是 S5 会成功当选 Leader, 然后用自己身上那条 [Term 3] 的日志, 强制覆盖掉 S1, S2, S3 上的 [Term 2] 日志.
**灾难发生了: ** 一条已经被标记为"已提交"(并且可能已经返回给客户端成功)的日志, 竟然被覆盖删除了!这就违背了分布式系统最底线的 State Machine Safety(状态机安全)原则.
3. Raft 的完美解法: 绝不直接提交前任日志
为了封死这个漏洞, Raft 制定了一条非常违反直觉、但极其严密的铁律:
**Leader 绝对不能通过"计算副本数"的方式, 去提交之前任期的日志. ** **Leader 只能通过"计算副本数"来提交当前任期(Current Term)产生的日志. **
破局之道: ** 在刚才的图 (c) 中, S1 (Term 4) 虽然把 [Term 2] 复制到了多数派, 但它只能看着, 不能提交. S1 必须等. 等到有客户端发来一条新的请求, 产生了一条 [Term 4] 的日志. 只要这条 [Term 4] 的日志被复制到了多数派, 由于 Log Matching(日志匹配)特性的存在, [Term 4] 前面的所有日志(包括 [Term 2])就顺理成章地被一并提交了**.
这就彻底斩断了 S5 重新翻盘的可能. Raft 用这一条克制的规则, 堵死了分布式系统中隐藏最深的"幽灵复现"漏洞.
三、 场景三: Follower 闪断与 RPC 幂等性
相比于前两个惊心动魄的场景, 这个场景更偏向于日常的工程细节.
1. 灾难现场: Leader 的消息重发
Leader A 向 Follower B 发送了一条 AppendEntries(追加日志 x=1). Follower B 顺利收到, 将其落盘写入了本地, 然后向 Leader 发送 ACK. **但是, 在这个 ACK 飞在半空中的时候, Follower B 突然断网了 2 秒钟, 导致 ACK 丢包了. **
此时 Leader A 的超时定时器触发, 认为 B 没有收到日志, 于是 Leader A 毫不犹豫地重新发送了一模一样的 AppendEntries 给 B.
2. 致命威胁: 数据会重复写入吗?
等 B 网络恢复时, 它收到了 Leader 发来的那条它明明已经写过一次的请求. 如果在状态机里执行两次 x=1(或者转账 100 元执行两次), 业务数据就全乱了.
3. Raft 的完美解法: 严格的幂等性校验
Raft 的日志流设计天然具备幂等性(Idempotency). Follower B 收到重发的 AppendEntries 时, 会执行严格的校验逻辑:
- 提取请求中携带的日志条目.
- 检查本地日志的相同
Index处, 是否已经有一条Term完全一样的记录? - 发现本地已经有了!
- **Follower B 默默地丢弃这条重复的日志, 不进行任何覆盖, 但仍然向 Leader 热情地返回
Success ACK. **
这种设计使得 Leader 永远可以毫无顾忌地重试任何超时的 RPC 请求, 而不用担心把 Follower 的数据搞乱.
**结语: ** 从"Pre-Vote"对稳定性的保护, 到"只能提交当前任期日志"的严密推理, 再到"幂等性"工程设计. Raft 之所以伟大, 不在于它描绘了一个多么理想的系统, 而在于它预判了物理世界所有的肮脏与混乱, 并用几条极简的规则, 为数据的一致性筑起了一道不可逾越的高墙.
