Raft 故障切换排障(下):Leader 已选出,为何仍不可用

有一类故障切换很反直觉:日志显示新节点已经获得多数票,become leader 也出现了,但业务请求仍在一段时间内返回 NotLeader 或"节点未就绪"。继续优化选举参数没有效果,因为选举早已结束。

问题通常出在"协议上的 Leader"和"状态机可提供服务"之间。

Raft的三大index

Raft 节点至少有三条容易混淆的进度线:

指标 含义
last_log_index 本地 Raft Log 已写入的最大 Index
commit_index 已被多数派确认提交的最大 Index
last_applied_index 已经应用到本地状态机的最大 Index

它们满足:

text 复制代码
last_applied_index <= commit_index <= last_log_index

commit 只说明多数节点已经对日志达成共识,并不代表每个节点的业务状态已经更新。两者的差值就是常说的 Apply Backlog:

text 复制代码
apply_backlog = commit_index - last_applied_index

如果新 Leader 的 last_log_index 已经追平,而 last_applied_index 仍落后很多,它在协议层面有资格参选,状态机却还要补课。

Leader Transfer 检查了什么

braft 的公开文档描述了 Leader Transfer 的主要过程:旧 Leader 暂停接收新写入,等待目标 Follower 的日志追上,然后发送 TimeoutNow,让目标节点立即进入选举。

公开实现中的核心判断也围绕日志复制进度:目标节点尚未追到指定的 Log Index,就继续等待;追上后,才发送 TimeoutNow。用伪代码可以表示为:

text 复制代码
if target.log_index < required_log_index:
    wait_for_replication()
else:
    send_timeout_now()

这个检查回答的是"目标节点有没有缺 Raft Log",不是"目标状态机是否已经 Apply 完这些日志"。所以一次 Leader Transfer 可以在复制层面完全成功,同时留下很大的 Apply Backlog。

为什么 Backlog 会拖住 Leader 就绪

braft 的状态机回调按顺序执行。官方文档明确说明,on_applyon_snapshot_*on_leader_start 等接口不需要自行保证线程安全,因为框架会串行调用;代价是一个耗时回调会挡住后续回调。

FSMCaller 使用执行队列消费这些事件。其公开实现会合并连续的 COMMITTED 事件并调用 do_committed。当队列里已有待处理的提交事件时,框架会先推进状态机,再处理排在后面的 LEADER_START

一个常见时间线是:

text 复制代码
T+0        发起 Leader Transfer
T+很短     目标节点的 Raft Log 追平 (commit idx)
T+很短     目标节点获得多数票,成为 Leader
T+...      FSMCaller 顺序处理已提交但未 Apply 的日志
T+较久     Apply Backlog 清空,on_leader_start 得到执行
T+较久     业务层将节点标记为 Ready (apply idx)

如果业务把 on_leader_start 作为设置 Ready 的条件,become leader 与 Ready 之间就会出现明显空档。这不是第二次选举,也不意味着日志复制失败。它是状态机消费速度落后造成的排队。

Backlog 从何而来

最本质的原因是提交速度持续高于 Apply 速度。

text 复制代码
backlog 增长速度 = commit_rate - apply_rate

下面几种情况经常同时出现:

  • 节点重启后需要回放一段已提交日志;
  • 多个 Raft Group 在同一节点上高强度写入;
  • 状态机的单批处理包含解码、校验和存储引擎写入,单条成本偏高;
  • Snapshot、压缩或后台刷盘与 Apply 争抢同一块磁盘;
  • 存储介质出现长尾延迟,平均吞吐看起来正常,P99 已经恶化;
  • 状态机回调里执行了阻塞操作,拖住同一执行队列上的后续事件。

只看 CPU 利用率很容易误判。Apply 可能受磁盘带宽、队列深度、写放大或单线程顺序约束限制,CPU 仍然不高。

日志证明

1. 同时采集三条 Index

对旧 Leader、目标 Follower 和新 Leader,按固定周期记录:

text 复制代码
last_log_index
commit_index
last_applied_index

通过日志分析发现选举前目标节点的 last_log_index 接近旧 Leader,但 last_applied_index 明显落后,基本已经抓到根因。

2. 计算两个 Gap

text 复制代码
replication_gap = leader.last_log_index - target.last_log_index
apply_gap       = target.commit_index - target.last_applied_index

Leader Transfer 只盯 replication_gap 会漏掉第二个问题。排障面板应把两者并排展示。

3. 时间对齐

text 复制代码
transfer requested
become leader
on_leader_start
first successful request

become leader -> on_leader_start 很长,同时 last_applied_index 持续前进,说明节点不是卡死,而是在追平 Backlog。

