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