Soak Testing:长时间运行下该怎么验证日志引擎

结合 bench_soak.sh 与 test_soak_and_fault.sh,分析 seastar-log-engine 如何通过长时间写入、重启恢复、损坏注入和查询校验来覆盖真实运行风险。

为什么 benchmark 不够

很多存储或日志组件在发布前都会跑一轮 benchmark,但 benchmark 擅长回答的是“快不快”,不一定能回答“跑久了会不会出问题”。

seastar-log-engine 这类系统来说,真正棘手的风险往往出现在长时间运行之后:active log 会不会持续膨胀、rotate 之后状态还能不能接上、checkpoint 出问题时恢复能不能兜住、query path 会不会在 archive 和 active log 混合读取时露出边角问题。这些都不是单次压测能轻易看出来的。

所以项目里把 soak testing 单独做成了一类验证,而不是把它混进一次性的性能测试里。

两个脚本分别覆盖什么

目前这套验证主要围绕两个脚本展开:

  • script/bench_soak.sh
  • script/test_soak_and_fault.sh

bench_soak.sh 更偏长时间写入观测。它持续执行 benchmark,把吞吐、提交延迟和运行时统计拉成一条时间线,目的是看系统在持续压力下会不会漂移。

test_soak_and_fault.sh 则更像一组长流程场景测试。它把运行、停机、恢复、损坏注入、查询校验串起来,覆盖的不是某一个函数,而是“系统连续经历多种状态切换之后是否还能自洽”。

bench_soak.sh 更关注趋势,不只是一组数字

这类 soak benchmark 通常会带上这些关键参数:

  • --target
  • --duration-seconds
  • --messages
  • --payload-size
  • --batch-size
  • --inflight
  • --shards
  • --ack-mode
  • --flush-ms
  • --checkpoint-enabled

输出里会重点记录:

  • elapsed_us
  • throughput_msg_per_sec
  • avg_submit_us
  • p50_submit_us
  • p95_submit_us
  • p99_submit_us

这些数字单看一轮结果其实意义有限,更重要的是它们随时间的走势。如果吞吐逐步回落、P99 持续抬升、提交延迟开始出现周期性尖峰,那通常说明系统内部已经有某种积压、抖动或者后台动作正在影响主写入路径。

也正因为如此,soak benchmark 的价值不在于制造一个“漂亮峰值”,而在于观察一条曲线是否稳得住。

test_soak_and_fault.sh 为什么更接近真实运行

相比长时间压测,test_soak_and_fault.sh 的风格更接近演练脚本。当前它把流程拆成多个 phase,典型包括:

Rendering diagram...
  1. Timed soak loop
  2. Query server + status check
  3. Restart recovery after clean stop
  4. Broken active tail + recovery
  5. Bad checkpoint + recovery
  6. Stale checkpoint + recovery
  7. Broken gzip + query
  8. Multi-shard recovery consistency

这个编排方式很有意思。它不是单独验证"恢复逻辑对不对",也不是单独验证"查询对不对",而是让这些路径在同一套长流程里连续发生。这样更容易暴露出系统边界处的问题,例如:

  • clean stop 之后 checkpoint 是否真的可用
  • active tail 损坏后 verified scan 能否接管
  • archive 出问题时 query 是否会如实暴露降级状态
  • 多 shard 情况下恢复边界是否仍然一致

查询校验在 soak 测试里很重要

项目在 soak 测试里并没有把 query 当成可有可无的附带功能,而是显式校验:

  • HTTP /v1/status
  • HTTP /v1/records
  • gRPC GetStatus
  • gRPC QueryRecords

这一步之所以重要,是因为很多恢复问题并不会直接表现成进程起不来,而是表现成“系统能跑,但查出来的数据不对”。如果只看写入成功率,很容易把这类问题漏掉。

同时,这里也顺带覆盖了一个事实:当前 query API 是 unary 形式,而不是 streaming。测试脚本围绕现有接口形态去做校验,本身也说明了项目对查询能力边界的定义是比较明确的。

health 状态为什么要放进测试闭环

在这套测试里,/v1/status 不是一个装饰性接口。它直接参与了系统状态判断。

当前健康状态值包括:

  • ok
  • degraded
  • unhealthy

把 health 放进 soak 测试闭环的意义在于,很多故障场景并不会一律表现成彻底失败。例如 broken gzip archive 可能不会影响所有写入,但它会让系统显式进入降级状态;checkpoint 失效但 verified scan 兜住恢复时,外部也能看到系统经历过异常恢复路径。

这类状态反馈,往往比“脚本最后退出码是不是 0”更有信息量。

损坏注入的价值,在于验证兜底路径真的可用

test_soak_and_fault.sh 覆盖的故障类型很有代表性:

  • broken tail
  • bad checkpoint
  • stale checkpoint
  • broken gzip archive

这些场景的共同点是,它们都不是理想路径上的行为,而是真实运行里迟早会遇到的边界条件。做损坏注入的意义,不是证明系统永远不会坏,而是确认坏了之后还能否按设计进入 fallback。

从当前实现看,这些兜底路径大体是明确的:

  • active tail 出问题时,依赖 verified scan 找回有效边界
  • checkpoint 不可信时,回退到 active log 恢复结果
  • archive 损坏时,查询层需要把问题反映到状态面上

如果 soak 测试不把这些链路真的跑起来,恢复设计写得再漂亮,最后也只是纸面方案。

多 shard 场景为什么不能省略

单 shard 下很多问题会被天然简化,但项目本身的目标之一就是围绕 shard 局部性组织写入。因此 soak 测试把 multi-shard recovery consistency 单独拿出来,是很有必要的。

一旦写入、rotate、checkpoint 和恢复都分散到多个 shard 上,系统就不再只是“单文件是否干净”这么简单,而是“每个 shard 是否都恢复到自己那条连续的日志边界上”。如果这一步不稳,route key 对应的数据分布就可能悄悄发生偏移。

所以多 shard soak 不是附加项,而是这类架构必须正面覆盖的验证维度。

小结

seastar-log-engine 的 soak testing 更像一次长期运行演练,而不是简单把 benchmark 拉长时间。它关心的核心问题是:系统在持续写入、状态切换、异常恢复和查询校验交织出现时,是否还能保持一致的行为。

从工程角度看,这类测试的价值通常比一次峰值数字更高。吞吐高峰可以说明实现有潜力,但只有 soak 和故障场景反复跑过,才更接近回答另一个更现实的问题:这套日志引擎能不能在真实环境里稳定跑下去。