文档目录

3.6 破坏性测试:崩溃点与崩溃形态

上一节:3.5 浸泡测试 | 下一节:3.7 层级选择与成本 配套代码:03-test-pyramid/06-breakdown-testing


一句话结论

破坏性测试的价值不在于「知道多少 QPS 会垮」,而在于「知道它是怎么垮的」。 崩溃形态决定了故障的影响范围、恢复难度,以及半夜被叫起来时你该做什么。


一、用「碰撞试验」理解破坏性测试

汽车厂商会故意把车撞毁。这不是为了证明车安全,而是为了回答:

碰撞试验要回答的 对应后端
撞了之后哪个部位先变形? 哪类资源先饱和?
乘员舱能不能保住? 核心功能是否还可用(优雅降级)?
是吸能变形还是直接断裂? 是排队变慢,还是直接报错?
撞完还能不能开? 流量回落后能否自愈?
二次碰撞会怎样? 重试会不会造成二次打击?

注意第一个问题:碰撞试验不是只测「多少速度会撞坏」,而是测撞坏的整个过程。

后端也一样:「800 QPS 时崩」这个信息价值有限;「800 QPS 时连接池先满,然后所有接口一起超时,且流量回落后需要 4 分钟才恢复」才是真正有用的结论。


二、五种典型的崩溃形态

崩溃形态 现象 根因 影响范围
① 超时级联 上游超时返回,但对下游的调用还在继续,资源被占住 超时链未递减(第 1 章 1.5 节) 扩散到所有接口
② 重试风暴 系统越慢,客户端重试越多,负载越高 没有退避、抖动、重试预算 自我放大,可能击穿
③ 连接池雪崩 所有连接被慢请求占住,新请求全部排队超时 池太小 + 查询变慢 + 无超时 该服务的所有数据库操作
④ 无界队列 OOM 请求堆积到内存耗尽,进程被杀 队列无上限、无拒绝策略 整个进程
⑤ 限流缺失 一个慢依赖拖垮整个服务的所有接口 没有隔板(按依赖隔离资源池) 全站

怎么观察这五种形态

形态 要采集的数据
超时级联 各层超时配置 + 各依赖的调用数与耗时
重试风暴 客户端重试次数、服务端 QPS 与服务端收到的实际请求数
连接池雪崩 active/idle/pending/total 四条曲线 + 获取连接等待时间
无界队列 OOM 队列深度 + 堆使用曲线 + 拒绝/丢弃计数
限流缺失 各依赖的并发数分布(一个依赖是否占用了全部线程/连接)

三、「崩溃点」是次要的,「恢复能力」才是关键

关键问题

① 崩溃点:多少 QPS 开始不可用?
② 恢复时间:流量回落后,多久回到正常?
③ 能否自愈:不重启能不能恢复?
④ 是否残留:恢复后有没有留下副作用(缓存被污染、连接泄漏)?

② 和 ③ 经常被忽略,但它们决定了故障的严重程度。

情况 影响
崩溃点 1000 QPS,流量回落后 10 秒恢复 影响小,能自愈
崩溃点 1000 QPS,流量回落后 10 分钟恢复,且需要重启 影响大,半夜必须人工介入
崩溃点 1500 QPS,但一旦崩溃就再也起不来(连接池永久耗尽) 最严重:需要重启 + 可能丢数据

一个真实的自愈失败案例

① 数据库慢查询导致连接池被占满(每个连接都在等 SQL)
② 流量回落后,理论上连接应该释放
③ 但应用给数据库的超时是 30 秒(默认值,没人改)
④ 那些连接仍在等数据库(还要等 30 秒)
⑤ 新请求继续进来,继续占用连接
⑥ 连接池永远处于满状态 → 无法自愈 → 必须重启

这个案例的教训:超时配置直接决定了「能否自愈」。 一个 5 秒的超时能让系统自己恢复;一个 30 秒的超时可能让它永远卡住。


四、执行要点(破坏性测试有风险)

要点 说明
绝不在生产环境做 除非你明确知道后果且有回滚方案
设定明确的终止条件 例如「P99 > 1s 或错误率 > 5% 持续 30 秒」就停
确保能重启 提前验证重启流程;准备好重置数据的脚本
记录完整时间线 加压时刻、崩溃时刻、终止时刻、恢复时刻
采集服务端数据 崩溃瞬间的线程 dump、堆概况、连接池状态最有价值
一次只测一种失效 不要同时压垮数据库和消息队列,否则无法归因

