文档目录

1.7 实验元数据:没有它,数字等于没有

上一节:1.6 基线与容量曲线 | 下一节:1.8 案例解剖 配套代码:01-metrics-and-slo/07-experiment-metadata


一句话结论

一个性能数字,如果没有配套的「实验条件」,就是不可用信息。 就像菜谱写「加盐适量」——你自己下次也做不出同一道菜。元数据清单的作用是:让三个月后的你(或另一个工程师)能复现出同样的数字。


一、用「菜谱」理解元数据

一份能用的菜谱:

中火,橄榄油 15 ml,蒜末 10 g,翻炒 90 秒,加盐 2 g。

一份没用的菜谱:

用合适的火候,加点油和蒜,炒一会儿,调味。

差别就在于「条件是否可复现」。 性能数据完全一样:

没用的说法 有用的说法
「压测了一下,P99 是 180 ms」 「JDK 21 + 默认 GC、4C8G、100 万行齐夫分布、k6 恒定到达率 500/s、预热 60 秒、稳态 5 分钟,客户端测得 P99 = 180 ms」

第二句话,别人能复现;第一句话,只是一次性的观察。


二、九项清单(缺一项都可能无法复现)

① 代码版本

  • 内容:commit hash 或镜像 tag。
  • 为什么必须:代码变了,性能就变了。这一项缺失,整个实验无法回溯。

② JVM 版本与完整参数

  • 内容:JDK 发行版与版本号 + 完整的 JVM 启动参数(堆大小、GC 选型、-XX 开关)。
  • 为什么必须:-Xmx 从 2g 改到 4g,GC 行为完全不同;换一个 GC 可能让吞吐差 20%。
  • 常见错误:只写「JDK 21」,不写堆大小和 GC 类型。

③ 机器规格

  • 内容:CPU 型号与核数、内存、磁盘类型(SSD/网络盘)、NUMA、是否容器。
  • 为什么必须:「4 核」不等于「4 核」——不同代 CPU 的单核性能可能差 2 倍。
  • 常见错误:写「8 核 16G」,但没写是物理机、云主机还是容器。

④ 容器与编排配置

  • 内容:CPU limit / request、内存 limit、是否有 sidecar、副本数。
  • 为什么必须:容器 CPU 节流是「本地压测很好、上线就慢」的头号原因。limit 设成 1 核时,即使宿主机有 32 核,你的进程也只能用 1 核,而且会被周期性掐停。

⑤ 操作系统与内核参数

  • 内容:OS 与内核版本、文件描述符上限、somaxconn、CPU governor。
  • 为什么必须:文件描述符上限会直接限制并发连接数;CPU governor 设为省电模式会导致频率波动,让数据抖动。

⑥ 数据库与中间件

  • 内容:版本、规格、关键配置(连接数上限、缓存大小、隔离级别)、连接池配置。
  • 为什么必须:PostgreSQL 的 max_connections、shared_buffers 直接决定性能上限。

⑦ 数据集:规模 + 分布

  • 内容:数据量级、访问分布(均匀 / 幂律,幂律要写 alpha)、是否预热缓存、连接是否复用。
  • 为什么必须:这是最常被忽略的一项。1 万行 vs 1 亿行会导致执行计划完全不同;均匀分布 vs 幂律分布会让缓存命中率差几倍。

⑧ 压测方式

  • 内容:工具与版本、流量模型(到达率 / 虚拟用户)、目标 QPS、预热时长、测量时长、脚本 commit。
  • 为什么必须:闭环模型会系统性低估尾延迟(第 0 章 0.6 节)。不写模型,读者无法判断数据可信度。

⑨ 测量点与观测工具

  • 内容:客户端 / 网关 / 应用内(见 1.3 节)、观测工具与采样频率、直方图桶边界。
  • 为什么必须:不同测量点的数字不能比较;桶边界不对会让分位数失真。

三、两个真实的反例

反例 1:漏了 JVM 参数

实验 A(上周):P99 = 120 ms
实验 B(本周):P99 = 95 ms,结论「优化有效,提升 21%」

真相:本周换了 JVM 参数(堆从 2g 到 4g,GC 从 G1 换 ZGC),
      代码一行没改。所谓「优化」是环境变化带来的。

