> 吞吐上限要报在 spread 收敛的地方,不是报在还能跑通的地方

Systems & Infrastructure Intermediate 2026-10-11 07:06 2026-10-09
#benchmarking #measurement #tail-latency #reproducibility #capacity-planning

压测到 3,000 cycles/s,服务跑完了 2,999 个——但能报的容量上限是 1,000。

结论先行

压测到 3,000 cycles/s,服务跑完了 2,999 个——但能报的容量上限是 1,000。

区别不在于服务撑不撑得住,而在于从哪一点开始,这个测量测的不再是服务:

offered p99 (ms) 五轮 p99 的范围 spread
250 0.70 0.65–0.71 1.1×
1000 0.73 0.71–0.89 1.3×
2000 1.48 1.44–6.05 4.2×
2500 6.91 2.87–29.01 10.1×
3000 9.55 3.23–17.99 5.6×

到 2,500 的时候,同一个二进制在两次运行之间的尾延迟差异, 比它在 250 → 2,000 之间的全部变化还大。 那条曲线此时描述的是一台共享机器的负载,不是这个服务的性质。

判据:报那个 spread 还收敛的点。 一个在未知争用下测出来的尾延迟, 不是可以拿来做容量规划的数。

为什么

单次测量看不出这件事——单看 2,500 那一行的 6.91 ms,它像一个数。 只有重复同一组压测若干轮、把范围列出来,才能看出它不是。

两个具体的反常现象,都是在重复之后才出现的(下面这两个数来自五轮汇总表之前的 两次预备 sweep,不是表内那一行的组成部分):

  • 同一份代码、同一台机器,连续两轮在 2,500 得到 14.9 ms 和 38.8 ms;
  • 有一轮 3,000 的结果比 2,500 更好——这不可能是服务的性质。

所以 spread 这一列不是"附加的严谨性",它是判断前一列是不是测量的唯一依据。 没有它,上面那张表读起来就是"容量 3,000,尾延迟 9.55 ms",而那句话是错的。

为什么诚实的那个数反而更有用

报 3,000 更好看,而且不算撒谎——服务确实处理了 2,999 个请求。 但拿 3,000 去做容量规划的人会按 9.55 ms 的尾延迟设计, 而那个数字在一台安静的机器上可能是 3 ms,在一台更忙的机器上可能是 30 ms。 他拿到的不是一个保守估计,是一个方差未知的估计。

报 1,000 的同时附上完整的表和 spread 列,读者能自己判断: "我的机器更安静,我可以重跑一遍"。 把判断权连同数据一起交出去,比替他做一个乐观判断要有用。

怎么用

跑容量压测时:

  1. 每个负载点至少重复 3–5 轮,报中位数和范围,不报单次。 一次抽样在这里不构成测量。
  2. 加一列 spread(max/min 或范围宽度)。它是全表唯一能自我证伪的列。
  3. 容量上限划在 spread 开始发散的前一个点,不是划在"还能跑完"的点。 经验阈值:spread 超过 ~2× 就要怀疑,超过 5× 基本可以认为在测环境。
  4. 把宿主机负载一起记进产物(load average、核数、是否独占)。 事后有人质疑时,这是唯一能分辨"服务退化"和"机器变忙"的证据。
  5. 同时记 CPU 占用——它通常比尾延迟稳定得多, 所以"CPU 线性上涨但尾延迟乱跳"本身就是"瓶颈不在被测服务"的证据。

写进报告的句式:

一个副本在 N 的时候 p99 低于 X,五轮之间的离散度在 1.3× 以内。 超过 M 之后离散度达到 10×,该区间的数字描述的是宿主机而不是服务; 原始产物在 bench/results/,有安静机器的人可以自己重跑。

出处

~/Engineering/Rate-Limiter-Service 的 bench/,一个副本、开环负载、 每请求一次完整的 reserve + commit。五轮扫描的原始输出逐个提交在 bench/results/ 下,所以表里每一行都能回到它读自的那个产物。 相关:预留不加租约,配额会单调归零——而且每一步决策都是对的,没有任何错误可查、你的 p90 和工具的 p90 不是一个 p90——差 31%,而且没人报错、 事先声明的偏差是限制,事后发现的偏差是借口。