土法炼钢兴趣小组的算法知识备份

【系统架构设计百科】容量规划:从拍脑袋到数据驱动

文章导航

分类入口
architecture
标签入口
#capacity-planning#queueing-theory#load-testing#resource-modeling#baseline

目录

大促前两周,运维问架构师:“这次需要加多少台机器?”架构师打开去年大促的监控数据看了看,说:“去年 100 台顶住了,今年流量预计翻倍,加到 200 台吧。”运维追问:“内存要多大?磁盘要 SSD 还是 HDD?数据库连接池开多少?”架构师沉默了三秒,说:“先按去年配置来,不够了再加。”

大促当天,应用服务器 CPU 使用率只有 40%,但数据库连接池打满了——200 台应用服务器同时向数据库发起连接,远超数据库的最大连接数。紧急扩容数据库连接数后,数据库 CPU 飙到 95%,因为连接数增加导致并发查询暴增。最终靠临时关闭非核心功能才撑过峰值。

这个场景的根源不是”机器不够”,而是没有容量模型。容量规划(Capacity Planning)不是”估一下要多少台机器”,而是一个从需求预测到资源建模到验证反馈的完整工程过程。

本文要回答三个问题:

  1. 排队论(Queueing Theory)在容量规划中怎么用?Little 定律和 M/M/c 模型能给出什么?
  2. 容量基线(Baseline)和水位线(Waterline)怎么建、怎么管?
  3. 全链路压测怎么设计,才能真正验证容量模型而不只是”跑个数”?

上一篇中,我们讨论了 SLO 工程——如何量化管理系统的可靠性。容量规划是 SLO 的前置条件:如果容量不足,再好的 SLO 也无法达标。


一、为什么”加机器”不是容量规划

“流量涨了就加机器”——这个策略在简单场景下有效,但随着系统复杂度增加,它的局限性会迅速暴露。

瓶颈不在你以为的地方

一个典型的请求链路涉及多个组件:负载均衡 → API 网关 → 应用服务 → 缓存 → 数据库 → 消息队列。每个组件都有自己的容量上限,系统整体容量取决于最窄的那个瓶颈。

你给应用服务加了 10 台机器,但瓶颈在数据库连接池(最多 500 个连接),那 10 台新机器只会让更多请求排队等待数据库连接,延迟反而增加。

资源之间的耦合

CPU、内存、磁盘 I/O、网络带宽之间不是独立的。增加并发线程数可以提高 CPU 利用率,但也会增加内存占用和上下文切换开销。当内存不足时,操作系统开始使用 Swap,磁盘 I/O 急剧增加,CPU 花在 I/O 等待上的时间增多,吞吐量反而下降。

非线性扩展

大多数系统的性能不会随资源线性增长。根据 Amdahl 定律(阿姆达尔定律):

加速比 = 1 / ((1 - P) + P / N)

P = 可并行化的比例
N = 处理器数量

如果一个系统 80% 的工作可以并行化(P = 0.8),那么即使你加无限台机器,理论最大加速比也只有 1 / (1 - 0.8) = 5 倍。剩余 20% 的串行部分(数据库写锁、分布式锁、全局序号生成)决定了系统的扩展上限。

USL 模型

Amdahl 定律假设并行部分没有协调开销,但实际系统中并行工作者之间需要通信和同步。Neil Gunther 提出的通用可扩展性定律(Universal Scalability Law,USL)在 Amdahl 定律基础上增加了”串扰”(Crosstalk)参数:

C(N) = N / (1 + α(N-1) + β·N·(N-1))

N = 节点数
α = 串行化参数(Amdahl 定律中的非并行部分)
β = 串扰参数(节点间通信开销)
C(N) = 相对吞吐量

当 β > 0 时,增加节点数到一定程度后,吞吐量不仅不增长,还会下降——因为节点间协调的开销超过了新增节点带来的收益。这就是为什么某些系统在扩容后性能反而变差。


二、排队论基础:Little 定律与 M/M/c 模型

