Skip to content

微服务容量规划:SLO、压测、瓶颈识别、扩缩容与容量闭环

容量规划解决的不是“机器够不够”,而是“在明确业务目标和故障余量下,系统每一层能稳定承载多少请求,瓶颈在哪里,怎样扩容才真的有效”。很多线上事故不是 QPS 突然特别高,而是某个慢下游、连接池、线程池、数据库锁、MQ 分区或 Redis 热 Key 先到瓶颈,然后把整个链路拖慢。

本页只讲微服务和分布式生产常用场景,不写泛泛博客实战。相关知识可继续阅读:服务发现负载均衡稳定性治理依赖治理可观测性消息堆积

学习目标

学完本页要能回答:

  1. QPS、并发、RT、P95、P99、利用率、饱和度、错误率分别代表什么。
  2. 为什么低 QPS 也可能把线程池打满。
  3. 怎样从 SLO 推导接口超时、线程池、连接池、实例数和下游容量。
  4. 压测结果怎样转成生产容量,而不是看一个峰值数字就上线。
  5. 为什么扩容应用实例后 RT 不降,甚至堆积还会回来。
  6. HPA 自动扩缩容为什么会抖动,冷启动、预热和优雅下线怎样设计。
  7. 遇到线上容量问题时按什么顺序定位瓶颈。

一、容量规划为什么不是简单扩机器

微服务调用链不是一台机器的问题,而是一条共享资源链。

mermaid
flowchart TD
    A["用户请求"] --> B["Gateway连接和过滤器"]
    B --> C["订单服务Tomcat线程"]
    C --> D["Feign连接池"]
    D --> E["库存服务线程池"]
    E --> F["Redis连接和热点Key"]
    E --> G["MySQL连接和锁"]
    E --> H["MQ发送和分区"]

任何一层先饱和,都会变成全链路瓶颈:

瓶颈层表现为什么加应用实例不一定有效
Gateway入口 503、504、连接等待后端扩了,但入口连接、过滤器或限流仍是瓶颈
应用线程池QPS 不高但线程占满下游慢导致线程等待,扩容会把更多压力打给下游
HTTP 连接池Feign 慢、pending connection 高连接上限太小或连接复用旧实例
Redis热 Key RT 升高、CPU 单核高所有实例仍访问同一个热点 Key
MySQL慢 SQL、锁等待、连接池满DB 是共享瓶颈,应用扩容会增加并发和锁竞争
MQLag 增长、消费延迟分区数、单分区热、消费事务慢限制吞吐
第三方超时、限流、配额失败对方配额固定,本方扩容只会更快撞限流

容量规划的核心原则:

  1. 先定义业务目标,再谈机器数量。
  2. 先找到最小瓶颈,再谈扩容方向。
  3. 先保护事实源和强依赖,再优化弱依赖体验。
  4. 扩容必须配合限流、熔断、降级、连接池、缓存和观测。
  5. 任何容量数字都要说明测试条件、数据分布、依赖状态和安全系数。

二、核心指标

2.1 QPS、TPS、RPS

指标含义常见误区
QPS每秒查询或请求数只适合描述读接口或宽泛请求量
TPS每秒事务数更适合下单、支付、入库这类完整业务动作
RPS每秒请求数网关、HTTP 服务常用

不要只看平均 QPS。订单创建 500 QPS 和商品详情 500 QPS 完全不是一个重量级:前者可能涉及库存、优惠、风控、支付预创建、MQ 和数据库事务;后者可能主要读缓存。

2.2 RT、P95、P99

RT 是响应时间。平均 RT 只能说明“平均用户大概多慢”,不能代表尾部用户体验。

指标含义用途
Avg RT平均响应时间看整体趋势
P5050% 请求小于该耗时看典型用户
P9595% 请求小于该耗时看大多数用户体验
P9999% 请求小于该耗时看尾部延迟和容量拐点
Max最大耗时排查个例,容易受异常值影响

