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