排队论(Queueing Theory)是容量规划的数学基础。它不能精确预测系统在真实负载下的表现,但能提供一个方向性正确的分析框架。

Little 定律

Little 定律是排队论中最基本、最实用的结论:

L = λ × W

L = 系统中的平均请求数(正在处理 + 排队等待的)
λ = 到达速率(每秒到达的请求数,即 QPS)
W = 平均逗留时间(从请求到达到处理完成的总时间)

这个公式看似简单,但适用范围极广——它不依赖任何关于到达分布或服务时间分布的假设。

应用场景一:估算并发连接数。

假设一个 Web 服务的 QPS = 1000,平均响应时间 = 200 毫秒。

L = 1000 × 0.2 = 200

系统中平均有 200 个并发请求。这意味着至少需要 200 个工作线程(或连接)来处理这些请求。如果线程池大小设为 100,那一半的请求必须排队等待,延迟会显著增加。

应用场景二:评估扩容效果。

如果通过优化数据库查询,把平均响应时间从 200 毫秒降到 100 毫秒,相同的 QPS 下并发请求数降为:

L = 1000 × 0.1 = 100

不需要加机器,只需要优化代码,就把资源需求减半了。

M/M/c 模型

M/M/c 模型是排队论中用于分析多服务台(Multi-Server)系统的经典模型。

M = 到达过程服从泊松分布(Poisson Distribution)
M = 服务时间服从指数分布(Exponential Distribution)
c = 服务台数量(即可并行处理请求的工作者数量)

核心参数:

ρ = λ / (c × μ) = 系统利用率

λ = 到达速率
μ = 单个服务台的服务速率(1 / 平均服务时间)
c = 服务台数量

当 ρ 接近 1 时,队列长度趋向无穷大,延迟急剧增加。这是容量规划中最重要的结论:不要让系统利用率接近 100%

平均响应时间
     ↑
     |                          /
     |                        /
     |                      /
     |                    /
     |                  /
     |                /
     |             /
     |          /
     |       /
     |    ──
     | ──
     +——————————————————————→ 利用率 ρ
     0%     50%     80% 90%   100%

M/M/c 模型给出了一个关键的工程建议:CPU 利用率的安全水位线应该控制在 60-75%。超过这个范围后,排队延迟会快速增长。

CPU 利用率 排队延迟增幅(相对于空闲时) 工程建议
30% 约 1.4x 安全,有充足余量
50% 约 2x 正常运行水位
70% 约 3.3x 接近水位线,准备扩容
80% 约 5x 超出水位线,应立即扩容
90% 约 10x 危险,延迟显著恶化
95% 约 20x 紧急,系统即将不可用

排队论的局限

排队论模型基于一系列理想假设(泊松到达、指数服务时间、无限队列长度),真实系统的行为往往偏离这些假设:

  1. 到达过程不是泊松分布。真实流量有突发性(Burstiness),尤其是大促开始瞬间的流量尖峰,比泊松分布预测的极端情况更极端。
  2. 服务时间不是指数分布。数据库查询的响应时间通常是双峰分布——缓存命中时很快,缓存未命中时很慢。
  3. 队列不是无限的。线程池有大小限制,连接池有上限,超出限制的请求会被拒绝而不是排队。

因此,排队论应该用来做”方向性判断”(这个配置大概能不能扛住这个负载),不能用来做”精确预测”。精确预测需要全链路压测。


三、资源建模:CPU、内存、I/O、网络

容量规划需要对每种资源分别建模,然后找出最先触达瓶颈的资源——它决定了系统的整体容量上限。

CPU 建模

CPU 需求 = 每个请求的 CPU 时间 × 请求速率
CPU 容量 = CPU 核心数 × (1 - 安全余量)
最大 QPS = CPU 容量 / 每个请求的 CPU 时间

例如:一台 8 核服务器,每个请求平均消耗 4 毫秒的 CPU 时间,安全余量 30%。

CPU 容量 = 8 × (1 - 0.3) = 5.6 核
最大 QPS = 5600 毫秒/秒 / 4 毫秒/请求 = 1400 请求/秒

注意:这里的”CPU 时间”是纯 CPU 计算时间,不包括 I/O 等待。如果一个请求的总响应时间是 100 毫秒但 CPU 时间只有 4 毫秒,说明 96 毫秒花在了 I/O 等待上——这种情况下 CPU 通常不是瓶颈。

内存建模

内存需求 = 基础内存 + 每连接内存 × 并发连接数 + 缓存大小

基础内存 = JVM 堆 + 操作系统 + 其他常驻进程
每连接内存 = 请求缓冲区 + 线程栈 + 上下文数据

Java 应用的典型内存构成:

磁盘 I/O 建模

IOPS 需求 = 每个请求的 I/O 操作数 × 请求速率
IOPS 容量 = 磁盘的 IOPS 上限

HDD:约 100-200 IOPS(随机读写)
SSD:约 10000-100000 IOPS
NVMe SSD:约 100000-1000000 IOPS

如果每个数据库查询平均产生 5 次随机磁盘读取,QPS = 1000 时需要 5000 IOPS。HDD 无法满足,必须使用 SSD。

网络建模

带宽需求 = 每个请求的数据量 × 请求速率

入方向:请求体大小 × QPS
出方向:响应体大小 × QPS

如果平均响应体大小 = 10 KB,QPS = 10000:

出方向带宽 = 10 KB × 10000 = 100 MB/s = 800 Mbps

一根千兆网卡(1 Gbps)在 80% 利用率时只能提供 800 Mbps,刚好触达上限。这种情况下需要万兆网卡或多网卡绑定。

综合建模

把各维度的容量上限汇总,找出最低的那个——它就是系统的瓶颈。

graph TD
    subgraph 资源容量模型
        CPU["CPU<br/>单机最大 QPS: 1400"]
        MEM["内存<br/>单机最大连接: 200"]
        DISK["磁盘 I/O<br/>单机最大 QPS: 2000<br/>(SSD)"]
        NET["网络<br/>单机最大 QPS: 12500<br/>(万兆网卡)"]
    end

    CPU --> BN["瓶颈分析"]
    MEM --> BN
    DISK --> BN
    NET --> BN

    BN --> RESULT["系统瓶颈: CPU<br/>单机最大 QPS = 1400<br/>目标 QPS = 10000<br/>需要 8 台应用服务器"]

四、需求预测:从历史数据到业务增长

容量规划的输入是”未来需要多少容量”。这个预测来自两个方向:历史趋势外推业务增长预期

历史趋势外推

收集过去 3-12 个月的流量数据(QPS、DAU、订单量等),拟合增长曲线,外推到目标时间点。

常见的增长模型:

QPS
  ↑
  |            ○ ○ ○   ← 线性外推
  |         ○
  |       ○
  |     ○
  |   ○
  |  ○
  | ○
  +——————————————→ 时间
     历史数据    预测

注意:历史趋势外推假设未来的增长模式与过去相同。如果业务计划有大的变化(例如新增一个大客户、开拓新市场),历史趋势就不准了。

业务增长预期

与产品和业务团队对齐未来的业务计划:

把业务预期转化为技术指标:

预计新增 100 万 DAU(日活跃用户)
→ 平均每用户每日请求 50 次
→ 日增请求量 5000 万
→ 峰值系数 3x(峰值 QPS ≈ 平均 QPS × 3)
→ 峰值 QPS 增量 = 5000 万 / 86400 × 3 ≈ 1736 QPS

峰值估算

容量规划必须基于峰值而非平均值。峰值 / 平均值的比率(Peak-to-Average Ratio)因业务类型而异:

业务类型 峰均比 说明
企业 SaaS 2-3x 工作日白天是峰值,夜间和周末是低谷
社交媒体 3-5x 午间和晚间峰值明显
电商(日常) 3-5x 晚间购物高峰
电商(大促) 10-50x 大促开始瞬间流量尖峰
游戏 5-10x 新版本上线、节假日

