57.2 容量模型
一句话
容量不是“平时够用”,而是**“峰值 × 冗余 × 增长”三件事乘出来的数**——少乘任何一项,都会在某个具体的时间点(大促、故障、半年后)还债。
一个公式
节点数 = ceil( 峰值QPS × (1 + 余量) / 单节点安全容量 )
再叠加:故障冗余(N+1)、可用区打散、未来增长
| 参数 | 怎么取 | 常见错误 |
|---|---|---|
| 峰值 QPS | 历史峰值 × 2(或压测拐点) | 用日均值 |
| 余量 | 20–30% | 压到 90% 利用率 |
| 单节点安全容量 | 压测容量的 70%(留抖动空间) | 用理论值 |
| 故障冗余 | N+1(至少) | 按 N 配,挂一台就过载 |
代码:从峰值到节点数
package main
import (
"fmt"
"math"
)
func main() {
type plan struct {
name string
peakQPS float64
perNode float64 // 单节点压测容量
headroom float64 // 余量
redundant int // 额外冗余节点
azs int // 可用区数
}
plans := []plan{
{"按日均值配", 800, 1000, 0.1, 0, 1},
{"按峰值配", 3000, 1000, 0.2, 0, 1},
{"峰值 + 余量 + N+1", 3000, 1000, 0.2, 1, 3},
{"峰值×2 + 余量 + N+1 + 3AZ", 6000, 1000, 0.2, 1, 3},
}
fmt.Printf("%-30s %-10s %-10s %-10s %s\n", "方案", "峰值", "需要节点", "实际节点", "说明")
for _, p := range plans {
need := math.Ceil(p.peakQPS * (1 + p.headroom) / (p.perNode * 0.7))
total := int(need) + p.redundant
// 3AZ:每 AZ 至少 1 个,且尽量均匀
if p.azs > 1 && total < p.azs {
total = p.azs
}
note := ""
switch p.name {
case "按日均值配":
note = "大促当天直接过载"
case "按峰值配":
note = "挂一台就超载,且没有余量吸收抖动"
case "峰值 + 余量 + N+1":
note = "单 AZ 故障仍可服务"
case "峰值×2 + 余量 + N+1 + 3AZ":
note = "最稳,成本也最高"
}
fmt.Printf("%-30s %-10.0f %-10.0f %-10d %s\n", p.name, p.peakQPS, need, total, note)
}
fmt.Println("\n=== 利用率与延迟的关系(排队论直觉)===")
fmt.Printf("%-10s %-14s %s\n", "利用率", "排队放大", "说明")
for _, u := range []float64{0.5, 0.7, 0.8, 0.9, 0.95} {
factor := 1 / (1 - u) // M/M/1 的等待放大
note := ""
if u >= 0.9 {
note = "轻微抖动就雪崩"
}
fmt.Printf("%-10.0f%% %-14.1f %s\n", u*100, factor, note)
}
}
本机实测(macOS,Apple M1,Go 1.27):
方案 峰值 需要节点 实际节点 说明
按日均值配 800 2 2 大促当天直接过载
按峰值配 3000 6 6 挂一台就超载,且没有余量吸收抖动
峰值 + 余量 + N+1 3000 6 7 单 AZ 故障仍可服务
峰值×2 + 余量 + N+1 + 3AZ 6000 11 12 最稳,成本也最高
=== 利用率与延迟的关系(排队论直觉)===
利用率 排队放大 说明
50 % 2.0
70 % 3.3
80 % 5.0
90 % 10.0 轻微抖动就雪崩
95 % 20.0 轻微抖动就雪崩
读数:
| 方案 | 节点数 | 后果 |
|---|---|---|
| 按日均值 | 2 | 大促当天过载 |
| 按峰值 | 6 | 挂一台超载 |
| 峰值+余量+N+1 | 7 | 可扛单节点故障 |
| 峰值×2+余量+N+1+3AZ | 12 | 可扛区域级波动 |
🔑 利用率的真相:70% 是舒适区,80% 开始敏感,90% 以上排队放大 10 倍。所以“单节点安全容量”要按压测值的 70% 算,而不是“压到 100% 时能扛多少”。
容量模型要包含的四类资源
| 资源 | 是否容易成为瓶颈 | 说明 |
|---|---|---|
| CPU | 常见 | 计算密集、序列化 |
| 内存 | 常见 | 缓存、JVM 堆、连接缓冲 |
| 连接数 | 隐蔽 | 数据库连接、下游连接池 |
| 带宽/IOPS | 隐蔽 | 大对象、日志、备份 |
⚠️ 只算 CPU 是最常见的错误:一个服务 CPU 40%,但数据库连接池已满,容量实际到顶了。
反模式
| 反模式 | 后果 |
|---|---|
| 用日均值做容量规划 | 大促必崩 |
| 按“压测最大值”配节点 | 没有余量吸收抖动 |
| 只算应用节点,不算 DB/缓存 | 应用扩了,DB 先挂 |
| 不留故障冗余 | 单机故障即全站降级 |
| 容量一次算完就不管 | 业务增长半年后重新踩坑 |
| 不记录容量假设 | 后来的人不知道为什么是 12 台 |
自检
Q1 为什么“单节点安全容量”要取压测值的 70%,而不是压测峰值?
▸ 答案:因为排队系统的延迟随利用率非线性上升:利用率 70% 时等待放大 3.3 倍,90% 时 10 倍,95% 时 20 倍(M/M/1 的 1/(1-ρ))。压测到 100% 时“还没挂”,但生产中任何抖动(GC、慢查询、网络重传)都会把它推过 100%,直接雪崩。70% 留出的不是“浪费”,是吸收抖动的缓冲。
Q2 为什么容量模型必须包含 N+1?
▸ 答案:因为没有冗余的容量在故障时等于“容量除以剩余节点数”。N 台按 70% 利用率配,挂一台后剩 N-1 台要承担全部流量,利用率瞬间超过 100%,引发雪崩。N+1 保证“挂一台后剩余节点仍能扛住峰值”,这是可用性设计的底线(57.3)。
Q3 一个服务 CPU 只有 40%,为什么可能已经到容量上限了?
▸ 答案:因为瓶颈可能在别处:数据库连接池满(等待)、下游接口限流(排队)、磁盘 IOPS 打满、网络带宽打满、GC 停顿。CPU 只是“计算”这一维;容量是木桶的最短板。判断方法:看 P99 延迟与队列长度——如果 QPS 上不去而 CPU 不高,瓶颈几乎一定在 IO 或下游。
Q4 Little 定律怎么用来估并发?
▸ 答案:并发 = 到达速率 × 平均停留时间(L = λW)。例如 1000 QPS、每个请求平均 50ms,则并发约 50;这决定了线程池/连接池的下限。注意要用 P95/P99 的停留时间做保守估计,否则高峰期会排队。
Q5 为什么 “按峰值 QPS × 2” 估容量通常浪费?
▸ 答案:因为峰值往往只持续很短时间,而容量是按 “长期可承载” 买的:按峰值翻倍买,等于为几分钟的尖峰付整月费用。更合理的是 “基线容量 + 弹性伸缩 + 排队/降级”,用弹性覆盖尖峰,用降级兜住极端情况。
关联
- 前置:57.1 压测方法论、34.2 性能建模 (利特尔法则、排队论)
- 并列:37.1 垂直扩展与水平扩展、57.3 冗余系数
- 下游:57.4 FinOps 实践、[39.2 容量规划](待补)
- 待补:39.2 容量规划(未写,无链接)