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 : ±___%
填进你的实验档案,并回答:
- 哪个指标的噪声最大?为什么?(提示:通常 P99 > P95 > P50,因为尾部样本少)
- 你的环境能不能检测 10% 的优化?20% 呢?
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%);③ 你在两组实验之间无意中改变了别的变量(检查环境记录)。处理:先确认是哪种情况,再如实记录。