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.shscript/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_usthroughput_msg_per_secavg_submit_usp50_submit_usp95_submit_usp99_submit_us
这些数字单看一轮结果其实意义有限,更重要的是它们随时间的走势。如果吞吐逐步回落、P99 持续抬升、提交延迟开始出现周期性尖峰,那通常说明系统内部已经有某种积压、抖动或者后台动作正在影响主写入路径。
也正因为如此,soak benchmark 的价值不在于制造一个“漂亮峰值”,而在于观察一条曲线是否稳得住。
test_soak_and_fault.sh 为什么更接近真实运行
相比长时间压测,test_soak_and_fault.sh 的风格更接近演练脚本。当前它把流程拆成多个 phase,典型包括:
- Timed soak loop
- Query server + status check
- Restart recovery after clean stop
- Broken active tail + recovery
- Bad checkpoint + recovery
- Stale checkpoint + recovery
- Broken gzip + query
- 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 不是一个装饰性接口。它直接参与了系统状态判断。
当前健康状态值包括:
okdegradedunhealthy
把 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 和故障场景反复跑过,才更接近回答另一个更现实的问题:这套日志引擎能不能在真实环境里稳定跑下去。