文档目录

5.3 重复、抖动与异常值

上一节:5.2 预热、稳态与运行时长 | 下一节:5.4 环境隔离清单 配套代码:05-experiment-design/03-repetition-and-outliers


一句话结论

一次运行的结果不是结论,是一个样本。 你至少需要 3–5 轮重复才能看到分布。而异常值不能"觉得不好看就删"——必须能解释成因,否则它可能正是你需要面对的真实情况。


一、用「投篮命中率」理解重复

判断一个球员的命中率,你不会只看他投一次:

做法 问题
只投 1 次(进了) 可能是运气
只投 1 次(没进) 也可能是运气
投 100 次 开始有意义
投 100 次,但把没进的都算作"手滑了" 自欺

性能实验完全一样:

❌ 「跑了一次,P99 = 95ms,达标」
✅ 「跑了 5 次,P99 = 93, 96, 94, 210, 95 ms,中位数 95ms」
                                  ↑ 这一轮需要解释

二、该跑几轮

轮数 能做什么 局限
1 轮 只能发现「明显的崩溃」 无法区分差异与噪声
3 轮 能看到波动范围,取中位数 推荐的最低要求
5 轮 能做简单的统计检验(非参数检验需要足够样本) 时间成本
10 轮以上 能估算置信区间 只在关键决策时值得

权衡:3–5 轮是性价比最高的区间。如果环境噪声大(CV > 10%),增加轮数的收益不如先改善环境。

注意:这里的「轮」是指完整的独立实验(重启服务、重新预热、重新压测),而不是「压测过程中的多次采样」。压测过程中每 10 秒一个点的采样,是同一个实验内部的波动。


三、怎么报告「多轮结果」

❌ 常见但不合格的报告

「5 轮平均 P99 = 118 ms」

问题:平均值被异常轮拉动,而且掩盖了波动幅度。

✅ 合格的报告

| 轮次 | P50 | P95 | P99 | 错误率 | 备注 |
| --- | --- | --- | --- | --- | --- |
| 1 | 32 | 68 | 93 | 0.0% | |
| 2 | 33 | 70 | 96 | 0.0% | |
| 3 | 32 | 67 | 94 | 0.0% | |
| 4 | 41 | 142 | 210 | 0.3% | ⚠️ 期间有备份任务,见说明 |
| 5 | 33 | 69 | 95 | 0.0% | |

中位数(全部 5 轮):P50=33, P95=69, P99=95
剔除第 4 轮后:P50=32.5, P95=68.5, P99=94.5(几乎无差异)
结论:第 4 轮异常由外部因素造成,已定位原因;其余 4 轮稳定。

注意这份报告的三个特点:

  1. 逐轮列出,不是只给一个数。
  2. 给出了中位数,不是平均值。
  3. 异常轮被保留并解释,而不是删掉。

四、异常值的正确处理流程

发现异常值
    │
    ├─ ① 能解释成因吗?
    │      ├─ 能(如:备份任务、日志轮转、其他服务部署、机器负载)
    │      │     → 记录成因,可以剔除并重跑该轮
    │      │
    │      └─ 不能
    │            → ② 保留它,并把它当作重要线索
    │
    └─ ③ 判断它是「环境异常」还是「系统异常」
           ├─ 环境异常(如备份任务)→ 修复环境后重跑
           └─ 系统异常(如某种输入触发慢路径)→ 这是真实问题,必须追查

关键区分:环境异常 vs 系统异常

类型 例子 处理
环境异常 备份任务、日志轮转、邻居抢占、网络抖动 修环境,重跑
系统异常 ⭐ 某种数据分布触发了慢路径、缓存击穿、锁竞争爆发 这是真实问题,不能删!

最危险的错误:把「系统异常」当成「环境异常」删掉。

❌ 「这一轮 P99 突然变成 800ms,肯定是机器抽风,重跑一下吧。」
   (实际上:这一轮恰好触发了缓存击穿,是一个真实的缺陷)

怎么区分:看异常是否可复现。

重跑 3 次:
  - 异常不再出现 → 可能是环境异常
  - 异常稳定复现 → 系统异常(必须追查)
  - 间歇性出现(比如 5 次里出现 1 次)→ 可能是系统问题 + 特定条件,需要进一步定位

五、别被「平均值」和「最好的一次」骗到