为什么生产更关注 P95/P99?因为微服务链路会放大尾延迟。一次订单请求可能调用 5 个下游,只要其中一个下游出现 P99 慢调用,入口请求就可能变慢。

2.3 并发和 in-flight

并发不是线程数本身,而是同一时刻正在系统里“停留”的请求数量。请求越慢,同样 QPS 下并发越高。

核心公式是 Little 定律:

text
系统内平均并发 = 到达率 × 平均停留时间

例如:

text
1000 QPS × 50ms = 50 个并发请求
1000 QPS × 500ms = 500 个并发请求

QPS 没变,只是 RT 从 50ms 变成 500ms,并发就放大 10 倍。线程池、连接池、队列都会被占住,所以“QPS 不高为什么线程池满”的答案通常是:请求停留时间太长,in-flight 堆起来了。

2.4 利用率、饱和度、错误率

指标看什么危险信号
利用率CPU、内存、磁盘、网络、连接池使用比例长期超过安全水位
饱和度是否出现排队、等待、拒绝队列长度、等待连接、线程池队列堆积
错误率失败请求占比5xx、超时、熔断、限流、业务失败异常升高
延迟Avg、P95、P99P99 先升高通常是容量拐点信号

容量不够时,常见顺序不是“先报错”,而是:

mermaid
flowchart TD
    A["并发上升"] --> B["队列和等待增加"]
    B --> C["P95和P99升高"]
    C --> D["超时和重试增加"]
    D --> E["线程池连接池被占满"]
    E --> F["错误率升高或雪崩"]

三、从 SLO 推导容量

SLO 是服务目标,比如“订单创建 P99 小于 800ms,成功率 99.9%”。容量规划必须从 SLO 出发,而不是从机器数出发。

mermaid
flowchart TD
    A["业务峰值和SLO"] --> B["拆入口预算"]
    B --> C["拆下游依赖预算"]
    C --> D["压测单实例能力"]
    D --> E["乘以安全系数和冗余"]
    E --> F["得到实例数和资源池大小"]
    F --> G["上线观测和容量复盘"]

示例:订单创建链路目标 P99 800ms:

层级预算
Gateway 认证和路由50ms
订单服务本地逻辑100ms
库存冻结200ms
优惠计算100ms
MySQL 事务提交150ms
MQ 发送80ms
网络和序列化余量120ms

如果库存冻结 P99 已经 500ms,订单接口不可能稳定满足 800ms。此时扩容订单服务没意义,要治理库存服务、数据库锁、缓存热点或库存模型。

四、压测类型和边界

压测类型目的适合回答什么
单接口压测看某个接口自身容量单接口 QPS、RT、线程池、SQL瓶颈
链路压测看完整调用链网关、服务、DB、Redis、MQ共同瓶颈
容量压测找吞吐拐点单实例安全容量和最大容量
稳定性压测长时间运行内存泄漏、连接泄漏、GC、日志膨胀
峰值压测模拟秒杀、批量采集瞬时峰值、限流、削峰、降级
混合流量压测模拟真实业务比例热点接口互相影响
影子压测线上真实链路验证生产容量,但必须隔离数据和副作用

压测不能只看“最高 QPS”。要记录:

  1. 数据量是否接近生产。
  2. 热点分布是否真实。
  3. 缓存命中率是否真实。
  4. 下游依赖是 Mock 还是真实。
  5. 数据库是否有锁冲突。
  6. MQ 分区数和消费者数是否一致。
  7. 日志级别、采样率、监控是否接近生产。
  8. 是否包含登录鉴权、网关过滤、序列化和网络成本。

五、容量曲线怎样读

典型压测曲线不是线性上升。

mermaid
flowchart TD
    A["低并发"] --> B["吞吐随并发上升"]
    B --> C["达到平台区"]
    C --> D["P95和P99明显上升"]
    D --> E["队列等待和超时增加"]
    E --> F["吞吐下降且错误率升高"]

真正的安全容量通常在拐点之前,而不是最大吞吐点。

