5.4 环境隔离清单
上一节:5.3 重复、抖动与异常值 | 下一节:5.5 数据集设计 配套代码:05-experiment-design/04-environment-isolation
一句话结论
实验环境里任何「你没关掉的东西」,都会变成数据里的噪声。 而与性能测试不同,噪声不会报错——它只会让你的结论变得不可信,而你往往察觉不到。
一、用「化学实验的洁净室」理解环境隔离
化学实验里,一点灰尘就可能让结果完全偏差。所以:
| 化学实验室 | 性能实验 |
|---|---|
| 无尘环境 | 独占的测试机器 |
| 恒温恒湿 | 固定的 CPU 频率、无降频 |
| 纯净试剂 | 真实但固定的数据集 |
| 空白对照 | 基线组 |
| 记录环境参数 | 采集环境元数据 |
关键差异:化学实验的污染通常是可见的(溶液变色),性能实验的污染是不可见的——你只会看到一个稍微高一点的 P99,然后开始怀疑代码。
二、五类污染源与处理方式
❶ 同一台机器上的其他负载
| 污染源 | 影响 | 处理 |
|---|---|---|
| 压测客户端与服务同机 | 互相抢 CPU,两个方向都失真 | 必须分离(最低要求:不同容器 + CPU 限制) |
| IDE / 浏览器 / 本地开发 | 抢占 CPU 与内存 | 关掉 |
| 其他服务实例 | 争抢资源 | 独占环境 |
| 监控 agent | 持续采集消耗资源 | 记录其开销,或降频采样 |
❷ 机器自身的周期性任务
| 污染源 | 频率 | 表现 | 处理 |
|---|---|---|---|
| 数据库 autovacuum | 不定 | 磁盘 IO 尖刺 | 实验前手动 VACUUM ANALYZE,期间可临时调整 autovacuum 参数 |
| 数据库 checkpoint | 不定 | IO 抖动 | 观察 checkpoint 配置与日志时间戳 |
| 日志轮转 | 按大小/时间 | 磁盘写尖刺 | 避开轮转时刻,或临时调大阈值 |
| 备份任务 | 通常夜间 | 全盘 IO 与网络 | 实验前确认已关闭 |
cron / 定时任务 |
各种 | 周期性 CPU/IO | pgrep -a -f cron 确认 |
| 系统更新 | 不定 | 突发 CPU/IO | 关闭自动更新 |
❸ CPU 频率与调度
| 污染源 | 影响 | 处理 |
|---|---|---|
| CPU governor(节能模式) | 频率波动 → 延迟抖动 | 设为 performance |
| Turbo Boost | 前后跑的基准频率不同 | 要么固定频率,要么全程接受 turbo(关键是一致) |
| 未绑核 | 线程在不同核间迁移,缓存失效 | taskset 绑定 |
| 超线程干扰 | 同一物理核上的两个逻辑核互相影响 | 明确是否启用,并保持一致 |
| NUMA 跨节点 | 内存访问变慢 | numactl 绑定 |
❹ 云环境的不可控因素
| 污染源 | 表现 | 处理 |
|---|---|---|
| 邻居抢占(steal) | st 指标高,延迟抖动 |
换独占型实例;或多次实验取可比时段 |
| 网络路径变化 | 跨 AZ 调用延迟变化 | 固定可用区;记录网络拓扑 |
| 存储 IOPS 配额 | IO 突发受限 | 确认卷类型与 IOPS 上限 |
| 宿主机维护 | 突发迁移 | 记录实验期间的事件 |
❺ 实验自身的干扰
| 污染源 | 影响 | 处理 |
|---|---|---|
| 高开销剖析(profiler) | 占用 CPU,抬高 P99 | 基线组和实验组必须用同样的观测配置 |
| 详细日志(DEBUG) | 同步 IO,可能占 10%+ | 统一日志级别 |
/metrics 高频抓取 |
序列化开销 | 统一 scrape interval |
| 压测机自身瓶颈 | 实际到达率低于设定值 | 每次压测先验证到达率(第 4 章 4.1) |
三、实验前的检查清单(脚本化)
## 环境检查(实验前逐项确认)
### 机器
- [ ] CPU governor = performance
- [ ] 已绑核(或明确不绑核,并保持一致)
- [ ] 关闭了自动更新
- [ ] 无其他无关进程(`ps aux --sort=-%cpu | head`)
- [ ] `st`(steal)< 1%(多次采样确认)
### 容器
- [ ] CPU limit 与生产一致
- [ ] 内存 limit 与生产一致
- [ ] 无 sidecar 干扰(或已计入)
- [ ] 节流比例 < 1%(`/sys/fs/cgroup/cpu.stat`)
### 数据库
- [ ] autovacuum 状态已知(关闭或已记录)
- [ ] 无长事务(`pg_stat_activity`)
- [ ] 统计信息已更新(`ANALYZE`)
- [ ] 连接数无人抢占
### 服务
- [ ] JVM 参数与生产一致
- [ ] 日志级别一致
- [ ] 观测配置一致(JFR/profiler)
- [ ] 服务已重启(避免上一轮热状态污染)
### 压测
- [ ] 客户端独立机器/容器
- [ ] 脚本已冻结快照
- [ ] 已预热(或明确预热阶段)
- [ ] 到达率可验证
### 记录
- [ ] 环境元数据已采集(第 1 章 1.7)
- [ ] 环境事件时间线已开启记录
四、绑核与频率固定的具体命令
# ① 固定 CPU 频率(Linux)
sudo cpupower frequency-set -g performance
# 验证
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
# ② 绑定到指定核(服务用 2-5,压测客户端用 6-7,互不干扰)
taskset -c 2-5 java -jar app.jar
taskset -c 6-7 k6 run loadtest.js
# ③ NUMA 绑定
numactl --cpunodebind=0 --membind=0 java -jar app.jar
# ④ 查看 NUMA 拓扑
numactl --hardware
# ⑤ 容器内限制 JVM 看到的核数(避免线程池过大)
java -XX:ActiveProcessorCount=2 -jar app.jar
# ⑥ 关闭 swap(避免内存压力导致的抖动)
sudo swapoff -a
注意:绑核能降低噪声,但也可能降低吞吐(因为无法利用其他核)。如果实验目的是测「最大吞吐」,绑核会让结果偏低。关键是「一致性」而不是「绝对最优」——基线组和实验组用同样的配置即可。
五、验证环境是否「干净」的三个方法
方法一:空跑基线(最有说服力)
在没有任何改动的条件下,连续跑 5 次实验,看结果的波动范围。
如果 5 次结果几乎一致(CV < 3%)→ 环境干净
如果波动很大(CV > 10%)→ 环境有问题,先修环境
这就是第 7 节的「噪声底线」——它是判断"环境干不干净"的量化标准。
方法二:观测 st 与上下文切换
# 连续采样 10 次
for i in $(seq 1 10); do
vmstat 1 1 | tail -1 | awk '{print "st=" $17 "% cs=" $12}'
sleep 1
done
判读:st 持续 > 3% → 环境不可控;cs 波动极大 → 有其他负载。
方法三:检查「实验期间的意外事件」
# 记录实验期间的系统日志(内核、OOM、CPU 限流)
dmesg -T | tail -50
journalctl --since "1 hour ago" | grep -iE "oom|throttl|error" | head -20
特别注意 dmesg 里的 OOM 与 TCP 丢包记录——它们能解释很多"莫名奇妙"的异常。
六、本节小结
- 环境里的任何"没关掉的东西"都会变成噪声,而噪声不报错——它只让结论不可信。
- 五类污染源:机器上的其他负载、周期性任务、CPU 频率与调度、云环境不可控因素、实验自身的干扰。
- 最后一项最容易被忽略:剖析工具、详细日志、指标抓取都会影响结果,基线组和实验组必须配置一致。
- 环境检查要清单化 + 脚本化,否则一定会漏。
- 验证环境是否干净的方法:空跑基线看 CV(最有力)、看
st与cs、查系统日志。 - 绑核与固定频率的目标是一致性,不是绝对最优。
七、自测
- 你发现每次压测的前 2 分钟 P99 都偏高,之后下降。这可能是什么原因?请列出至少三种可能,以及各自的验证方法。
- 为什么「观测工具的变化」属于环境隔离问题?如果基线组开了 JFR 而实验组没开,你会看到什么假象?
- 一个团队在云主机上做实验,结果波动很大(P99 在 80–300 ms 之间跳)。请给出三个排查方向,并说明哪个最可能是根因。
- 可能原因与验证:① JVM 预热不足——验证:用滚动 CV 观察是否收敛(第 2 章 2.4);看
-XX:+PrintCompilation的编译事件时间分布。② 连接池/缓存冷启动——验证:观察db_pool_active是否在前 2 分钟逐渐上升至稳态;观察缓存命中率曲线。③ 数据库缓存未热——验证:看 PG 的pg_stat_database.blks_hit / blks_read比例在前 2 分钟的变化;或看查询耗时的 P50 曲线形状。④ 还有可能是上一个实验的残留影响(如果没重启服务)——验证:确认实验前是否重启了服务。⑤ 压测机预热(k6 自身也需要 GC/JIT 预热,如果是 JVM 系工具)。结论:无论哪种,处理方式都是把前 2 分钟作为预热阶段,不计入统计——但前提是确认它确实会收敛。 - 因为观测工具会消耗被测系统的资源,所以它和"机器上跑着别的程序"在本质上是一类污染(第 0 章 0.9 节:观测改变被观测对象)。如果基线组开了 JFR(约 1–2% 开销)而实验组没开,你会看到实验组"变快了"1–2%——但这个差异完全来自观测开销,不是代码优化。更严重的情况:如果基线组用了
-e cpu -i 1ms的高频采样(开销可能 5–15%),那么实验组会显得"提升 10%",而实际上什么都没优化。这就是为什么观测配置必须冻结。 - 三个排查方向与优先级:① 云主机邻居抢占(steal)——最可能的根因。验证:
vmstat看st列是否持续 > 3%;如果是,说明宿主机的其他租户在抢占 CPU,你的应用什么都没做也会抖动。处理:换独占型实例、调整部署位置,或接受这个噪声并把它写进噪声底线。② CPU 频率波动或降频——验证:grep MHz /proc/cpuinfo多次采样看频率是否变化;检查 governor。③ 周期性任务或自动扩缩容——验证:记录实验期间的环境事件时间线(cron、备份、其他服务部署),看抖动是否与某些时刻对齐。为什么 steal 最可能:因为它的典型特征就是"随机的大幅抖动"(应用侧所有指标正常,但延迟周期性飙升),而后两者通常有更规律的模式。验证顺序:先看st(一条命令),再查频率,最后查事件时间线。