文档目录

5.9 Lab 5:量化你自己的噪声底线

上一节:5.8 实验记录与常见反模式 | 下一节:第 6 章 分析与定位 配套代码:05-experiment-design/09-lab5 预计时长:90 分钟


一、这个 Lab 的目标

产出你自己环境的噪声底线,并用它验证统计方法。

做完之后你会得到一个数字——低于这个数字的"优化",你以后就不该声称。这个数字是你后续所有实验的标尺。


二、任务清单

# 任务 产出 时长
1 冻结环境(按第 4 节清单) 环境检查记录 20 min
2 跑 10 轮相同配置实验 10 组原始数据 40 min
3 计算噪声底线与 MDD 噪声底线表 15 min
4 显著性验证(5% 与 30% 两个改动) 两次判定结论 15 min

三、任务 1:冻结环境

按 第 4 节 的清单逐项确认,并记录:

## 环境检查记录

- CPU governor: __________
- 绑核配置: __________
- 容器 CPU limit: __________
- 容器内存 limit: __________
- 节流比例(实验前): __________
- 压测客户端位置: 独立机器 / 独立容器 / 同机(⚠️)
- 后台任务检查:
  - [ ] 已关闭备份
  - [ ] 已关闭日志轮转(或临时调大阈值)
  - [ ] 数据库 autovacuum 状态已知
- JVM 参数: __________
- 观测配置(JFR/profiler): __________
- `st`(steal)采样值: __________

关键要求:实验期间(10 轮)不做任何改动——包括不调参数、不部署、不打开 IDE。


四、任务 2:跑 10 轮

每轮必须做的事(缺一项数据就不可比):

for i in $(seq 1 10); do
  echo "═══ 第 $i 轮 ═══"

  # ① 重启服务(确保从同一初始状态开始)
  scripts/restart-app.sh "docs/experiments/E05-noise/results/gc-$i.log"
  sleep 5

  # ② 独立预热(不计入统计)
  BASE_URL=http://127.0.0.1:8080 k6 run --quiet --vus 20 --duration 60s \
    loadtest/profile-constant.js > /dev/null

  # ③ 正式测量(同样的负载、同样的时长)
  BASE_URL=http://127.0.0.1:8080 RATE=500 DURATION=5m \
  k6 run --summary-export="docs/experiments/E05-noise/results/round-$i.json" \
         loadtest/profile-constant.js | tail -20
done

为什么每轮都要重启:如果不重启,上一轮的热状态(JIT、缓存、连接池、GC 代际分布)会影响下一轮——那测的不是噪声,是"热状态的累积"。

记录每轮的值:

轮次 QPS P50 P95 P99 错误率 备注
1
…
10

五、任务 3:计算噪声底线与 MDD

用配套代码里的脚本(或自己算):

python3 tools/noise_floor.py docs/experiments/E05-noise/results/

输出应该包含:

指标   中位数   最小值   最大值   波动范围   CV
QPS
P50
P95
P99

噪声底线(取最大波动 × 1.5):
  QPS : ±___%
  P50 : ±___%
  P99 : ±___%

最小可检测差异(MDD ≈ 噪声底线 × 2):
  P99 : ±___%

填进你的实验档案,并回答:

  1. 哪个指标的噪声最大?为什么?(提示:通常 P99 > P95 > P50,因为尾部样本少)
  2. 你的环境能不能检测 10% 的优化?20% 呢?
  3. st(steal)指标是多少?如果 > 3%,你的噪声底线是否与之相关?

六、任务 4:显著性验证(验证你的测量系统)

这一步验证的是「你的统计方法」,不是被测代码。

验证 A:制造一个约 5% 的改动,确认判定为「不显著」

制造 5% 改动的方法(选一个):

  • 在热路径上加一次无用的内存计算(约 5% 的开销);
  • 加一行 debug 级日志(不开 debug,开销很小);
  • 把某个小循环的次数从 10 改成 10.5 倍。

预期结果:如果 5% 小于你的 MDD,Mann-Whitney U 检验应该给出 p > 0.05(不显著)。

