2502 字
约 8 分钟
14
S27-A15 · 测速的陷阱:为什么你的数字不准

一份实测记录写着"改完后快了 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. 自测

  1. 测速必须写清的四件事分别是什么?为什么"测量方式"最常被省略?
  2. 环境里为什么版本要写到 minor?"网络距离"为什么可以造成一个数量级的差异?
  3. 为什么测速数据要包含"倾斜分布"?均匀分布的数据会漏掉什么问题?
  4. 闭环与开环各给出什么结论?为什么闭环的吞吐上限不能当作"能扛住的流量"?
  5. "未预热"为什么会让数字偏乐观?为什么平均值永远好看?
  6. 判断"是否需要严格测"的那个问题是什么?
S27-A15 · 测速的陷阱:为什么你的数字不准
https://www.clxhxhhr.top/posts/6479/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。