系统容量规划与压测实战:从1万到100万QPS的科学扩容
大家好,我是迪哥。2026 年 618 前,我们的预估峰值从 10 万 QPS 涨到了 100 万 QPS,压测发现瓶颈在数据库连接池,扩容了 20 倍才顶住。今天聊聊,如何科学地做容量规划和压测,不让你的系统死在峰值。
为什么要做容量规划?
2023 年我们踩过坑:
- ❌ 618 峰值预估 5 万,实际来了 20 万,系统直接雪崩
- ❌ 为了保险,提前扩容了 10 倍,成本超了 3 倍
- ❌ 不知道瓶颈在哪,瞎扩容,钱花了性能没上去
容量规划三步法
1. 建立性能基准线
单机性能测试:
bash
# 用 JMeter 或者 wrk 压测
wrk -t12 -c400 -d30s http://localhost:8080/api/order
# 结果示例
Running 30s test @ http://localhost:8080/api/order
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 20.5ms 15.2ms 200.5ms 89.00%
Req/Sec 5000.0 200.0 6000.0 90.00%
1800000 requests in 30.00s, 2.50GB read
Requests/sec: 60000.00 ← 单机 6 万 QPS
基准表:
| 指标 | 数值 |
|---|---|
| 单机 QPS | 60000 |
| 单机 CPU 峰值 | 80% |
| 单机内存峰值 | 8GB |
| 数据库最大连接数 | 2000 |
2. 预估峰值 & 冗余系数
预估峰值 QPS = 历史峰值 × 增长系数 × 突发系数
= 5万 × 2倍 × 1.5倍
= 15万 QPS
需要机器数 = 预估峰值 / 单机 QPS × 冗余系数
= 15万 / 6万 × 1.3
≈ 4台
但实际是 100 万,所以需要 100万 / 6万 × 1.3 ≈ 22台
经验值:
- 日常冗余系数:1.3-1.5
- 大促冗余系数:2-3
- 新业务:3-5倍冗余
3. 识别瓶颈 & 扩容
压测实战:JMeter
安装 JMeter
下载:https://jmeter.apache.org/download_jmeter.cgi
测试计划
Test Plan
└── Thread Group (1000 threads, 10s ramp-up, 300s duration)
├── HTTP Request (POST /api/order)
├── View Results Tree
└── Summary Report
关键监控指标
| 层级 | 指标 | 告警阈值 |
|---|---|---|
| 系统 | CPU | > 80% |
| 系统 | Load Average | > CPU 核数 |
| 系统 | 内存 | > 85% |
| 系统 | 网络带宽 | > 80% |
| JVM | Heap Used | > 80% |
| JVM | GC 次数 | Full GC > 3次/5分钟 |
| 应用 | 错误率 | > 0.1% |
| 应用 | P99 延迟 | > 500ms |
| 中间件 | 数据库连接池 | > 90% |
| 中间件 | Redis 连接池 | > 90% |
瓶颈定位示例
现象:
- 应用 QPS 上不去,CPU 只有 30%
- 错误率上升,提示
GetConnectionTimeoutException
定位:
yaml
# application.yml
spring:
datasource:
hikari:
maximum-pool-size: 20 ← 太小了!
优化:
yaml
spring:
datasource:
hikari:
maximum-pool-size: 200 # 改成 200
全链路压测
单系统压测不够,要全链路!
压测工具 → 网关 → 订单服务 → 库存服务 → 数据库
↓
Redis
关键点:
- ✅ 影子表(数据库隔离)
- ✅ 消息隔离(避免污染真实数据)
- ✅ 灰度流量(从 10% 开始逐步放大)
压测脚本:Go 版本
go
package main
import (
"fmt"
"net/http"
"sync"
"time"
)
func main() {
concurrency := 1000
requests := 1000000
var wg sync.WaitGroup
client := &http.Client{Timeout: 10 * time.Second}
start := time.Now()
for i := 0; i < concurrency; i++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for j := 0; j < requests/concurrency; j++ {
resp, err := client.Get("http://localhost:8080/api/order")
if err != nil {
continue
}
resp.Body.Close()
}
}(i)
}
wg.Wait()
duration := time.Since(start)
qps := float64(requests) / duration.Seconds()
fmt.Printf("QPS: %.0f, Duration: %v\n", qps, duration)
}
容量规划表格
| 系统 | 单机 QPS | 预估峰值 | 冗余系数 | 所需机器 | 现状机器 | 是否要扩容 |
|---|---|---|---|---|---|---|
| 订单服务 | 60000 | 100万 | 1.5 | 25 | 10 | ✅ 是 |
| 库存服务 | 80000 | 80万 | 1.5 | 15 | 8 | ✅ 是 |
| 用户服务 | 40000 | 30万 | 1.5 | 11 | 12 | ❌ 否 |
| 网关 | 15万 | 100万 | 1.5 | 10 | 5 | ✅ 是 |
经验总结
- 不要瞎拍脑袋! 基于历史数据 + 增长预期
- 先压测再扩容! 不然钱花了,性能没上去
- 容量规划是动态的! 每季度重估一次
- 监控是前提! 没有监控,谈何容量规划
- 灰度很重要! 别一上来全量,从 10% 开始
说到容量规划,我家那只叫 Docker 的哈士奇最近饭量从 1 碗涨到了 4 碗,我要是早做容量规划,就不至于三天买一次狗粮了 😂
我是迪哥,我们下期再见!
往期推荐: