功能测试验证"能不能用",长稳(soak)测试验证"能不能一直用"——内存泄漏、连接池耗尽、句柄泄漏、缓存膨胀,这些 bug 的触发条件是时间,跑 30 分钟的回归永远抓不到。2026 年把 7×24 长稳做成了常态化基建,这篇讲设计。
一、长稳 ≠ “把回归跑 24 小时”
最常见的误用:把回归套件循环跑 24 小时,看最后有没有挂。这只能抓到"累计执行量"类问题,抓不到状态漂移类问题——长稳的核心观察对象是系统随时间的指标曲线,不是用例通过率:
1 | # soak/plan.yaml |
用例只是背景流量,让系统"活着干活";观察项的阈值告警才是长稳的产出。
二、三个必须预埋的探针
长稳的"能看见"靠三个探针,全部在长稳开始前部署:
1. 资源采样器(每 60s)
进程级(heap/RSS/fd/线程数)+ 依赖级(DB 连接池占用、Redis 内存、消息队列深度)+ 系统级(CPU/IO/磁盘)。基线是第 0~2 小时的稳态值,不是历史值——同一套系统换个部署配置,历史基线就没意义。
2. 泄漏判定:看"斜率"不看"绝对值"
1 | def leak_verdict(samples, window_hours=12): |
锯齿曲线要单独处理:Java/Go 的 GC 曲线天然锯齿,直接对"当前值"设阈值会天天误报;对峰值序列做回归(峰值也在涨 = 真泄漏)才是对的判定。
3. 功能哨兵(每小时)
长稳期间系统可能在"资源还健康"之前就已经功能悄悄坏了(如 WS 消息延迟从 200ms 漂到 3s,资源曲线看不出)。每小时跑一组 5 分钟的功能哨兵用例(登录/发消息/下单核心路径),哨兵失败 = 立即终止长稳并保留现场,比 72 小时后才发现"中间 40 小时都是坏的"便宜得多。
三、现场保留:长稳的价值 80% 在崩溃后
长稳崩了(或判定泄漏)的那一刻,现场数据比什么都重要,必须预埋自动保留:
1 | def on_soak_alert(alert): |
流量回放包是被低估的宝贝:泄漏/崩溃的复现往往需要"特定流量序列",保留最近 10 分钟的完整流量,本地重放就能在 30 分钟内复现,不用等下次长稳(72 小时后)。
四、常态化运行:成本与节奏
| 项 | 策略 |
|---|---|
| 频率 | 每周一次 72h(覆盖一个完整的工作周期 + 周末流量形态) |
| 环境 | staging 独立实例(不占生产、不和功能测试互扰) |
| 成本 | 一台 8C16G + 一台流量发生器,月成本约 = 半个人日 |
| 终止条件 | 观察项告警 / 功能哨兵失败 / 72h 自然结束 |
| 产出 | 每周一份长稳周报(斜率曲线 + 哨兵结果 + 告警明细),进发版评审 |
关键纪律:长稳告警必须有人认领。第一次长稳揪出泄漏后,修复验证要再跑一次定向短长稳(12h,放大该流量维度),确认斜率归零才算闭环——"修了"和"修好了"在泄漏问题上是两回事。
五、实测战绩
落地三个月抓到三个功能测试完全无感的问题:
- WS 连接句柄泄漏:长连接重连后旧 fd 不释放,72h 后 fd 增长 8000+,线上表现为"服务跑两周后偶发断连"——正是用户工单里那个"时好时坏";
- 日志异步队列膨胀:日志级别配置漂移导致异步队列只进不出,内存 6h 涨 2GB,锯齿回归判定精准锁定;
- 搜索缓存雪崩:缓存过期策略在"凌晨低流量 + 批量预热"组合下触发,只有 72h 覆盖凌晨+早高峰的完整长稳能撞上。
六、小结
长稳测试的三个支柱:流量是背景,指标斜率是主体(看回归不看绝对值)、功能哨兵保功能真相(资源健康 ≠ 功能健康)、现场自动保留(崩溃那一刻的数据决定问题能否复现)。长稳不是"跑更久的回归",是用时间换 bug 维度——功能、性能、泄漏,各测各的,谁也不能代替谁。