文档目录

1.6 基线与容量曲线:拐点才是容量

上一节:1.5 延迟预算 | 下一节:1.7 实验元数据 配套代码:01-metrics-and-slo/06-baseline-and-capacity-curve


一句话结论

「CPU 打满时的 QPS」不是容量,「延迟开始非线性上升的那个点」才是容量。 前者是崩溃点,后者是拐点。容量规划要取拐点再打折,取崩溃点等于计划着让服务宕机。


一、用「高速公路」理解容量曲线

高速公路的通行能力,是一个非常好的类比:

车流量 表现
很少(20%) 你可以跑到 120 km/h,速度稳定
中等(50%) 还是 120 km/h,几乎不受影响
偏多(75%) 偶尔要减速,速度掉到 100 km/h
接近饱和(90%) 速度骤降到 40 km/h,开始走走停停
完全饱和(100%) 完全堵死,通行量反而下降

注意最后一行:车流再多,路的通行量反而下降(因为车距变小、刹车波传播)。这在交通工程里叫「相变」,在排队论里就是第 0 章讲的非线性。

关键洞察:

  • 你绝对不会说「这条路能通行 100% 的车流」——那意味着堵死。
  • 你会说「这条路在开始堵之前,每小时能过 2000 辆车」。
  • 那个「开始堵」的点,就是拐点(knee)。

后端服务完全一样:

  • QPS 低时,延迟平稳;
  • QPS 到某个点后,延迟开始非线性上升(拐点);
  • 继续加压,延迟爆炸、错误率上升(崩溃点);
  • 再加压,实际 QPS 反而下降(因为请求超时、重试、连接被占满)。

二、基线:没有参照物的数字没有意义

2.1 一个残酷的类比

你家的体重秤显示 70 kg。这个数字是好是坏?

  • 如果你上个月是 65 kg → 涨了 5 kg,需要关注。
  • 如果你上个月是 75 kg → 减了 5 kg,进展良好。
  • 如果你从来没有称过 → 你只知道「70 kg」,无法判断。

性能数字完全一样:

「接口 P99 = 180 ms」——在没有基线的情况下,这句话不包含任何信息。

2.2 基线的定义

基线 = 一组固定条件下的指标快照。「固定条件」包括:

  • 同一份代码(commit)
  • 同一套环境(机器规格、JVM 参数、数据库版本)
  • 同一份数据集(规模 + 分布)
  • 同一个压测脚本(同样在版本控制里)
  • 同样的预热与测量时长

基线的作用:

用途 说明
判断「是不是变慢了」 当前数据 vs 基线
证明「优化有没有效」 优化前 = 基线,优化后 = 新数据
发现渐进退化 每周跑一次,看趋势(最常见的性能问题形态)

没有基线就没有优化。 第 7 章会反复用到这一点:任何优化都必须有 before。


三、容量曲线怎么测

3.1 阶梯加压

QPS
 ↑
2000 ┤                                    ╭────  崩溃点
1500 ┤                            ╭───────╯
1000 ┤                  ╭─────────╯   ← 拐点在这里
 500 ┤      ╭───────────╯
 100 ┤──────╯
     └───────────────────────────────────────→ 时间
      每级维持 2–5 分钟稳态

做法:从低到高分若干级(例如 100 → 200 → 500 → 1000 → 2000 RPS),每级维持到稳态(2–5 分钟),记录每级的 P99、错误率、资源利用率、饱和度。

为什么要「维持到稳态」:刚升上去时队列是空的,延迟会短暂偏低;要等队列建立起来,才看到这一级的真实表现。

3.2 三种曲线形态与含义

「QPS → P99」曲线形态 含义 行动
平缓上升 资源还宽裕,瓶颈不明显 继续加压找拐点
在某个点突然抬头 这就是拐点,某类资源接近饱和 定位是哪类资源(饱和度指标)
一直很平,最后骤然断裂 有硬上限(连接数、文件描述符、线程数) 检查是否有配置天花板

3.3 同时必须观察的指标

只看 QPS 和 P99 是不够的,加压过程中还要看哪类饱和度先动:

现象 提示的瓶颈
连接池 pending 先上升 数据库连接或查询速度
线程池/调度器队列先上升 CPU 饱和或阻塞调用
CPU 先上升(尤其 sys) 计算密集、锁竞争、系统调用
GC 停顿先上升 分配速率过高
磁盘 await 先上升 IO 瓶颈
什么都不高但延迟上升 排队在网关/上游,或下游依赖慢