4. 检查资源竞争

把 Apply 吞吐和延迟与以下指标放在同一时间轴:

  • 磁盘吞吐、延迟和队列深度;
  • Snapshot 保存或加载状态;
  • 后台压缩、刷盘和检查点任务;
  • 每个 Raft Group 的 Apply Gap;
  • FSM 当前任务及单批 Apply 耗时。

如果 Snapshot 开始后 Apply 吞吐下降,或者多个 Group 同时回放时磁盘队列升高,因果关系会比一段错误日志清楚得多。

多层修复

目标节点准入

执行 Leader Transfer 前,不只检查复制进度,还要检查目标节点的状态机进度和资源状态。一个实用的准入条件可以写成:

text 复制代码
replication_gap <= replication_threshold
apply_gap       <= apply_threshold
snapshot_state  == idle
disk_pressure   <= pressure_threshold

阈值应根据业务允许的切换停顿和实测 Apply 吞吐计算,不能简单照搬其他集群。若目标不满足条件,应换一个节点或等待,而不是强行迁移。

就绪语义

把"Raft 角色是 Leader"和"可以承接业务流量"拆成两个状态。前者供协议诊断,后者供负载均衡和客户端路由。

对于线性一致性读,常见做法是先取得安全的 Read Index,再等待本地 last_applied_index 追到该位置后返回。写请求可以在库和业务定义的安全条件满足后进入提交流程,但客户端成功响应仍应以提交并应用完成为准。

Leader Transfer 命令返回成功,也只表示迁移动作已被接受或协议流程完成到某个阶段。运维系统还需要等待业务 Ready,而不是把命令返回当成流量切换完成。

Apply 吞吐

优化前先确认瓶颈在哪:

  • 状态机支持批量写时,扩大合理批次,减少每条日志的固定开销;
  • 避免在 on_apply 中执行不受控的网络请求或长时间锁等待;
  • 不同 Raft Group 可以并行,但单个 Group 内仍要保持日志顺序;
  • 对 Snapshot 做限速,避免它吞掉 Apply 所需的磁盘带宽;
  • 条件允许时,将 Raft WAL、状态机数据和 Snapshot 的 I/O 路径隔离;
  • 为 Apply Gap、单批耗时和追平预计时间设置告警。

不要只追求更大的批次。批次过大也会增加单次回调延迟,让 LEADER_START 等事件等待更久。吞吐和事件响应时间需要一起测。

测试完善

故障切换测试不应依赖固定 sleep。机器快时浪费时间,机器慢时制造偶发失败。更可靠的断言是等待一组可观察条件:

text 复制代码
新 Leader 已产生
新 Leader 已进入业务 Ready
apply_gap 低于阈值
一次读写探测成功

测试还应主动覆盖慢路径:节点重启后立即迁主、写入高峰中迁主、Snapshot 期间迁主、磁盘延迟注入以及多个 Group 同时追赶。只有正常路径的切主测试,很难发现 Apply Backlog。

小结

Raft 的 replicatecommitapply 是三段不同的进度。日志追平只说明目标节点具备一致的 Raft Log,不代表本地状态机已经追平。

故障切换的完整准入条件应同时看复制进度和 Apply 进度;完整完成条件则应落到业务 Ready 和真实请求成功。把这两个边界补上,become leader 后仍然不可用的问题才会从偶发故障变成可观测、可解释、可控制的工程状态。

参考资料

相关推荐
CodeStats6 天前
《源纹天书》第一百九十一章至第一百九十五章:三界路由网络的部署、压力测试的模拟、混沌巨兽的苏醒、三界联合防御、背压的极限挑战!
java·raft·源纹天书
Solis程序员1 个月前
Raft:分布式系统的定海神针
java·分布式·kafka·rabbitmq·agent·raft
飞将2 个月前
从零实现Raft协议
raft
.魚肉2 个月前
Raft 共识算法 · 演示系统(多终端)
算法·go·raft·分布式系统
weisian1513 个月前
Java并发编程--45-分布式一致性协议入门:Raft、Paxos与ZAB的核心思想
java·分布式·raft·paxos·zab
源代码•宸6 个月前
分布式理论基础——Raft算法
经验分享·分布式·后端·算法·golang·集群·raft
中间件XL8 个月前
jraft原理源码分析(一)-架构,启动和初始化
分布式·raft·原理源码分析·jarft
踏浪无痕8 个月前
准备手写Simple Raft(二): 跑通最基本的Leader选举
后端·raft
踏浪无痕8 个月前
准备手写Simple Raft(三) 日志复制——一致性检查
后端·raft