教训:只有 JVM 参数被记录,这个错误才能被发现。

反例 2:漏了数据分布

实验 A(均匀分布):缓存命中率 35%,P99 = 180 ms,结论「不达标,需要优化」
实验 B(幂律分布):缓存命中率 85%,P99 = 60 ms,结论「达标」

两次压测压的是「同一个接口」,但流量分布不同,
结论从「不达标」变成「达标」。

教训:均匀分布会低估缓存的效果,也会低估锁竞争。真实业务几乎总是幂律分布(少数热点承担大部分请求)。用均匀分布测出的结论,可能同时不适用于「平时」和「大促」。


四、怎么落地:自动化采集

不要靠手写——手写的元数据一定会漏。建议做一个脚本,在每次实验开始时自动落盘:

# tools/collect-env.sh —— 由实验驱动器自动调用
{
  echo "commit=$(git rev-parse HEAD)"
  echo "date=$(date -Iseconds)"
  java -version 2>&1 | head -3
  echo "jvm_args=$JVM_ARGS"
  uname -a
  echo "cpus=$(nproc)"
  echo "mem=$(free -h | awk '/Mem:/{print $2}')"
  echo "cgroup_cpu_max=$(cat /sys/fs/cgroup/cpu.max 2>/dev/null || echo n/a)"
  echo "ulimit_nofile=$(ulimit -n)"
  echo "governor=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null || echo n/a)"
  psql -c "select version()" 2>/dev/null | head -3
  psql -c "select count(*) from links" 2>/dev/null
} | tee "docs/experiments/$EXP_ID/results/env.txt"

生成的文件直接粘进实验档案的「环境元数据」一节(模板见 docs/experiments/_TEMPLATE/README.md)。

记住那条纪律:手写会漏,脚本不会。


五、本节小结

  1. 没有元数据的性能数字是不可用的——别人无法复现,你自己三个月后也无法复现。
  2. 九项清单:代码版本、JVM 参数、机器规格、容器配置、OS 与内核参数、数据库与中间件、数据集规模与分布、压测方式与模型、测量点与观测工具。
  3. 最常被忽略、后果最严重的两项是:JVM 完整参数和数据分布。
  4. 用脚本自动采集,不要手写——手写一定会漏。

六、自测

  1. 同事发给你一份压测报告:「P99 = 95 ms,达标。」你至少要追问哪四个问题才能判断这个结论是否可信?
  2. 为什么「容器 CPU limit」这一项在本地开发机上压测时完全不重要,但在生产环境却是头号嫌疑?
  3. 数据分布从「均匀」换成「幂律」,对以下三个指标分别有什么影响:缓存命中率、锁竞争程度、P99?
  1. ① 测量点:客户端、网关还是应用内?(不能混用比较)② 负载前提:多少 QPS、什么流量模型(开环还是闭环)、数据量多大、什么分布?③ 环境:机器规格、JVM 参数、是否容器及 CPU limit?④ 样本与时长:跑了几轮、预热多久、稳态多久、样本量多少、错误率如何?可以再追问第五个:桶边界怎么设的(如果 P99 是从直方图算的)。
  2. 因为本地开发机(或独立压测机)通常不受 cgroup CPU 配额限制,进程可以使用所有核心;而在容器里,CPU limit 会通过 CFS 配额周期性限制进程的 CPU 时间——进程被"掐停"再恢复。表现为:平均 CPU 利用率看起来不高(比如 40%),但延迟会间隔性抖动,P99 明显变差。这个现象非常反直觉,如果不看 cpu.stat 里的 nr_throttled / throttled_usec,几乎不可能定位到。
  3. ① 缓存命中率上升:幂律分布下少数热点 key 承担大部分请求,缓存能覆盖大部分流量(可能从 30% 涨到 85%);② 锁竞争加剧:热点数据被反复访问,如果访问路径上有锁,竞争会集中在少数 key 上(这既可能让竞争更严重,也让分片这类优化更有效——按 key 分片能把热点打散);③ P99 的表现不确定:缓存命中率上升会让 P99 下降,但热点 key 的失效瞬间(缓存击穿)会制造尖刺,可能让 P99 反而更差。所以必须用真实分布测,并且要专门测「热点 key 失效」这个场景。