文档目录

0.7 九条常见谬误自查清单

上一节:0.6 协调遗漏:压测报告的最大谎言 | 下一节:0.8 性能工程闭环 用法:拿你现在项目里的做法,逐条对照。踩了 3 条以上,说明你现在的性能数据基本不可用。


一、九条谬误(对照自查)

❶ 只看均值,不看分布

  • 表现:汇报「平均延迟 12 ms,性能良好」。
  • 为什么错:均值会把 1% 用户的糟糕体验摊薄掉(见 0.3)。
  • 改成:至少同时报 P50 / P95 / P99 + 错误率 + 样本量。

❷ 用闭环(VU)模型测尾延迟

  • 表现:「用 100 个并发用户压了 5 分钟」。
  • 为什么错:协调遗漏会系统性低估尾延迟,服务越糟报告越好看(见 0.6)。
  • 改成:用恒定到达率(开环)模型;或至少交叉验证实际请求数是否等于期望值。

❸ 压测期间不观测服务端

  • 表现:只盯着压测工具输出的 QPS 和延迟,服务端除了一个 top 什么都没看。
  • 为什么错:你只知道「慢了」,不知道「为什么慢」,无法定位也无法优化。压测的价值有 80% 在服务端数据里。
  • 改成:压测同时采集四层指标(业务/延迟/资源/饱和度),并保存火焰图与 GC 日志。

❹ 改完代码不回归压测

  • 表现:「我觉得这次改动应该快了不少,直接上线吧。」
  • 为什么错:没有 before/after 对比,优化收益无法证明,甚至可能引入了退化。
  • 改成:同一脚本、同一环境、≥3 轮对比,并用统计方法判断差异是否显著(第 5 章)。

❺ 把一次运行的结果当结论

  • 表现:「跑了一次,P99 是 95 ms,达标。」
  • 为什么错:你自己的环境噪声可能有 ±8%,单次运行的数字毫无意义。
  • 改成:先量化噪声底线(跑 5 次相同配置),再做结论;报告里给出每轮的值。

❻ 忽略预热(JVM 特有)

  • 表现:服务启动后立刻压测,把前 30 秒的数据算进 P99。
  • 为什么错:那时代码还在解释执行 / C1 阶段、连接池还是空的、数据库缓存还没热,数据不能代表稳态。
  • 改成:预热阶段独立于测量阶段,且预热数据不计入统计。

❼ 数据集与线上不一致

  • 表现:用 1 万行数据压测,线上是 1 亿行。
  • 为什么错:数据量决定执行计划——1 万行可能全表扫描很快,1 亿行则会走索引。结论完全不可迁移。同样,均匀分布的数据会让缓存命中率虚高、锁竞争虚低。
  • 改成:数据量与线上同数量级,访问分布用**幂律(齐夫)**模拟真实热点。

❽ 压测客户端与服务同机

  • 表现:在同一台机器上跑服务和 k6。
  • 为什么错:压测工具自己也要吃 CPU、内存、网络,和服务互相抢资源,两边数据都失真。而且会掩盖网络延迟。
  • 改成:压测客户端独立部署;至少做到绑核隔离。

❾ 只看性能不看错误率

  • 表现:「QPS 从 3000 提升到 5000,优化成功!」——同时错误率从 0.1% 涨到 4%。
  • 为什么错:这不是性能提升,是把「慢」换成了「错」。返回 500 的请求不计入成功延迟统计,所以延迟数字反而「变好」了。
  • 改成:性能与错误率必须同时看,且延迟统计的分母用总请求数,不是成功请求数。

二、这些谬误会互相掩盖

单独看每一条都不致命,但它们组合起来会形成「看起来很专业、实际上毫无信息量」的报告:

闭环压测(❷)
   + 只看均值(❶)
   + 不看服务端(❸)
   ─────────────────────
   = 一个漂亮但没有信息量的数字

不预热(❻)
   + 单次运行(❺)
   + 数据量不一致(❼)
   ─────────────────────
   = 一个无法复现、也无法迁移的数字

不回归(❹)
   + 不看错误率(❾)
   ─────────────────────
   = 一个把问题藏起来的"优化"

最危险的不是某一个错误,而是「所有错误都朝同一个方向偏」——它们联合起来让数据看起来比实际更好。


三、一页纸自查表

打印或贴在显示器边上,每次做完实验过一遍:

  • 报了 P50/P95/P99,不只是平均值
  • 报了错误率,且延迟统计的分母是总请求数
  • 说明了测量点(客户端 / 网关 / 服务端)
  • 说明了负载前提(QPS、数据量、数据分布)
  • 压测用的是开环(到达率)模型,或已验证实际请求数符合预期
  • 压测期间采集了服务端指标(四层)+ 火焰图 + GC 日志
  • 预热与测量分离,预热数据未计入统计
  • 同一配置跑了 ≥3 轮,给出了每轮的值
  • 知道自己的噪声底线,结论的差异大于噪声
  • 压测客户端与服务不在同一台机器
  • 有 before/after 对照,且统计上显著
  • 原始数据已落盘,别人能按记录复现

四、本节小结

  1. 九条谬误里,最隐蔽的是 ❷(协调遗漏)和 ❾(只看性能不看错误率)——它们不会报错,只会给你一个错误但漂亮的数字。
  2. 谬误的危险在于方向一致地偏乐观,组合起来会形成「专业但无用」的报告。
  3. 把上面的自查表变成固定流程,比记住九条谬误更有效。

五、自测

  1. 下面这份报告有几处问题?分别对应哪条谬误?

    「我们对订单接口做了优化。用 50 个并发用户压测了 3 分钟,平均延迟从 45 ms 降到 22 ms,性能提升一倍。压测在本地开发机上完成,数据库用了 5000 条测试数据。」

  2. 为什么「错误率上升」会让「延迟指标变好」?(这道题的答案会改变你看待性能报告的方式)
  3. 在这九条里,哪一条是只有 JVM 平台才有的?为什么 C++ 项目不需要担心它?
  1. 至少五处:
    • ❷ 闭环 VU 模型(50 个并发用户)→ 尾延迟被低估
    • ❶ 只报平均值,没有百分位
    • ❸❺ 没提服务端任何数据,且只有一次运行
    • ❽ 本地开发机压测(客户端与服务同机 + 机器规格不代表生产)
    • ❼ 5000 条测试数据(与线上量级差几个数量级)
    • ❾ 没提错误率 (另外「性能提升一倍」的说法也不严谨:平均延迟减半 ≠ 吞吐翻倍。)
  2. 因为延迟统计通常只统计成功完成的请求。当一部分慢请求变成「超时/报错」后,它们就从延迟样本里消失了——剩下的是那些「又快又成功」的请求。于是 P99 看起来变好了,实际上系统是变差了(更多用户拿到了错误)。这就是为什么错误率必须和延迟一起看,且分母要用总请求数。
  3. ❻ 忽略预热。JVM 上代码要经历「解释执行 → C1 → C2」的升级过程,性能随时间变化,所以必须预热。C++ 代码编译产物固定,第一次调用和第一万次调用的性能基本一致,因此不存在预热问题。(代价是 JVM 有 JIT 的收益,C++ 没有——这是一个权衡,不是优劣。)