故障注入:为什么要故意把日志引擎推到坏状态里

结合 test_soak_and_fault.sh 中的几个典型场景,分析 seastar-log-engine 如何通过故障注入验证恢复、查询和健康状态是否真的符合设计。

可靠性问题不能只在理想路径上验证

日志引擎最容易写出"正常情况下可以跑"的实现,最难写的是"出事之后还讲得通"的那部分。很多恢复设计在文档里都很完整,但只要没有真的把 active log、checkpoint 或 archive 文件弄坏过,系统到底会怎么反应,其实并不确定。

这也是故障注入测试存在的原因。它不是为了制造戏剧性场面,而是把那些迟早会遇到的坏状态提前拉到开发阶段,让系统在真正上线前就把兜底路径走一遍。

当前故障注入主要覆盖哪些问题

从现有脚本看,项目重点覆盖的是几类非常典型的损坏场景:

Rendering diagram...
  • active log 尾部损坏
  • checkpoint 内容损坏
  • checkpoint 过旧
  • gzip archive 损坏
  • 多 shard 恢复一致性

这几类问题组合在一起,已经能覆盖日志引擎最关键的几个边界:写入尾部、恢复入口、归档读取和多 shard 状态收敛。

脚本入口为什么放在 soak 流程里

当前故障注入并不是孤立的单元测试,而是被放进 script/test_soak_and_fault.sh 这类长流程脚本里。这样做的好处是,系统不是在一个静态初始状态下被"硬砸一下",而是在经历过实际写入、查询、停机和重启之后再进入损坏场景。

Rendering diagram...

这更接近真实运行环境。因为很多问题本来就不是冷启动时产生的,而是在一轮真实写入和状态切换之后才变得有意义。

broken tail 验证的是 active log 最后的可信边界

active log 尾部损坏是最直接的一类测试。它会逼着系统回答一个问题:当前文件最后那一段到底还能不能信。

这时真正起作用的,不是某个乐观假设,而是恢复流程里的 verified scan。只有当系统能在损坏尾部之后重新找回一个可信 valid_size,并把 writer 状态收敛到那里,broken tail 这类场景才算真的被处理掉。

换句话说,故障注入在这里验证的不是"能否检测出坏了",而是"坏了之后能不能回到一个可继续写的边界"。

bad checkpoint 和 stale checkpoint 覆盖了两种不同风险

checkpoint 出问题并不只有一种形式。

一种是文件本身不完整,或者内容已经损坏;另一种是文件看起来完整,但记录的 logical_size 已经落后于 active log 当前状态,也就是 stale checkpoint。

这两类场景之所以值得分开测,是因为它们考验的是同一条恢复链路的不同环节:

  • 系统能否识别 checkpoint 不再可信
  • 一旦不可信,是否会回退到 active log verified scan 的结果

如果只测"checkpoint 不存在",很多边界其实是覆盖不到的。真正难处理的,恰恰是那些"看起来像真的,但其实不该继续相信"的状态。

broken gzip 针对的是读路径而不只是恢复路径

archive 文件损坏后,系统不一定马上写不进去,但查询行为会受到影响。因此 broken gzip 这类测试的重点,不只是看 reader 会不会报错,还要看 query 层和状态层如何反映这个问题。

当前实现里,这类问题会体现在读路径相关指标和状态结果上,例如 gzip 读取错误、损坏 segment 计数,以及对外 health 的降级表现。

这说明故障注入并不是只围绕写路径设计的。对日志引擎来说,读路径一样是可信性的一部分,尤其是当 active 和 archive 会被一起查询时更是如此。

health 状态为什么必须进入故障测试闭环

故障注入测试里还有一个很重要的观察点,就是 /v1/status 这类状态接口到底会给出什么结论。

当前实现使用的健康状态值是:

  • ok
  • degraded
  • unhealthy

把 health 一起纳入测试非常重要,因为很多场景并不会表现成彻底不可用。比如 archive 出现局部问题,系统可能仍然能继续写;checkpoint 出错但 verified scan 成功兜底时,恢复也可能继续完成。这个时候更合理的行为,往往不是直接崩掉,而是进入可见的降级状态。

如果不测试 health,只看脚本是否退出成功,很容易把这类"功能没死,但语义已经变了"的问题漏过去。

多 shard 故障注入最能暴露边界问题

单 shard 下的损坏和恢复已经不简单,但多 shard 情况会把问题进一步放大。因为系统必须确认的不再是一条日志边界,而是每个 shard 是否都回到了各自正确的恢复位置。

这也是为什么 fault injection 会和 multi-shard consistency 一起出现。它验证的不是一个文件,而是一套分散状态在异常之后是否还能重新对齐。

对 Seastar 风格的日志引擎来说,这一步尤其重要。因为 shard 局部性本来就是设计基础,一旦恢复阶段破坏了这种局部一致性,前面所有写路径优化都会开始失去意义。

故障注入的真正价值

故障注入最有价值的地方,不是证明系统永远不会坏,而是证明系统在坏的时候会按设计坏下去。

seastar-log-engine 来说,这意味着几件事:

  • 损坏尾部时,恢复边界可重新确认
  • checkpoint 不可信时,fallback 路径真的会接管
  • archive 损坏时,查询层和状态层能如实暴露问题
  • 多 shard 状态在恢复后仍然保持可解释的一致性

从技术博客的角度看,这比"又多跑了一轮测试"更值得关注。因为真正决定日志引擎成熟度的,往往不是理想路径跑得多快,而是坏路径是否还能被系统自己接住。