四、从拐点到容量规划

4.1 公式

单实例安全容量 = 拐点 QPS × (1 − headroom)

headroom 通常取 30%–50%

为什么留 headroom(第 0 章 0.5 节已给出数学依据):

  • 排队是非线性的:利用率 90% 时,排队时间已经是服务时间的 9 倍;
  • 突发流量不可控(重试风暴、缓存集中失效、定时任务、上游抖动);
  • 需要吸收 GC 停顿、JIT 编译、日志轮转等内部抖动;
  • 需要应对实例故障(少一台时其余要顶上)。

4.2 完整推导示例

① 压测发现:4C8G 单实例在 900 RPS 时 P99 开始非线性上升(拐点 = 900)
② 取 headroom 40%:安全容量 = 900 × 0.6 = 540 RPS
③ 峰值流量预估:3000 RPS
④ 所需副本 = ceil(3000 / 540) = 6
⑤ 加上 N+1 冗余(坏一台不影响服务)= 7

4.3 三个必须注意的非线性因素

因素 说明
副本翻倍 ≠ 容量翻倍 共享的数据库、Redis、下游服务会成为新瓶颈
数据量增长会移动拐点 数据从 100 万涨到 1000 万,索引效率变化,拐点可能从 900 降到 600
headroom 要重新评估 业务形态变化(比如新增一个重型接口)会改变排队特性

结论:容量不是一次测完就固定的,要用真实峰值定期复测。


五、本节小结

  1. 基线 = 固定条件下的一组指标快照。没有基线,任何数字都无法解读。
  2. 拐点才是容量:延迟开始非线性上升的那个点;崩溃点只是「服务已经不行了」。
  3. 容量规划:安全容量 = 拐点 × (1 − headroom),headroom 常用 30%–50%。
  4. 找拐点时不能只看 QPS 和 P99,要同时观察哪类饱和度先动——那才是瓶颈所在。
  5. 副本翻倍不等于容量翻倍;数据量增长会移动拐点;容量要定期复测。

六、自测

  1. 压测发现单实例在 1200 RPS 时 CPU 打到 100%,此时 P99 是 800 ms。团队说「我们的容量是 1200 RPS」。这句话有什么问题?
  2. 为什么「每级维持 2–5 分钟稳态」很重要?如果每级只跑 30 秒会得到什么错误结论?
  3. 一个服务的拐点从半年前的 900 RPS 降到了现在的 600 RPS,代码没变。可能的原因有哪些?
  1. ① 1200 RPS 是崩溃点而非拐点——此时 P99 已经 800 ms,服务实际已经不可用,不能作为容量。② 没有说明拐点在哪:应该找出「P99 开始非线性上升」的那个 QPS(可能只有 600–700)。③ 没有应用 headroom:即使拐点是 1200,安全容量也只有 720–840。④ 缺少基线条件(什么数据量、什么分布、测于哪个测量点)。正确说法:「4C8G 单实例在 X 万行数据、幂律分布下,拐点约 700 RPS;按 40% headroom,安全容量 420 RPS。」
  2. 因为刚提升负载时,队列是空的、缓存可能是热的,延迟会短暂偏低;需要一段时间让队列建立、缓存进入真实的命中/未命中混合状态、GC 周期走过至少一轮,才能看到该负载下的稳态表现。只跑 30 秒的后果:① 数据可能落在 JVM 预热期,P99 虚高;② 也可能因为队列还没建立而虚低,误判为「这个负载很轻松」;③ 完全测不出周期性的 GC 停顿与缓存失效。
  3. ① 数据量增长:从 100 万涨到 1000 万,原本走索引的查询可能因为统计信息过期或索引选择性下降而变慢;② 数据分布变化:热点更集中,锁竞争或缓存击穿加剧;③ 依赖变慢:数据库版本升级、下游服务新增逻辑、共享资源被其他服务抢占;④ 配置漂移:JVM 参数、连接池大小、容器 CPU limit 被改过;⑤ 环境变化:实例规格换了、宿主机邻居变多(steal 时间上升)、磁盘从 SSD 换成网络盘。排查方法:对比当前的元数据清单(第 1.7 节)与半年前的记录,找出差异项。