一份实测记录写着"改完后快了 3 倍"。复现时怎么也复现不出来:作者那台机器是本地磁盘、数据量 10 万行、只跑了一次;复现环境是网络存储、数据量 300 万行、并发 20。两组数字都对,但它们不是同一个实验。
测速的问题几乎从来不是"数据不真实",而是没有说明测的是什么。
这四件事之外还有一个"元信息":测的人是谁、什么时候测的。因为环境会变(同机不同时段、别人也在跑任务),所以"三个月前测的"与"今天测的"即使四件事都写清,也可能不可比。
而"测的是什么"包含四件事:环境、数据规模、负载形态、测量方式——缺一件,数字就不能被引用。
1. 四件必须写清的事
| 项目 | 缺了会怎样 |
|---|---|
| 环境(硬件、OS、版本) | 无法判断是否可比 |
| 数据规模与分布 | 结论只在特定规模成立 |
| 负载形态(并发、速率) | 单线程结果不能外推 |
| 测量方式(计时点、轮次) | 包含/不包含无法判断 |
计时点还有一个"分布"问题:端到端耗时可以分解成排队、处理、传输三段,而报告里通常只给一个总数。总数相同、三段构成不同,优化方向完全相反。
第四项最常被省略:"耗时 200ms"是从哪一刻到哪一刻,包含了序列化还是只有 SQL?包含网络还是只有服务端?两个都"200ms",含义差一倍。
2. 环境里必须写的六项
| 项 | 为什么 |
|---|---|
| CPU 核数与主频 | 并发能力 |
| 内存与磁盘类型 | IO 上限 |
| OS 与内核版本 | 调度与 IO 行为 |
| 运行时版本(到 minor) | 行为差异 |
| 部署形态(容器/裸机/云) | 资源限制 |
| 网络距离 | 往返时延 |
第四项是"8.0"与"8.0.34"的差别:同一个大版本内的补丁版本也可能改变默认值与调度行为。
还有一个环境项常被漏掉:其他负载。一台机器上还跑着别的东西时,"8 核"实际上不是 8 核可用;而报告里写的是规格,不是实际可用量。
第六项在分布式场景里最容易被忽略:跨机房与同机房的结果可以差一个数量级,而报告里只写"分布式环境"。
// 环境块的标准写法(十行以内,可复制粘贴)
env:
cpu: 8 vCPU (Xeon 6248 @2.5GHz)
mem: 32 GiB
disk: NVMe SSD (顺序读写 3.2/2.0 GB/s)
os: Ubuntu 24.04 / kernel 6.8
runtime: "MySQL 8.0.34, innodb 默认参数"
deploy: docker, cpu limit 8, mem limit 32G
network: 同机房,RTT < 0.2ms
3. 数据规模与分布
| 维度 | 影响 |
|---|---|
| 行数 | 索引与全扫的交叉点 |
| 字段长度 | 页密度、IO 量 |
| 值分布(倾斜) | 计划选择 |
| 索引数量 | 写入成本 |
| 冷热状态 | 缓存命中 |
第一项决定结论的适用范围:"加了索引快 10 倍"在 10 万行成立,在 1000 万行可能只快 2 倍(因为两者瓶颈不同)。
第五项(冷热状态)决定了两类测法:冷启动测试与稳态测试。前者测"服务刚起来的头几十秒",后者测"跑了一小时之后"。两者都有用,但不能混着报告。
第三项常被忽略:均匀分布的数据不会暴露计划问题,而真实数据的倾斜恰恰是问题的来源。所以测速数据应当包含"少数值占多数行"的形态。
// 数据描述的三行
rows: 1,000,000(sku 维度 10% 集中在 3 个值)
avg_row_size: 420 B(含 2 个 text 字段,平均 120 B)
indexes: 5(主键 + 4 个二级索引)
// 缺"分布"这一行 → 测不出计划退化
4. 负载形态:闭环与开环
| 形态 | 含义 |
|---|---|
| 闭环(closed-loop) | N 个客户端循环发请求 |
| 开环(open-loop) | 按固定速率发请求 |
| 突发(burst) | 短时间内高速率 |
| 混合 | 读写比例固定 |
第一项的局限是它自带背压:服务变慢时客户端也跟着变慢,于是"QPS 下降"与"延迟上升"互相掩盖。而真实流量往往是开环的——用户不会因为你慢就少发请求。
第三项(突发)在秒杀类场景里是常态:平时 200 QPS,开抢瞬间 5000 QPS。此时系统需要的是"能扛住短时爆发"的能力,而稳态压测完全测不到这一点——它需要的是"限流 + 排队 + 快速失败"的组合设计。
所以"闭环压测得到的吞吐上限"不能直接当作"能扛住的流量"。
// 两种形态测出的结论不同
闭环(20 客户端):QPS 稳定 1800,P99 120ms → 但这是"系统愿意接受"的速率
开环(固定 2400 QPS):队列积压,P99 涨到 2s → 真实流量超出承载
// 结论:闭环给"最佳工作点",开环给"崩溃点",两者都要
5. 测量方式的四个坑
| 坑 | 表现 |
|---|---|
| 计时点不一致 | 两边"200ms"含义不同 |
| 只有一次 | 噪声被当成结果 |
| 未预热 | 测的是冷启动 |
| 未取分位 | 平均值掩盖长尾 |
还有一个"测量本身影响结果"的情况:采样与日志会改变被测系统的行为。开启慢查询日志后,写入性能可能下降 10%——于是"带日志优化"与"不带日志优化"的对比失真。
第三项最典型:JIT 未编译、缓存未填充、连接未建立,第一次运行的耗时可以是稳态的三倍。所以至少丢弃前若干轮。
第四项:平均值永远好看。一个接口 95% 请求 10ms、5% 请求 900ms,平均值是 55ms——而用户感受到的是 900ms。
// 预热 + 多轮 + 分位:最小可信测法
for i in 1 2 3; do curl -s -o /dev/null "$URL"; done # 预热 3 次
for i in $(seq 1 20); do
curl -s -o /dev/null -w '%{time_total}\n' "$URL"
done | sort -n | awk '{a[NR]=$1} END {
printf "p50=%.3f p95=%.3f p99=%.3f max=%.3f\n", a[int(NR*0.5)], a[int(NR*0.95)], a[int(NR*0.99)], a[NR]
}'
测速还有一个"对比有效性"的前提:只改一个变量。同时换机器、换数据量、换参数,即使结果变好也无法归因;而"我们做了一堆优化后快了 3 倍"这类结论,恰恰最常被这样得出。
// 对比矩阵:一次只动一个变量
基线: 机器 A,100 万行,参数默认 → 1200ms
实验 1:机器 A,100 万行,参数 X=1 → 700ms (可归因)
实验 2:机器 B,100 万行,参数 X=1 → 500ms (换机器,仍需单独验证)
// 反例:机器 B + 300 万行 + 参数 X=1 → 800ms,无法说明任何事
6. 排错:五种"数字不准"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 复现不出来 | 环境或规模不同 | 补环境块与规模 |
| 快得离谱 | 未预热或测的是缓存 | 丢弃前几轮、清缓存 |
| 波动极大 | 采样不足 | 增轮次、看分位 |
| 与线上相反 | 闭环 vs 开环 | 换负载形态 |
| 只在特定数据量成立 | 规模依赖 | 说明适用范围 |
与之相对的错误是"过度限定":把适用范围写得比实际窄,于是结论没人敢用。合理的做法是写"在 A 到 B 之间成立,超出需要重测",给出可延伸的方向而不是只写一个点。
第五项本身不是错误,而是结论的必要限定:任何性能结论都应当带一句"在 X 规模下"。没有这句,它一定会被用到不成立的场景。
7. 边界:什么情况不必严格测
| 场景 | 为什么 | 做法 |
|---|---|---|
| 数量级差异(10 倍以上) | 噪声不影响结论 | 粗测即可 |
| 一次性对比(选型) | 只需相对结论 | 同环境同数据 |
| 已有权威基准 | 无需重复 | 引用并标注版本 |
| 结论只用于自己判断 | 不被引用 | 随意 |
最后一条与"记录成本"有关的边界:四件事全写清只要十分钟,而重测一次要半天。所以"没时间写清楚"在成本上是不划算的——尤其当这个数字会被第二个人引用时。
判断口径一句话:这个数字会不会被别人拿去支撑一个决定。会,四件事都要写清;不会(自己看一眼),粗测就够。
8. 自测
- 测速必须写清的四件事分别是什么?为什么"测量方式"最常被省略?
- 环境里为什么版本要写到 minor?"网络距离"为什么可以造成一个数量级的差异?
- 为什么测速数据要包含"倾斜分布"?均匀分布的数据会漏掉什么问题?
- 闭环与开环各给出什么结论?为什么闭环的吞吐上限不能当作"能扛住的流量"?
- "未预热"为什么会让数字偏乐观?为什么平均值永远好看?
- 判断"是否需要严格测"的那个问题是什么?