区间表现策略
健康区QPS 上升,RT 稳定,错误率低可作为日常运行区
临界区P99 开始升高,队列等待增加设置告警和限流阈值
饱和区吞吐不再增长,超时增加不能继续加压,要找瓶颈
崩溃区错误率升高,重试放大熔断、降级、扩容或止血

生产容量建议用:

text
生产安全吞吐 = 单实例拐点前吞吐 × 实例数 × 安全系数 × 冗余系数

常见取值:

系数示例说明
安全系数0.6 到 0.8给 GC、抖动、数据倾斜、发布留余量
N-1 冗余少算 1 台实例任一实例宕机或发布下线仍能承载
下游折扣按最小下游容量限制DB、Redis、MQ、第三方配额可能更小

例如单实例在 P99 稳定前能承载 300 QPS,有 6 个实例,安全系数 0.7,按 N-1 计算:

text
安全容量 = 300 × (6 - 1) × 0.7 = 1050 QPS

这不是绝对真理,而是容量估算起点。上线后还要用真实流量持续校准。

六、容量预算怎样拆

6.1 线程池预算

线程池容量不能只看 CPU 核数。IO 型接口大部分时间在等下游,线程数可能比 CPU 核数多;CPU 型任务线程过多会增加上下文切换。

简单估算:

text
需要并发线程数 ≈ 峰值QPS × 平均RT秒数

如果订单接口 500 QPS,平均 RT 200ms:

text
500 × 0.2 = 100 个并发请求

这说明入口线程、业务线程、下游连接池都要能承载约 100 个 in-flight,还要留发布、GC、慢请求和突发余量。

6.2 HTTP 连接池预算

Feign 或 WebClient 最终要通过 HTTP Client 发请求。连接池过小会让请求卡在“等连接”,不是下游真的慢。

参数关注点
最大总连接数所有下游共享连接上限
每路由最大连接数单个下游服务可用连接上限
连接获取超时等连接多久算失败
连接超时TCP 建连多久失败
读取超时请求发出后多久没响应失败
连接最大存活时间防止旧连接长期指向旧实例

6.3 数据库连接池预算

数据库连接不是越多越好。连接太少会上游排队,连接太多会把 DB 线程、锁和 CPU 打爆。

正确思路:

  1. 先压测数据库单 SQL、单事务能力。
  2. 看慢 SQL、锁等待、buffer pool、redo flush、CPU 和 IO。
  3. 应用连接池上限不能超过数据库能稳定处理的并发。
  4. 多个服务共享同一个库时,要给每个服务分配连接预算。
  5. 批处理和在线请求最好隔离连接池或错峰。

6.4 MQ 消费容量预算

MQ 消费能力由 Broker、Topic 分区或队列、消费者实例、单条处理耗时、下游写入能力共同决定。

text
消费能力 ≈ 分区数或队列数 × 每分区单消费者处理能力

Kafka 场景下,同一个 Consumer Group 内,一个分区同一时刻只能被一个消费者消费。消费者实例数超过分区数后,额外实例不会提升该 Topic 的并行度。

RocketMQ 和 RabbitMQ 也要看队列数、消费者并发、ACK、重试、死信和下游数据库能力。消费者扩容后短暂有效,后面又堆积,常见原因见消息堆积扩容后再次堆积

七、为什么扩容不一定有效

扩容只有在瓶颈位于可横向扩展层时才有效。应用 CPU 满且无共享下游瓶颈,扩容通常有效;如果瓶颈在共享数据库、热点 Key、单分区、锁、第三方配额,扩容应用只会把更多请求推到瓶颈上。

现象常见原因处理方向
加实例后 QPS 不涨DB、Redis、MQ、第三方是瓶颈治理共享下游,不能只扩应用
加实例后 RT 仍高慢 SQL、锁等待、连接池等待看 P95/P99、锁、连接池、线程堆栈
加消费者后 Lag 短暂下降又上升分区限制、单条处理慢、下游写入慢、重试死循环增分区、批量、幂等、死信、优化下游
新实例流量少本地服务列表未刷新、权重、慢启动、长连接查注册中心、LB候选、连接池
流量不均长连接、热租户、灰度、实例性能不同按实例看 QPS、RT、CPU、错误率
HPA 来回抖动指标滞后、冷启动慢、缩容太快稳定窗口、预热、冷却时间、最小副本