如果判定为"显著",有两种可能:① 你的噪声底线其实比 5% 小(好事,说明环境很干净)——那就在报告里如实记录;② 你的改动实际影响大于 5%。

验证 B:制造一个约 30% 的改动,确认判定为「显著」

制造 30% 改动的方法:

  • 把某个查询加上一个明显无用的排序;
  • 在接口里加 Thread.sleep(2)(对 10 ms 级接口约 20–30%);
  • 把某个批量大小从 100 改成 10。

预期结果:p < 0.05(显著)。

两个验证都要记录

## 显著性验证

### 验证 A:约 5% 的改动
- 改动内容:__________
- 基线 P99 中位数:_____ ms
- 改动后 P99 中位数:_____ ms
- 实际差异:_____%
- Mann-Whitney U: p = _____
- 判定:☐ 不显著(符合预期)☐ 显著(需解释)
- 解释:__________

### 验证 B:约 30% 的改动
- 改动内容:__________
- 实际差异:_____%
- Mann-Whitney U: p = _____
- 判定:☐ 显著(符合预期)☐ 不显著(说明测量系统有问题)

如果验证 B 判定为「不显著」,说明你的测量系统有问题——5 轮样本太少,或者环境噪声太大。


七、验收标准

  • 环境检查清单逐项填完,且实验期间无改动。
  • 跑了 10 轮(每轮都重启 + 独立预热)。
  • 每轮的原始数据都保存了(round-N.json)。
  • 算出了 QPS / P50 / P95 / P99 各自的波动范围与 CV。
  • 给出了噪声底线(含 1.5 倍安全系数)和最小可检测差异(MDD)。
  • 验证 A(约 5% 改动)判定为「不显著」(或给出了合理解释)。
  • 验证 B(约 30% 改动)判定为「显著」。
  • 检查了 10 轮是否有趋势漂移(如果有,说明环境有问题)。
  • 实验档案含「预期 vs 实际」表与至少三条假设台账条目。

八、常见问题

Q:10 轮太多了,能不能只跑 5 轮? A:可以,但要注意:① 5 轮也能算出波动范围,但置信区间会很宽;② Mann-Whitney U 检验在每组 5 个样本时勉强可用;③ 如果 5 轮的 CV 就超过 10%,说明环境有问题,此时增加轮数不如先修环境。建议:先跑 5 轮,如果 CV > 10%,停下来修环境;如果 CV < 5%,5 轮够用。

Q:每轮都要重启服务,太花时间了。 A:如果每轮 5 分钟 + 预热 1 分钟 + 重启 10 秒,10 轮约 1 小时——这是必要的成本。如果不重启,你测的是"热状态累积的影响",不是环境噪声。折中方案:可以缩短每轮时长(比如 3 分钟),但要保证 ≥ 2 分钟以覆盖至少一个 GC 周期。

Q:我发现 10 轮有单调上升趋势,怎么办? A:这不是噪声,是环境漂移(第 7 节)。常见原因:数据量增长(压测写了数据)、缓存污染、内存泄漏、机器降频。处理:先查清原因并消除,再重测噪声底线。有漂移的环境测出的底线不可信。

Q:我的环境噪声底线是 ±20%,太大了。 A:这说明环境不适合做精细对比。三个方向:① 降低噪声——独占机器、绑核、固定频率、关后台任务、检查 st;② 只做大颗粒度判断——用你的环境只能确认「> 40% 的差异」(MDD = 20% × 2);③ 改用组件基准——它对环境噪声更不敏感(因为不依赖网络与完整链路)。

Q:验证 A 的 5% 改动被判成"显著"了,是不是我做错了? A:不一定是错。三种情况:① 你的环境非常干净(噪声底线 < 5%),那么 5% 确实可以被检测到——这是好消息;② 你制造的改动实际影响大于 5%(比如加了 Thread.sleep(1) 在 10 ms 接口上就是 10%);③ 你在两组实验之间无意中改变了别的变量(检查环境记录)。处理:先确认是哪种情况,再如实记录。