大促场景下的峰值估算尤其困难。2019 年天猫双十一零点峰值达到 54.4 万笔/秒,是日常峰值的数十倍。这种量级的尖峰,历史趋势外推几乎无法预测,必须结合业务部门的营销计划和流量预估。


五、容量基线与水位线管理

容量基线

容量基线(Capacity Baseline)是系统在标准负载下各项资源指标的正常范围。它是容量管理的”参照物”。

建立基线的步骤:

  1. 选择一个代表性的时间段(通常是最近 4-8 周的工作日)。
  2. 排除异常数据点(故障期间、大促期间)。
  3. 计算各指标的统计分布:p50、p90、p99、最大值。
  4. 将基线文档化,并与团队共享。

基线指标示例:

┌────────────────────────────────────────────────┐
│ 容量基线 — 订单服务 — 2025 Q4                      │
├────────────────┬──────┬──────┬──────┬──────────┤
│ 指标            │ p50  │ p90  │ p99  │ 峰值      │
├────────────────┼──────┼──────┼──────┼──────────┤
│ QPS            │ 800  │ 1200 │ 1500 │ 2000     │
│ CPU 利用率(%)   │ 35   │ 50   │ 65   │ 78       │
│ 内存利用率(%)   │ 60   │ 65   │ 70   │ 75       │
│ DB 连接池利用率 │ 30   │ 45   │ 60   │ 72       │
│ p99 延迟(ms)   │ 120  │ 180  │ 350  │ 800      │
└────────────────┴──────┴──────┴──────┴──────────┘

水位线

水位线(Waterline)是各资源指标的告警阈值,分为多个级别:

级别 CPU 利用率 内存利用率 DB 连接池 动作
绿色 < 50% < 60% < 50% 正常运行
黄色 50-70% 60-75% 50-70% 关注,制定扩容计划
橙色 70-80% 75-85% 70-85% 告警,启动扩容流程
红色 > 80% > 85% > 85% 紧急,立即扩容或降级

水位线的设定要考虑扩容的提前量(Lead Time)。如果从决定扩容到新资源上线需要 2 小时(包括审批、采购、部署、预热),那么在达到橙色水位线时就应该开始扩容,不能等到红色才动手。

水位线与 SLO 的关系

水位线是保障 SLO 的”提前预警”。如果 CPU 持续在红色水位线运行,排队延迟会急剧增加,最终导致延迟 SLO 被突破。

flowchart LR
    A["资源水位监控"] --> B{"当前水位"}
    B -->|绿色| C["正常运行"]
    B -->|黄色| D["生成扩容报告<br/>排入下周计划"]
    B -->|橙色| E["触发扩容工单<br/>2小时内完成"]
    B -->|红色| F["紧急扩容<br/>+ 启动降级预案"]
    E --> G["扩容完成<br/>水位回落"]
    F --> G
    G --> A

六、全链路压测

容量模型给出了理论预测,全链路压测(Full-Chain Load Testing)是验证这个预测的唯一手段。

什么是全链路压测

与传统的单接口压测不同,全链路压测模拟真实用户的完整行为链路——从浏览商品、加入购物车、提交订单到完成支付——按照真实的流量比例同时施压。

传统压测:只压一个接口
  压测工具 ──→ 搜索 API ──→ 看搜索 API 扛多少 QPS

全链路压测:按真实比例压所有接口
  压测工具 ──→ 搜索 API (50%)
            ──→ 详情 API (30%)
            ──→ 下单 API (15%)
            ──→ 支付 API (5%)

压测环境

全链路压测最理想的方式是在生产环境做。原因是:

  1. 测试环境的硬件配置、网络拓扑、数据量、中间件版本与生产环境不同,压测结果不可信。
  2. 测试环境的数据量通常远小于生产环境,数据库查询在小数据量下的表现与大数据量下完全不同。

在生产环境做压测需要解决两个关键问题:

流量隔离:压测流量必须与正常用户流量隔离,不能影响真实用户。常见方案是在请求头中添加压测标识(如 X-Test: shadow),中间件根据标识把压测请求路由到影子表/影子库。