八、HPA 和自动扩缩容

Kubernetes HPA 常按 CPU 扩容,但微服务瓶颈不一定体现在 CPU。

mermaid
flowchart TD
    A["指标采集"] --> B["HPA计算期望副本"]
    B --> C["创建新Pod"]
    C --> D["镜像拉取和启动"]
    D --> E["Readiness通过"]
    E --> F["注册中心和Endpoint传播"]
    F --> G["调用方逐步拿到新实例"]
    G --> H["连接池和缓存预热"]

HPA 的坑:

问题原因方案
CPU 不高但接口慢阻塞在 DB、Redis、第三方、锁或连接池用 in-flight、P99、队列长度、连接等待等指标
扩容太慢镜像拉取、JIT、类加载、缓存预热、注册传播预留最小副本、镜像预拉取、慢启动
新实例一上来就被打爆Ready 只证明可接请求,不代表缓存和连接已热低权重慢启动、预热接口、观察 P99
缩容导致请求失败Pod 被杀时仍有在途请求或消费者任务Readiness 摘流、preStop、graceful shutdown
扩缩容抖动指标延迟和阈值太敏感设置稳定窗口、冷却时间和限速

九、商业场景

9.1 医疗数据采集批量入库

场景:每晚从多家医院拉取检验、检查、患者和资产数据,清洗后写入 MySQL,同时同步 ES。

容量设计:

  1. 按医院、接口、数据类型拆任务,不让单医院慢接口拖垮全局。
  2. 采集线程池、清洗线程池、入库线程池分开。
  3. MySQL 批量写入要控制 batch size,避免大事务和锁等待。
  4. ES 同步异步化,失败进入重试表或 MQ 死信。
  5. hospitalId + sourceId + batchNo + bizId 做幂等。
  6. 监控每个医院的 RT、错误率、积压量和入库 TPS。

9.2 订单创建高峰

场景:活动开始后订单创建峰值升高。

容量设计:

  1. Gateway 先按用户、活动、接口限流。
  2. 订单创建强依赖库存,库存失败不能伪成功。
  3. 营销、推荐、画像是弱依赖,短超时快速降级。
  4. 下单接口必须有幂等号,超时后查询订单事实。
  5. MQ 用于异步通知、积分、ES 同步,不要让非核心动作阻塞下单。
  6. 压测要覆盖真实 SKU 热点,否则上线遇到热库存行会崩。

9.3 ES 查询容量

场景:资产平台按关键字、科室、状态、时间范围搜索。

容量设计:

  1. 控制深分页,使用 search_after 或滚动导出。
  2. 为常用过滤条件建 keyword 字段和合适 mapping。
  3. 避免高基数字段大聚合直接打在线集群。
  4. 搜索接口设置超时和降级,不能拖垮主业务。
  5. 观察分片级慢查询、CPU、Heap、GC、segment merge。

9.4 MQ 堆积恢复

场景:下游数据库慢导致消费 Lag 上升。

容量设计:

  1. 先确认是生产太快、消费太慢、Broker 慢还是下游慢。
  2. 消费者扩容前看分区数或队列数。
  3. 单条处理慢时优先批量、减少远程调用、优化 SQL。
  4. 毒消息进入死信,不要无限重试阻塞队列。
  5. 恢复期间控制生产速率,避免边恢复边继续放大。

十、JDK 8 Demo:Little 定律容量估算器

下面 Demo 不依赖框架,用来理解“RT 变慢为什么会让并发膨胀”。

java
public class CapacityCalculator {
    public static void main(String[] args) {
        print("健康状态", 1000, 50);
        print("下游变慢", 1000, 500);
        print("低QPS慢接口", 100, 3000);
    }

