文档目录

8.7 容量规划

上一节:8.6 流量回放与金丝雀发布 | 下一节:8.8 性能评审与组织落地 配套代码:08-continuous-performance/07-capacity-planning


一句话结论

容量 = 拐点 × (1 - headroom),不是「CPU 打满时的 QPS」。 而且容量不是一次测完就固定的——数据量增长、依赖变化、业务形态变化都会移动拐点。


一、用「餐厅订桌」理解容量规划

一家餐厅要决定「今晚准备多少食材」:

依据 问题
按「平均每天 50 桌」备料 ❌ 周末 120 桌时不够
按「历史最高 200 桌」备料 ❌ 平时浪费严重
按「上座率的临界点」备料 ✅ 超过这个点服务质量会崩

「上座率的临界点」就是拐点:

60% 上座率 → 服务员从容,出菜快,翻台正常
80% 上座率 → 开始排队,出菜变慢
95% 上座率 → 厨房崩溃,所有人等很久
100%       → 实际上已经瘫痪(等位的人走了,实际营业额反而下降)

所以备料的目标不是「撑到 100%」,而是「撑到 80% 且留有余量」。

后端完全一样。


二、从拐点到副本数

四步推导

① 压测找拐点
   → 4C8G 单实例在 900 RPS 时 P99 开始非线性上升
   → 拐点 = 900 RPS

② 应用 headroom
   → 安全容量 = 900 × (1 - 0.4) = 540 RPS
   → headroom 通常 30%~50%

③ 按峰值流量计算副本数
   → 峰值 = 3000 RPS
   → 副本数 = ceil(3000 / 540) = 6

④ 加冗余
   → N+1 冗余(坏一台不影响服务)
   → 最终副本数 = 7

headroom 该留多少

依据是排队论(第 0 章 0.5 节):

利用率 排队时间倍数 评价
50% 1× 很保守(成本高)
60%–70% 1.5–2.3× 推荐区间
80% 4× 开始吃紧
90% 9× 危险

换算成 headroom:

headroom = 1 - 目标利用率
目标利用率 60% → headroom = 40%
目标利用率 70% → headroom = 30%

headroom 要覆盖的东西:

要覆盖的 说明
突发流量 重试风暴、活动峰值、爬虫
内部抖动 GC 停顿、JIT 编译、日志轮转
实例故障 少一台时其余要顶上(这就是 N+1)
数据量增长 拐点会随时间下降
部署期间 滚动发布时新实例是冷的

三、必须考虑的非线性因素

「副本翻倍 = 容量翻倍」是错的。

因素 影响
共享数据库 所有副本打同一个库——数据库可能先到瓶颈
共享缓存 Redis 的 QPS 上限、连接数上限
下游服务 你可能把压力转给了下游
锁竞争 多副本争抢同一批行锁(比如库存扣减)
数据量增长 索引效率变化,拐点下移
连接数总和 6 实例 × 30 连接 = 180,可能超过 DB 的 max_connections

一个真实的扩容失败

① 单实例拐点 900 RPS,峰值 3000 RPS
② 从 4 台扩到 8 台(理论上能扛 7000 RPS)
③ 结果:扩容后 P99 反而上升了

原因:
  - 8 台 × 30 连接 = 240 个数据库连接 > max_connections(100)
  - 部分实例连不上数据库 → 报错 + 重试 → 压力更大
  - 数据库的锁竞争加剧(8 台的并发事务争抢同一批行)

教训:扩容前先确认瓶颈不在共享资源上。

扩容后的验证

扩容后必须复测:
  ① 拐点是否真的提升了?(可能只是从 900 → 1100,而不是翻倍)
  ② 数据库侧指标是否恶化?(连接数、锁等待、IO 队列)
  ③ 饱和度是否正常?(pending、队列深度)

四、容量表

| 实例规格 | 拐点(实测) | headroom | 安全容量 | 峰值需求 | 所需副本 | 数据来源 | 测量日期 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 4C8G | 900 RPS | 40% | 540 RPS | 3000 | 6+1=7 | E02 | 2025-01-10 |
| 8C16G | 1600 RPS | 40% | 960 RPS | 3000 | 3+1=4 | E05 | 2025-01-15 |

**约束条件**:
- 数据库 max_connections = 100
  → 4C8G 方案:7 × 30 = 210 ❌ 超出!需要降池或加 PgBouncer
  → 8C16G 方案:4 × 30 = 120 ❌ 仍超出
  → 最终决定:每实例池 ≤ 20(4 × 20 = 80 ✅)+ PgBouncer

**非线性因素**:
- 拐点会随数据量增长下移(每翻倍约降 10%~20%,需每季度复测)
- 共享 Redis 的 QPS 上限约 50000(当前峰值 30000)
- 下游 user-service 的容量未验证(待补充)

注意「约束条件」这一栏:它常常否决掉看起来最优的方案——上面的例子中,纯粹按 QPS 算 8C16G 更划算(4 台 vs 7 台),但数据库连接数成了真正的约束。


五、定期复测(容量不是一次性的)

什么时候必须复测