数据隔离:压测产生的数据(订单、支付记录等)不能混入正常业务数据。解决方案包括影子数据库(Shadow Database)、影子表(Shadow Table)或压测结束后清理数据。

压测流量模型设计

压测流量模型需要尽可能贴近真实流量的特征:

  1. 请求比例:各接口的请求比例应与真实流量一致。
  2. 用户行为序列:用户不是随机访问接口的,而是按照一定的路径(浏览 → 加购 → 下单 → 支付)。
  3. 数据分布:热门商品和冷门商品的访问比例、大额订单和小额订单的比例。
  4. 时间模式:流量的递增模式。大促流量不是匀速增长的,而是在开始瞬间有一个陡峭的尖峰。

压测指标收集

压测过程中需要监控的核心指标:

┌──────────────────────────────────────────────────┐
│                全链路压测监控面板                      │
├────────────────┬─────────────────────────────────┤
│ 业务指标        │ QPS、成功率、错误类型分布             │
│ 延迟指标        │ p50、p90、p99、max                 │
│ 应用指标        │ CPU、内存、GC 次数/时长、线程数       │
│ 数据库指标      │ 连接数、QPS、慢查询数、锁等待          │
│ 缓存指标        │ 命中率、内存使用、连接数               │
│ 消息队列指标    │ 积压量、消费速率、延迟                  │
│ 网络指标        │ 带宽、连接数、TCP 重传率               │
└────────────────┴─────────────────────────────────┘

压测分析与调优

压测结果的分析不是”看 QPS 够不够”,而是找出系统在压力下的行为模式:

  1. 瓶颈在哪? 哪个组件最先达到容量上限?
  2. 劣化曲线是什么形状? 性能是线性下降还是突然崩塌?
  3. 恢复能力如何? 流量回落后,系统能否快速恢复到正常水平?

七、容量规划流程

把上述方法论整合为一个可执行的流程:

度量 → 建模 → 预测 → 供给 → 验证 → 反馈

1. 度量:收集当前系统的性能基线数据
2. 建模:构建资源消耗模型(CPU/内存/IO/网络)
3. 预测:结合业务增长预期,计算未来资源需求
4. 供给:采购/申请/预留资源
5. 验证:通过全链路压测验证预测的准确性
6. 反馈:用压测结果修正模型,迭代优化

时间线

时间节点 动作 产出
T-90 天 收集历史数据,构建容量模型 容量模型文档
T-60 天 与业务团队对齐增长预期,计算资源需求 资源需求清单
T-45 天 提交资源申请,启动采购 采购订单
T-30 天 资源到位,部署并加入集群 扩容完成报告
T-14 天 第一轮全链路压测 压测报告、问题清单
T-7 天 修复问题后第二轮压测 验证报告
T-3 天 最终检查,确认所有预案就位 上线 Checklist
T-0 大促 / 上线 实时监控
T+7 天 复盘,修正容量模型 复盘报告

八、工程案例:某电商平台的大促容量规划

以下案例基于国内某大型电商平台公开的技术分享,展示了一次双十一大促的容量规划全过程。

背景

容量模型

第一步:度量当前基线。

在日常峰值 5 万 QPS 下:

第二步:资源需求预测。

目标 QPS = 50 万,是日常的 10 倍。但不是所有组件都需要线性扩容 10 倍:

第三步:关键决策。

全链路压测

第一轮压测(T-14 天):

压到目标 QPS 的 80%(40 万)时,发现以下问题:

  1. Redis 集群某些节点出现热点 Key(某爆款商品的库存 Key),单节点 CPU 打到 95%。
  2. 下单 API 的 p99 延迟飙到 3.2 秒,超过 1 秒的 SLO。瓶颈定位到分布式锁的竞争。
  3. 消息队列消费端出现积压,原因是消费者数量不足。

修复措施:

  1. 热点 Key 问题:引入本地缓存 + Key 分片策略(同一个逻辑 Key 分散到多个物理 Key)。
  2. 分布式锁竞争:把库存扣减改为基于 Redis Lua 脚本的原子操作,消除分布式锁。
  3. 消息队列积压:增加消费者实例数,从 16 个扩到 64 个。