    private static void print(String name, double qps, double rtMs) {
        double rtSeconds = rtMs / 1000.0;
        double concurrency = qps * rtSeconds;
        double safeThreads = concurrency * 1.5;

        System.out.println("场景: " + name);
        System.out.println("QPS: " + qps);
        System.out.println("平均RT(ms): " + rtMs);
        System.out.println("估算in-flight并发: " + concurrency);
        System.out.println("建议预估线程或连接余量: " + Math.ceil(safeThreads));
        System.out.println();
    }
}

输出含义:

text
1000 QPS、50ms RT,系统里约 50 个请求同时停留。
1000 QPS、500ms RT,系统里约 500 个请求同时停留。
100 QPS、3000ms RT,也会有约 300 个请求同时停留。

这就是为什么“QPS 不高但线程池满”并不矛盾。慢请求会占住资源,队列继续堆积,最后 P99、超时、重试和错误率一起上升。

十一、生产 Runbook

11.1 QPS 不高但线程池满

排查顺序:

  1. 看入口 QPS、Avg RT、P95、P99 是否同时变化。
  2. 看线程池 active、queue、reject。
  3. 打线程栈,看线程在等 DB、Redis、HTTP、锁还是本地 CPU。
  4. 看连接池 pending、active、idle。
  5. 看下游 P99、错误率、慢 SQL、锁等待。
  6. 看是否有重试放大。
  7. 对弱依赖降级,对强依赖限流和保护。

11.2 扩容后 RT 不降

排查顺序:

  1. 按实例看 QPS 是否真的分摊。
  2. 看注册中心是否发现新实例,LoadBalancer 候选是否包含新实例。
  3. 看连接池是否还集中连旧实例。
  4. 看瓶颈是不是共享 DB、Redis、MQ 或第三方。
  5. 看新实例是否冷启动,JIT、缓存、连接是否未预热。
  6. 看是否因为扩容导致下游锁竞争更严重。

11.3 压测没问题,上线崩了

常见原因:

原因说明
数据不真实测试库小,生产数据大
热点不真实压测随机分散,生产集中打热 SKU 或热租户
依赖被 Mock压测没经过真实 DB、Redis、MQ、第三方
缓存命中率不真实压测缓存全热,生产大量冷数据
日志和监控不同生产日志、Trace、审计带来额外开销
发布窗口未考虑滚动发布时少一部分实例,容量下降
重试未计入生产超时后重试放大流量

11.4 HPA 扩容后又缩回去

排查顺序:

  1. 看 HPA 使用的是 CPU、内存还是自定义指标。
  2. 看指标采集周期和 HPA 计算周期。
  3. 看是否有 stabilization window。
  4. 看新 Pod Ready 后是否被瞬间打满又触发熔断。
  5. 看缩容是否过快,导致刚恢复的容量又被拿掉。
  6. 增加最小副本、冷却时间、慢启动和按 P99/in-flight 扩容的指标。

十二、面试标准回答

容量规划不能只说“加机器”。我会先明确业务峰值和 SLO,例如 P99、成功率和允许错误预算;然后画出完整调用链,拆 Gateway、应用线程池、HTTP 连接池、Redis、MySQL、MQ 和第三方依赖的容量预算。压测时找单实例拐点,不用最大吞吐当生产容量,而是在拐点前乘以安全系数,并考虑 N-1 冗余、发布下线、冷启动和下游共享瓶颈。线上扩容后仍慢时,要按实例 QPS、P99、线程池、连接池、DB 锁、Redis 热 Key、MQ Lag 和重试放大逐层定位,确认瓶颈是否真的在应用层。

十三、本章小结

容量规划的本质是把业务目标翻译成每一层资源的可承载边界。QPS 只是入口数字,RT 决定请求停留时间,P99 暴露尾部风险,队列和连接池说明饱和度,错误率说明保护是否失效。扩容只是手段,不是答案;真正可靠的系统必须同时具备容量预算、压测验证、限流熔断、优雅上下线、慢启动、观测告警和故障 Runbook。