容量问题通常在两种时刻暴露:一次营销活动把 QPS 打到平时的五倍,或者某个表悄悄涨到两亿行,然后某天凌晨接口开始超时。两次都不是"突然",而是容量从来没被算过。
容量规划不是写一张好看的数字表,它要回答三个可操作的问题:现在能扛多少、什么时候会不够、扩容要多久生效。三个问题里最容易被忽略的是第三个——扩容时延决定了你必须在多早之前动手。
1. 三个问题与它们的输入
| 问题 | 需要的数据 | 输出形态 |
|---|---|---|
| 现在能扛多少 | 单实例容量 × 实例数 × 冗余扣减 | 当前可承载 QPS / 写入量 |
| 什么时候不够 | 流量增长趋势 + 峰值系数 | 触达阈值的时间点 |
| 扩容要多久 | 资源申请 + 启动 + 预热 + 导流 | 提前量(分钟/小时/天) |
三个问题的输出必须能写成一句话:"当前容量 X,按当前增速在 T 之后触顶,扩容生效需要 L,所以必须在 T−L 之前启动。" 写不出这句话,说明规划还没做完。
2. 四个输入
| 输入 | 具体内容 | 常见错误 |
|---|---|---|
| 流量模型 | 均值、日峰、月峰、季节性、突发形态 | 只看日均,峰值按 1.5 倍拍 |
| 单请求消耗 | CPU 时间、内存、IO、下游调用次数 | 用"经验值",不测 |
| 冗余要求 | N+1 / N/2+1 / 跨可用区 | 只按 N 算,不留冗余 |
| 扩容时延 | 从决策到真正分担流量 | 假设"加机器就立刻生效" |
四个输入缺一个,容量结论就不可信。尤其是冗余要求:按 N 算出来的容量,任何一台故障都会导致过载,而过载会引发雪崩式连锁。
3. 流量模型的建立
均值与峰值的关系不是固定倍数,而是由业务形态决定。同一系统的工作日峰值可能是均值的 3 倍,大促峰值可能是均值的 20 倍。
| 形态 | 特征 | 规划要点 |
|---|---|---|
| 常态波动 | 日内双峰(午间、晚间) | 按日峰设计,冗余 1.3~1.5 倍 |
| 周期性季节 | 周末/月末/季度末偏高 | 找历史同期最高值,不用近 7 天 |
| 可预测突发 | 活动、开抢、定时任务 | 按预案峰值 + 预热,不靠弹性硬扛 |
| 不可预测突发 | 舆情、故障转移、重试风暴 | 需要限流与降级兜底 |
峰值系数 = 峰值 QPS / 均值 QPS # 按最近 4 周同一时段核对,不要用全年平均
设计容量 = 峰值 QPS × 安全系数 # 安全系数按业务可容忍的降级程度定
增长外推 = 近 3 个月复合增长率(去掉单次活动带来的尖峰后)
两个细节:增长外推要剔除一次性活动,否则会把营销脉冲当成趋势;峰值系数要按同一时段比,拿凌晨均值除晚间峰值没有意义。
4. 单请求资源消耗的度量
| 资源 | 度量方式 | 关键点 |
|---|---|---|
| CPU | 单请求 CPU 时间(用户态 + 内核态) | 用压测或 profile 采样,不用"感觉" |
| 内存 | 常驻 + 每请求临时分配 | 关注 GC 压力,不只是峰值 |
| IO | 每请求读写字节数、IOPS | 区分顺序/随机,二者成本差一档 |
| 下游调用 | 每请求调用次数与耗时占比 | 下游容量往往才是真瓶颈 |
单实例容量 ≈ 可用 CPU 核数 / 单请求 CPU 时间 # 理想上限,实际按 60%~70% 折算
并发需求 ≈ 目标 QPS × 单请求平均耗时(秒) # Little 定律
下游压力 ≈ 目标 QPS × 每请求下游调用次数
第三条最常被漏算:应用能扩,数据库连接池与下游接口不一定能按同样比例扩。每请求调用次数乘以 QPS,才是下游真正要承受的量。
5. 瓶颈的判定顺序
容量规划的核心不是"算总量",而是找最紧的那一环。扩容非瓶颈环节,投入全打水漂。
判定顺序(从最容易饱和的开始逐层排除):
1. 接入层:连接数、带宽、TLS 握手 CPU
2. 应用层:CPU、GC、线程池队列长度、超时率
3. 数据层:连接池占用、慢查询、锁等待、复制延迟
4. 下游依赖:限流阈值、超时率、重试放大
5. 基础设施:磁盘 IOPS/吞吐、网络、文件句柄
| 现象 | 更可能是瓶颈在哪 | 验证方式 |
|---|---|---|
| CPU 高且队列积压 | 应用层计算 | 看线程池队列与 GC 时间占比 |
| CPU 不高但延迟线性上升 | 连接池/锁/IO 等待 | 看等待时间占比、连接池 active 数 |
| 应用扩容后延迟不变 | 数据层或下游 | 对比扩容前后下游耗时 |
| 高峰期超时集中在少数接口 | 下游限流或慢查询 | 按接口看 P99 与下游调用耗时 |
关键判断:扩容后指标不变,说明瓶颈不在被扩容的那一层。 这时继续加机器只会增加成本。
6. 垂直与水平扩容
| 方向 | 做法 | 上限与限制 |
|---|---|---|
| 垂直(scale up) | 加 CPU/内存/更大机型 | 有单机上限;重启成本高;单点风险不变 |
| 水平(scale out) | 加实例 | 要求无状态;需要负载均衡与会话外置 |
水平扩容的隐性前提容易被忽略:
- 无状态:会话、本地缓存、上传临时文件必须外置;
- 可均衡:长连接、粘性会话会让流量分布不均;
- 可并发:定时任务、批处理在多实例下会重复执行,需要分布式锁或选主。
水平扩容前置检查:
[ ] 会话是否外置(Redis/DB,而不是本地内存)
[ ] 是否有本地缓存需要预热
[ ] 定时任务是否有选主/锁
[ ] 是否依赖本机文件或本机 IP
[ ] 启动时是否有全量加载(决定预热时间)
垂直扩容适合"有状态、难拆分"的组件,但它买到的是时间,不是架构——单机上限到了还是得拆。
7. 扩容时延的构成
扩容不是"点一下按钮"。真正的生效时间由五段串起来,任何一段被忽略,预算就会失准。
| 阶段 | 典型量级(按环境核对) | 拖长的原因 |
|---|---|---|
| 申请资源 | 分钟级(预留池)到小时级(云上竞价/审批) | 配额、审批、库存 |
| 启动进程 | 秒级到分钟级 | 依赖初始化、类加载 |
| 预热 | 分钟级 | JIT、连接池、本地缓存、字典加载 |
| 注册与健康检查 | 秒级到分钟级 | 健康检查周期、就绪探针 |
| 流量导入 | 秒级到分钟级 | 负载均衡权重调整、连接重建 |
扩容生效时间 = 申请 + 启动 + 预热 + 注册 + 导流
# 若预热需要预热脚本/回放流量,单独计入
# 若走弹性伸缩,还要加"指标触发延迟"(监控采集 + 判定周期)
预热为什么不能省:刚启动的实例没有 JIT 编译、没有连接池、没有本地缓存,直接承接全量流量会表现为"新实例反而更慢",触发更多超时与重试,形成恶性循环。
8. 弹性策略
| 策略 | 触发依据 | 优点 | 局限 |
|---|---|---|---|
| 按指标弹性 | CPU/队列长度/QPS 超阈值 | 自动化,成本可控 | 滞后:流量先到、扩容后到 |
| 预留缓冲 | 常备冗余实例 | 无滞后 | 成本固定 |
| 预测式扩容 | 历史周期 + 日历(活动/大促) | 提前量足 | 依赖历史准确性 |
| 手动预案 | 活动前人工扩容 | 可控 | 依赖人记得 |
按指标弹性的滞后是结构性的:指标要采集、要跨过判定周期、要等实例就绪与预热,这几段加起来通常远大于一次流量尖峰的上升时间。所以弹性适合"缓坡",不适合"陡峰"。
可操作的组合:常备冗余覆盖日常波动 + 预测式覆盖已知活动 + 限流降级兜住未知陡峰。弹性只作为收尾微调。
9. 成本与冗余:N+1 与 N/2+1
| 冗余模型 | 含义 | 故障时剩余容量 | 成本 |
|---|---|---|---|
| N | 刚好覆盖峰值 | 0,任一实例故障即过载 | 最低,风险最高 |
| N+1 | 多一台 | 可承受 1 台故障 | 中 |
| N/2+1 | 按半数冗余 | 可承受近半故障 | 高 |
| 跨可用区 | 每个 AZ 独立满足 N+1 | 可承受单 AZ 故障 | 最高(含跨区流量成本) |
例:峰值需要 10 台(N=10)
N+1 → 11 台,单台故障后仍余 10 台容量
N/2+1 → 10 台里最多容忍 5 台故障(剩余 5 台需能扛峰值 → 需按单台双倍容量设计)
跨 AZ → 2 个 AZ 各 11 台 = 22 台(成本约翻倍,另有跨区延迟与流量费)
跨可用区的额外开销不只是机器数量:跨区调用延迟、跨区流量费、数据同步带宽都要计入。很多方案在纸面上只算了机器钱,上线后账单翻倍。
10. 压测与容量台账
压测是容量结论的唯一验证手段,不是上线前的仪式。它要回答三个具体问题:单实例极限在哪、扩容是否线性、瓶颈在扩到第几台时转移。
压测设计要点:
1. 阶梯加压:每档稳定运行 ≥5 分钟,观察是否进入稳态
2. 记录拐点:延迟开始非线性上升的那个 QPS,就是实际容量上限
3. 验证线性:扩 2 倍实例,容量是否接近 2 倍(不接近说明有共享瓶颈)
4. 观察下游:压测期间同步看数据库、缓存、第三方接口的指标
5. 记录降级点:限流/熔断触发时的 QPS 与错误率
容量台账把结论沉淀成可复核的表,而不是散落在聊天记录里:
| 字段 | 例 |
|---|---|
| 服务/接口 | order-api / 列表查询 |
| 单实例容量 | 按最近一次压测核对 |
| 实例数 | 当前 / 冗余 |
| 峰值水位 | 最近 4 周最高 |
| 触顶预估 | 按增长率外推 |
| 扩容时延 | 分钟级(含预热) |
| 复核日期 | 每季度或大活动前 |
11. 常见坑
| 坑 | 后果 | 改法 |
|---|---|---|
| 按均值算容量 | 峰值直接打爆 | 用峰值系数 × 安全系数 |
| 扩了应用没扩数据库 | 应用空转,延迟不变 | 按瓶颈顺序逐层验 |
| 忽略下游限流 | 自己扩了却被下游挡住 | 把下游容量纳入台账 |
| 弹性启动时间 > 降级窗口 | 扩容还没生效,用户已经跑了 | 常备冗余 + 预测式 |
| 按 N 设计冗余 | 一台故障即雪崩 | 至少 N+1 |
| 压测只压单接口 | 混合负载下的瓶颈没暴露 | 按真实流量配比压 |
12. 排错:五种"扩了但没用"
| 现象 | 原因 | 处理 |
|---|---|---|
| 加实例后 QPS 不涨 | 存在共享瓶颈(DB/锁/单点服务) | 找共享资源,先解耦 |
| 新实例延迟比老实例高 | 没预热 | 加预热、延迟导流、就绪探针 |
| 高峰期扩容来不及 | 时延预算算错 | 改预测式 + 常备冗余 |
| 扩容后错误率反升 | 连接池/线程池未按实例调小,压垮下游 | 按实例数重算连接池上限 |
| 成本涨了容量没涨 | 扩的是非瓶颈层 | 回到瓶颈判定顺序 |
13. 边界:容量规划不解决什么
| 问题 | 能给答案吗 | 该用什么 |
|---|---|---|
| 某个慢查询怎么优化 | 不能(只给现象) | 执行计划分析、索引设计 |
| 架构是否需要重构 | 不能(只给压力证据) | 架构评审 |
| 成本该由谁承担 | 不能 | 成本分摊与账单标签 |
| 突发流量的具体形态 | 只能估 | 限流、降级、混沌演练 |
一句话:容量规划给出"需要多少",不给出"怎么变快"。 后者是性能优化与架构的事。
14. 自测
- 容量规划要回答的三个问题是什么?为什么"扩容要多久"最容易被忽略却最致命?
- 峰值系数为什么不能拿全年平均去算?增长外推要怎么剔除一次性活动?
- 每请求下游调用次数为什么必须乘以目标 QPS?漏算会导致什么?
- 扩容后指标完全不变,说明什么?下一步该查哪一层?
- 水平扩容的三个隐性前提是什么?
- 按指标触发的弹性扩容为什么不适合陡峰?替代组合是什么?