第二轮压测(T-7 天):

成功压到 55 万 QPS(110% 目标值),所有 SLO 达标:

大促当天

实际峰值 QPS = 48 万,略低于预估的 50 万。所有指标在安全水位内,无重大故障。

复盘

  1. 应用服务器实际使用了 362 台中的 320 台(利用率约 88%),多余的 42 台是冗余。但考虑到安全余量,这个冗余是合理的。
  2. 容量模型对 Redis 的预测偏保守(预估 40 节点,实际 32 节点就够了),下次可以减少。
  3. 数据库的分库分表方案有效,但运维复杂度显著增加。建议长期迁移到分布式数据库。

九、容量规划的权衡

维度 过度规划(Over-Provisioning) 合理规划 不足规划(Under-Provisioning)
资源成本 高(大量闲置) 适中 低(但故障损失可能更高)
可用性风险
资源利用率 低(20-30%) 中(50-70%) 高但不稳定
规划投入 低(直接多买) 高(需要建模和压测) 低(但事后救火投入更高)
弹性应对 好(有余量吸收突发) 好(有计划性的弹性策略) 差(突发即过载)
适用场景 关键金融系统、不允许任何风险 大多数生产系统 早期创业、POC 验证

容量规划与云计算

云计算的弹性伸缩(Auto Scaling)能力部分缓解了容量规划的压力——不需要提前几个月采购硬件,可以按需扩缩。但云计算没有消除容量规划的需要:

  1. 弹性伸缩有延迟。从触发扩容到新实例可用,通常需要 2-5 分钟。大促开始瞬间的流量尖峰来不及扩容。
  2. 预留实例更便宜。按需实例(On-Demand)的价格通常是预留实例(Reserved Instance)的 2-3 倍。如果能预测基础用量,用预留实例 + 按需实例的组合可以显著降低成本。
  3. 数据层不容易弹性伸缩。数据库、缓存、消息队列的扩缩容涉及数据迁移和状态同步,不是简单地”加一台机器”就行。

十、结论

容量规划的本质是一个预测 → 验证 → 修正的迭代过程:

  1. 不要靠猜。用排队论和资源模型做方向性预测,用全链路压测做精确验证。
  2. 找瓶颈,不是堆资源。系统性能取决于最窄的瓶颈,堆资源到非瓶颈组件是浪费。
  3. 管水位线,不是看平均值。峰值决定容量需求,p99 比 p50 重要。
  4. 留余量,但不要过度。70% 的利用率水位线是一个合理的起点。
  5. 持续迭代。每次大促或重大发布后,用真实数据修正容量模型。

容量规划不是一次性项目,是一个持续运转的工程流程。做得好,它是护城河——让你在竞争对手被流量击垮时稳如磐石。做得差,它是隐形炸弹——在你最需要系统稳定的时候爆炸。


导航

上一篇:SLO 工程:可靠性的量化管理

下一篇:混沌工程:主动验证系统的韧性


参考资料

书籍

论文

工业实践

工具

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-13 · architecture

【系统架构设计百科】架构质量属性:不只是"高可用高性能"

需求评审时写下的'高可用、高性能、高并发',到了架构设计阶段几乎无法落地——因为它们不是可执行的需求。本文从 SEI/CMU 的质量属性理论出发,用 stimulus-response 场景模型把模糊需求变成可量化、可验证的架构约束,并拆解属性之间的冲突与联动关系。

2026-04-13 · architecture

【系统架构设计百科】告警策略:如何避免"狼来了"

大多数团队的告警系统都在制造噪声而不是传递信号。阈值告警看似直观,实则产生大量误报和漏报,值班工程师在凌晨三点被叫醒,却发现只是一次无害的毛刺。本文从告警疲劳的工业数据出发,拆解基于 SLO 的多窗口燃烧率告警算法,深入 Alertmanager 的路由、抑制与分组机制,结合 PagerDuty 的告警疲劳研究和真实工程案例,给出一套可落地的告警策略设计方法。


By .