7×24 长稳测试:让系统在时间里暴露问题

功能测试验证"能不能用",长稳(soak)测试验证"能不能一直用"——内存泄漏、连接池耗尽、句柄泄漏、缓存膨胀,这些 bug 的触发条件是时间,跑 30 分钟的回归永远抓不到。2026 年把 7×24 长稳做成了常态化基建,这篇讲设计。

一、长稳 ≠ “把回归跑 24 小时”

最常见的误用:把回归套件循环跑 24 小时,看最后有没有挂。这只能抓到"累计执行量"类问题,抓不到状态漂移类问题——长稳的核心观察对象是系统随时间的指标曲线,不是用例通过率:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# soak/plan.yaml
name: customer-service-soak
duration: 72h
target: staging-full
traffic: # 流量模型:按线上比例混合
- profile: chat_mixed # 会话 + 消息 + 搜索(见 Locust 那篇)
weight: 70
- profile: ws_event_stream # 长连接事件流(Mock WS 服务供帧)
weight: 20
- profile: heavy_upload # 大文件/富媒体(每 4h 一波)
weight: 10
observe: # 观察项:这才是长稳的主体
- memory_heap: { threshold: "growth > 300MB / 12h" }
- memory_rss: { threshold: "abs > 8GB" }
- goroutine_fd: { threshold: "growth > 500 / 12h" }
- p99_latency: { threshold: "drift > 50% vs baseline" }
- ws_reconnect: { threshold: "rate > 0.1%/min" }
- disk_logs: { threshold: "abs > 50GB" }

用例只是背景流量,让系统"活着干活";观察项的阈值告警才是长稳的产出。

二、三个必须预埋的探针

长稳的"能看见"靠三个探针,全部在长稳开始前部署:

1. 资源采样器(每 60s)

进程级(heap/RSS/fd/线程数)+ 依赖级(DB 连接池占用、Redis 内存、消息队列深度)+ 系统级(CPU/IO/磁盘)。基线是第 0~2 小时的稳态值,不是历史值——同一套系统换个部署配置,历史基线就没意义。

2. 泄漏判定:看"斜率"不看"绝对值"

1
2
3
4
5
6
7
8
9
def leak_verdict(samples, window_hours=12):
"""
内存/句柄曲线做线性回归:
- 斜率 ≈ 0:平稳(GC 正常回收)
- 斜率 > 阈值:泄漏(持续只涨不跌)
- 锯齿但峰值下移:健康(周期性回收在变快,偶发正常)
"""
slope = linear_regression_slope(samples[-window_hours*60:])
return "leak" if slope > LEAK_SLOPE_THRESHOLD else "ok"

锯齿曲线要单独处理:Java/Go 的 GC 曲线天然锯齿,直接对"当前值"设阈值会天天误报;对峰值序列做回归(峰值也在涨 = 真泄漏)才是对的判定。

3. 功能哨兵(每小时)

长稳期间系统可能在"资源还健康"之前就已经功能悄悄坏了(如 WS 消息延迟从 200ms 漂到 3s,资源曲线看不出)。每小时跑一组 5 分钟的功能哨兵用例(登录/发消息/下单核心路径),哨兵失败 = 立即终止长稳并保留现场,比 72 小时后才发现"中间 40 小时都是坏的"便宜得多。

三、现场保留:长稳的价值 80% 在崩溃后

长稳崩了(或判定泄漏)的那一刻,现场数据比什么都重要,必须预埋自动保留

1
2
3
4
5
6
7
def on_soak_alert(alert):
# 1) 抓进程现场:heap dump(Java)/ py-spy dump(Python)/ fd 全表
# 2) 抓依赖现场:DB slow log 最近 1h / 连接池快照 / MQ 深度
# 3) 抓流量现场:最近 10min 的流量回放包(复现用)
# 4) 全量打包 var/soak/<run_id>/crash/ 并推送告警群(附一句话判定)
capture_all(alert.run_id)
notify(f"长稳 {alert.metric} 异常,现场已保留:{url}")

流量回放包是被低估的宝贝:泄漏/崩溃的复现往往需要"特定流量序列",保留最近 10 分钟的完整流量,本地重放就能在 30 分钟内复现,不用等下次长稳(72 小时后)。

四、常态化运行:成本与节奏

策略
频率 每周一次 72h(覆盖一个完整的工作周期 + 周末流量形态)
环境 staging 独立实例(不占生产、不和功能测试互扰)
成本 一台 8C16G + 一台流量发生器,月成本约 = 半个人日
终止条件 观察项告警 / 功能哨兵失败 / 72h 自然结束
产出 每周一份长稳周报(斜率曲线 + 哨兵结果 + 告警明细),进发版评审

关键纪律:长稳告警必须有人认领。第一次长稳揪出泄漏后,修复验证要再跑一次定向短长稳(12h,放大该流量维度),确认斜率归零才算闭环——"修了"和"修好了"在泄漏问题上是两回事。

五、实测战绩

落地三个月抓到三个功能测试完全无感的问题:

  1. WS 连接句柄泄漏:长连接重连后旧 fd 不释放,72h 后 fd 增长 8000+,线上表现为"服务跑两周后偶发断连"——正是用户工单里那个"时好时坏";
  2. 日志异步队列膨胀:日志级别配置漂移导致异步队列只进不出,内存 6h 涨 2GB,锯齿回归判定精准锁定;
  3. 搜索缓存雪崩:缓存过期策略在"凌晨低流量 + 批量预热"组合下触发,只有 72h 覆盖凌晨+早高峰的完整长稳能撞上。

六、小结

长稳测试的三个支柱:流量是背景,指标斜率是主体(看回归不看绝对值)、功能哨兵保功能真相(资源健康 ≠ 功能健康)、现场自动保留(崩溃那一刻的数据决定问题能否复现)。长稳不是"跑更久的回归",是用时间换 bug 维度——功能、性能、泄漏,各测各的,谁也不能代替谁。