触发条件 为什么
每季度 数据量增长会移动拐点
数据量翻倍 执行计划可能变化
大版本发布后 代码变化可能改变性能特征
依赖升级(DB/JDK/中间件) 性能特征可能变化
架构变更(加缓存、拆服务) 拐点会移动
业务形态变化(新增大接口) 负载特征变化

复测的简化版

不必每次都跑完整的阶梯加压(成本高):

简化版复测(30 分钟):
  ① 在「当前安全容量」的负载下跑 10 分钟
  ② 检查:P99 是否仍达标、pending 是否为 0、GC 是否正常
  ③ 如果指标变差 → 跑完整的阶梯加压找新拐点
  ④ 如果指标正常 → 记录本次结果,下次再测

完整版复测(2 小时):
  ① 阶梯加压找拐点
  ② 更新容量表
  ③ 重新计算副本数

「简化版」的价值:用 30 分钟确认「容量没有明显退化」——这比不测好得多,而且成本低到可以每季度做。


六、容量告警

容量本身也该有告警——在真正打满之前预警。

告警 阈值 含义
峰值 QPS 接近安全容量 > 安全容量 × 0.8 需要提前扩容
数据库连接使用率 > 70% 连接可能成为瓶颈
拐点下移 季度复测发现拐点降 > 20% 需要重新评估容量
单实例 CPU 峰值 持续 > 70% 接近排队区

最关键的是第一条:

# 峰值 QPS 与安全容量的比值(需要把安全容量作为一种"配置"记录进指标)
max_over_time(sum(rate(http_requests_total[5m]))[1h:5m]) / 安全容量

这条告警的提前量通常是几天到几周——足够安排扩容(而不用半夜救火)。


七、本节小结

  1. 容量 = 拐点 × (1 - headroom),不是「CPU 打满时的 QPS」。
  2. headroom 通常 30%–50%,依据是排队论(利用率 90% 时排队是服务时间的 9 倍)。
  3. headroom 要覆盖:突发流量、内部抖动、实例故障、数据量增长、部署期间。
  4. 必须考虑非线性因素:共享数据库/缓存/下游、锁竞争、数据量增长、连接数总和。
  5. 真正的约束常常不是 QPS——比如数据库的 max_connections 会否掉看起来最优的方案。
  6. 容量不是一次性的:数据量翻倍、依赖升级、架构变更后都要复测。
  7. 简化版复测(30 分钟)比不测好得多,而且成本低到可以每季度做。
  8. 容量本身要有告警(峰值 QPS 接近安全容量时提前预警)。

八、自测

  1. 单实例拐点是 1200 RPS,峰值需求 4000 RPS,headroom 取 40%。请算出所需副本数(含 N+1 冗余)。
  2. 你把副本数从 4 台扩到 8 台,但 P99 反而恶化了。请说出至少三个可能的原因。
  3. 为什么「简化版复测」(在当前安全容量下跑 10 分钟)比「不测」好得多?请说明它的价值与局限。
  1. 计算:① 安全容量 = 1200 × (1 - 0.4) = 720 RPS;② 所需副本 = ceil(4000 / 720) = ceil(5.56) = 6 台;③ 加 N+1 冗余 = 7 台。补充说明:① 这个计算假设「副本线性扩展」——但实际上必须确认瓶颈不在共享资源(数据库、缓存、下游)上;② 要检查连接数总和:7 × 每实例池大小是否超过数据库的 max_connections;③ headroom 40% 的选择要结合业务特性——如果流量波动大(有大促),可能需要更高的 headroom。
  2. 三个可能的原因:① 共享数据库成为瓶颈——8 台应用意味着 8 倍的并发查询打到同一个数据库,可能导致锁竞争加剧、IO 队列变长、CPU 打满,所有查询都变慢;② 数据库连接数超出上限——比如 8 × 30 = 240 个连接,而 max_connections = 100,导致部分实例连不上数据库,触发错误与重试,进一步加剧压力;③ 锁竞争加剧——多副本争抢同一批行锁(比如库存扣减、计数器更新),并发度提高反而让等待时间非线性增长(第 0 章 0.5 节);④ 第四种可能:下游服务被打垮——8 台的并发把压力转给了下游(第 7.3 节);⑤ 也有可能是负载均衡不均(新实例是冷的、连接分配不均)。验证方法:扩容后同时看数据库侧指标(连接数、锁等待、pg_stat_activity)与各实例的延迟分布(是否均匀)。
  3. 价值:① 成本低(30 分钟 vs 完整复测的 2 小时),所以真的会去做——而不测的容量表会在半年内失效;② 能发现明显退化——如果当前安全容量下 P99 已经超标、或 pending > 0、或 GC 异常,说明容量已经不够了,立刻就有结论;③ 提供趋势数据——即使每次都"通过",连续几个季度的 P99 与饱和度曲线也能看出缓慢劣化(第 8.1 节的渐进退化);④ 触发完整复测的信号——简化版失败时再跑完整版找新拐点,资源用得其所。局限:① 它不能发现拐点的具体位置——只能确认"在当前负载下是否正常";② 如果安全容量本身已经算错(比如之前测的拐点不准),简化版也发现不了;③ 它假设负载特征没变——如果新增了一个"更重"的接口,当前 QPS 下的实际压力可能已经不同了。所以:简化版适合季度例行检查,完整的阶梯加压适合重大变更后。