做法 问题
报平均值 被异常值拉动;掩盖波动幅度
报最好的一次 Cherry-picking,最典型的自欺
报最差的一次 过度悲观(除非你要评估最坏情况)
报「平均 ± 标准差」 延迟分布非正态,标准差没有解释力(第 6 节)

推荐:报 中位数 + 四分位距(IQR),或直接给出所有轮次的值。

中位数 = 95 ms
四分位距(Q1–Q3)= 94 – 96 ms      ← 说明波动很小
最大值 = 210 ms(第 4 轮,已解释原因)

六、四个必须记录的信息

信息 为什么
每一轮的原始值 让别人能自己判断,而不是只看你的结论
中位数 / 四分位距 比平均值更能代表"典型情况"与波动
异常轮及其成因 保留证据,供后续参考
环境事件时间线 备份、部署、其他任务的时间点(用于解释异常)

建议:在压测脚本里自动记录环境事件:

# 记录实验期间的系统事件
(
  while true; do
    date +%s
    # 记录后台任务
    pgrep -a -f 'backup|logrotate|cron' || true
    # 记录关键资源
    cat /proc/loadavg
    sleep 10
  done
) > events.log &

事后把 events.log 与延迟曲线对齐,就能解释大多数异常。


七、本节小结

  1. 一次运行不是结论,是一个样本——至少需要 3–5 轮。
  2. 报告要逐轮列出 + 中位数 + 四分位距,不要只给平均值。
  3. 异常值必须能解释成因才能剔除,否则要保留——它可能是真实问题。
  4. 区分环境异常(备份任务等,可剔除重跑)和系统异常(真实缺陷,必须追查)。
  5. 判断方法:重跑看是否可复现。稳定复现 = 系统问题。
  6. 自动记录环境事件时间线,是解释异常的低成本手段。

八、自测

  1. 你跑了 5 轮,P99 分别是 95、96、94、210、95 ms。这一轮 210 ms 该怎么处理?
  2. 一个团队报「5 轮平均 P99 = 118 ms,达标」。请指出这份报告的两个问题,并给出改进后的报告方式。
  3. 你怀疑「这一轮异常是机器抽风」,重跑 3 次后发现异常出现了 1 次。请说明你接下来该做什么,以及为什么不能简单地把它算作环境问题。
  1. 先不要删,先查成因。步骤:① 检查环境事件时间线(是否有备份、日志轮转、其他部署、邻居抢占)——如果找到明确的外部原因,记录后可以剔除并重跑;② 检查服务端数据(这一轮期间的 GC 日志、火焰图、连接池 pending)——如果是 GC 停顿或连接池排队,那是系统异常;③ 如果找不到原因,保留这一轮,并在报告里注明「第 4 轮异常,成因待查」;④ 用它做进一步线索:这一轮的输入或时序条件与其余轮有什么不同?(例如是不是恰好触发了缓存失效、或者是某个定时任务)。关键原则:不要因为"不好看"就删。
  2. 两个问题:① 用平均值——会被异常轮拉动,而且掩盖波动幅度(如果 5 轮是 50/50/50/50/390,平均值 118 完全不能反映「通常 50ms,偶尔 390ms」这个事实);② 只给一个数字——读者无法判断稳定性、无法判断是否有异常轮、无法复现。改进后的报告:逐轮列出 P50/P95/P99 + 错误率;给出中位数与四分位距(或最小值/最大值);注明轮数、每轮的样本量、是否有异常轮及其成因;补充环境元数据(或指向 env.txt)与实验编号。
  3. 这 1 次异常是重要线索,不能简单归为环境问题。理由:① 「1/4 的概率出现」意味着它在特定条件下会复现——这不是随机噪声,而是一个条件触发的缺陷(例如某种数据分布、某个时序窗口、缓存失效瞬间);② 环境噪声通常表现为「幅度较小的抖动」,而 3 倍以上的 P99 跳变更可能指向系统行为(缓存击穿、GC、锁竞争、连接池排队、重试风暴)。接下来该做:① 采集异常轮的服务端数据(GC 日志、wall 火焰图、连接池指标、线程快照),与正常轮对比;② 找出异常轮与正常轮的输入差异(数据 ID 分布、时序、并发模式);③ 如果能找到触发条件,就有机会在生产复现并修复——这正是「异常值」最有价值的地方;④ 如果确实无法定位,就在报告里明确写「存在偶发 P99 尖刺(约 25% 概率),成因未明,风险已记录」——诚实比好看更重要。