崩溃瞬间该抓什么

PID=$(jcmd | grep app.jar | awk '{print $1}')

# 崩溃瞬间的线程状态(连续抓 3 份,间隔 2 秒)
jcmd "$PID" Thread.print > "crash-threads-$(date +%s).txt"

# 堆概况
jcmd "$PID" GC.heap_info > "crash-heap-$(date +%s).txt"

# 连接池与队列状态(从 /metrics 抓)
curl -s localhost:8080/metrics | grep -E "pool|queue|pending" > "crash-metrics-$(date +%s).txt"

这些数据在崩溃后基本无法复现——现场的线程栈是排查「为什么卡住」的直接证据。


五、从崩溃形态推导改进措施

崩溃形态 改进方向
超时级联 统一超时预算,逐层递减;加熔断
重试风暴 指数退避 + 随机抖动 + 重试次数上限 + 重试预算
连接池雪崩 先优化慢查询,再按 Little’s Law 定池大小;加获取连接超时
无界队列 OOM 有界队列 + 明确的拒绝策略(快速失败优于堆积)
限流缺失 隔板:为每个下游分配独立的连接池/信号量
无法自愈 检查超时配置——这是自愈能力的关键

六、本节小结

  1. 破坏性测试的核心产出是「崩溃形态」,不是「崩溃点」。
  2. 五种典型形态:超时级联、重试风暴、连接池雪崩、无界队列 OOM、限流缺失。
  3. 「恢复能力」比「崩溃点」更重要:多久恢复?能否自愈?有无残留?
  4. 超时配置直接决定能否自愈——30 秒的超时可能让系统永远卡住。
  5. 崩溃瞬间的线程 dump、堆概况、池状态是最有价值的证据,且无法事后复现。
  6. 破坏性测试有真实风险:不在生产做、设终止条件、确保能重启、一次只测一种失效。

七、自测

  1. 两个服务的崩溃点都是 1000 QPS。A 在流量回落后 15 秒恢复,B 需要重启才能恢复。请说明这两个结论对「容量规划」和「值班响应」分别意味着什么。
  2. 为什么「所有连接都在等数据库」的状态,在流量回落后可能无法自动恢复?根本原因是什么?
  3. 你在做破坏性测试时把服务压垮了。请按优先级列出你要立刻采集的三样数据,并说明每样数据能回答什么问题。
  1. 对容量规划:A 可以更激进地使用容量(因为有过载自愈能力,短时超载不会造成持久故障),但 B 必须留更大 headroom——因为它一旦进入故障状态就需要人工介入,代价高得多。对值班响应:A 的告警可以是「观察级」(等它自愈,或轻微干预);B 必须配「立即呼叫」级别的告警,并且 runbook 里要写清重启步骤。另外,B 的情况还意味着发布和扩缩容更危险(一旦触发就雪崩),需要更保守的金丝雀策略。
  2. 根本原因是超时配置过长:那些连接并不是「忙」,而是在阻塞等待数据库返回。流量虽然回落了,但数据库仍然是慢的,所以这些连接会一直等到超时(比如 30 秒)才释放;如果新请求持续到来(哪怕量小),它们会立刻占用释放出来的连接,池子始终处于满状态。关键:这不是「资源不够」,而是「资源被长时间占用且没有主动放弃机制」。修法是缩短超时(例如 3 秒),让请求快速失败并释放连接——快速失败换来的是系统整体的自愈能力。
  3. 优先级排序:① 线程 dump(连续 3 份,间隔 2 秒)——回答「线程都卡在哪里」:是停在锁上、停在 socket read、还是停在等待连接池?这是定位「为什么卡住」的直接证据,且崩溃后无法复现。② 服务端指标快照(连接池 active/idle/pending、队列深度、错误率)——回答「哪类资源先耗尽」,区分是连接池雪崩、线程池饥饿还是队列积压。③ 堆概况与 GC 状态——回答「是不是 OOM 或者 GC 崩溃」。如果时间允许,再加第四样:火焰图或 JFR 录制,用于事后分析时间花在哪。注意:这些都要在重启之前采集——一旦重启,现场就没了。