引言:为什么需要容量规划?

在生产环境中,容量不足会导致:

  • 系统崩溃:流量高峰时服务不可用
  • 性能劣化:响应时间暴增,用户体验极差
  • 资源浪费:过度预留资源,成本高昂

而容量过剩则意味着:

  • 成本浪费:服务器、数据库、带宽资源闲置
  • 运维复杂:管理大量不必要的实例

容量规划的目标是在成本和性能之间找到最佳平衡点,确保系统在满足业务需求的同时,资源利用率最优。

一、容量规划基础

1.1 核心指标

吞吐量指标:

  • QPS(Queries Per Second):每秒查询数
  • TPS(Transactions Per Second):每秒事务数
  • RPS(Requests Per Second):每秒请求数
  • CPS(Connections Per Second):每秒连接数

响应时间指标:

  • Avg:平均响应时间
  • P50(中位数):50%的请求响应时间
  • P95:95%的请求响应时间(重要指标)
  • P99:99%的请求响应时间(核心指标)
  • P999:99.9%的请求响应时间(SLA承诺)

资源利用率指标:

  • CPU利用率:< 70%(预留buffer)
  • 内存利用率:< 80%
  • 磁盘I/O:< 70%
  • 网络带宽:< 70%

1.2 容量评估公式

基本公式:

系统容量 = (单机QPS × 机器数量) / 安全系数

安全系数 = 1.2 ~ 1.5(预留20-50%buffer)

容量评估示例:

业务需求:
- 日活用户(DAU):100万
- 人均日请求:20次
- 高峰期占比:30%
- 峰值QPS = (100万 × 20) / 86400 × 2 × 0.3 ≈ 139 QPS

单机性能:
- 单机QPS:1000(压测得出)
- 需要机器数:139 / 1000 × 1.5 ≈ 1台(实际部署2台)

1.3 Little’s Law(利特尔法则)

L = λ × W

L:系统中平均请求数(并发用户数)
λ:请求到达率(QPS)
W:请求在系统中的平均停留时间(响应时间)

示例:
- QPS = 1000
- 平均响应时间 = 100ms = 0.1s
- 并发用户数 = 1000 × 0.1 = 100

二、性能测试工具

2.1 JMeter 实战

安装:

1
2
3
4
5
6
# macOS
brew install jmeter

# Linux
wget https://downloads.apache.org//jmeter/binaries/apache-jmeter-5.5.tgz
tar -xzf apache-jmeter-5.5.tgz

测试计划配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2">
  <hashTree>
    <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="测试计划">
      <elementProp name="userVariables" elementType="Arguments">
        <!-- 全局变量 -->
        <collectionProp name="Arguments.arguments">
          <elementProp name="BASE_URL" elementType="Argument">
            <stringProp name="Argument.name">BASE_URL</stringProp>
            <stringProp name="Argument.value">http://localhost:8080</stringProp>
          </elementProp>
        </collectionProp>
      </elementProp>
    </TestPlan>
    <hashTree>
      <!-- 线程组 -->
      <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="线程组">
        <stringProp name="ThreadGroup.num_threads">100</stringProp>
        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
        <boolProp name="ThreadGroup.scheduler">true</boolProp>
        <stringProp name="ThreadGroup.duration">60</stringProp>
      </ThreadGroup>
      <hashTree>
        <!-- HTTP请求 -->
        <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP请求">
          <stringProp name="HTTPSampler.domain">${BASE_URL}</stringProp>
          <stringProp name="HTTPSampler.path">/api/v1/users</stringProp>
          <stringProp name="HTTPSampler.method">GET</stringProp>
        </HTTPSamplerProxy>
      </hashTree>
    </hashTree>
  </hashTree>
</jmeterTestPlan>

命令行运行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# GUI模式(调试)
jmeter

# 命令行模式(压测)
jmeter -n -t test-plan.jmx -l result.jtl -e -o report/

# 参数说明:
# -n:非GUI模式
# -t:测试计划文件
# -l:结果文件
# -e:生成报告
# -o:报告输出目录

分布式压测:

1
2
3
4
5
6
7
8
9
# 1. 启动远程服务器
jmeter-server

# 2. 修改本地配置
# jmeter.properties
remote_hosts=192.168.1.10,192.168.1.11,192.168.1.12

# 3. 运行分布式测试
jmeter -n -t test-plan.jmx -R 192.168.1.10,192.168.1.11,192.168.1.12

2.2 K6 实战(Go开发的压测工具)

安装:

1
2
3
4
5
6
# macOS
brew install k6

# Linux
curl https://dl.k6.io/rpm/repo.rpm | sudo rpm/i -
sudo yum install k6

测试脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';

// 配置
export const options = {
  stages: [
    { duration: '1m', target: 100 },   // 1分钟内增加到100用户
    { duration: '3m', target: 100 },   // 保持100用户3分钟
    { duration: '1m', target: 200 },   // 1分钟内增加到200用户
    { duration: '3m', target: 200 },   // 保持200用户3分钟
    { duration: '1m', target: 0 },     // 1分钟内降到0
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'],  // 95%的请求响应时间<500ms
    http_req_failed: ['rate<0.01'],    // 错误率<1%
  },
};

export default function () {
  // GET请求
  const res1 = http.get('http://localhost:8080/api/v1/users');
  check(res1, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  });

  // POST请求
  const payload = JSON.stringify({
    name: 'test_user',
    email: 'test@example.com',
  });

  const params = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

  const res2 = http.post('http://localhost:8080/api/v1/users', payload, params);
  check(res2, {
    'status is 201': (r) => r.status === 201,
  });

  sleep(1);  // 暂停1秒
}

运行:

1
2
3
4
5
6
7
8
# 运行测试
k6 run load-test.js

# 输出到InfluxDB(用于Grafana可视化)
k6 run --out influxdb=http://localhost:8086/k6 load-test.js

# 分布式测试(需要k6-operator)
k6 operator run load-test.js

2.3 Locust 实战(Python)

安装:

1
pip install locust

测试脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# locustfile.py
from locust import HttpUser, task, between

class WebsiteUser(HttpUser):
    wait_time = between(1, 3)  # 任务间隔1-3秒

    def on_start(self):
        """每个用户启动时登录"""
        self.client.post("/api/v1/login", json={
            "username": "test_user",
            "password": "password123"
        })

    @task(3)  # 权重为3(执行频率更高)
    def index(self):
        self.client.get("/")

    @task(1)  # 权重为1
    def about(self):
        self.client.get("/about")

    @task(2)
    def api_get_users(self):
        with self.client.get("/api/v1/users", catch_response=True) as response:
            if response.status_code == 200:
                response.success()
            else:
                response.failure(f"Got status code {response.status_code}")

运行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 单机运行
locust -f locustfile.py

# 分布式运行(Master)
locust -f locustfile.py --master

# 分布式运行(Worker)
locust -f locustfile.py --worker --master-host=<master-ip>

# 无头模式(命令行)
locust -f locustfile.py --headless -u 1000 -r 100 -t 1m

三、压测方案设计

3.1 压测类型

基准测试(Baseline Test):

  • 目标:确定系统在理想条件下的性能基线
  • 并发用户:1
  • 持续时间:10分钟
  • 关注指标:单用户响应时间

负载测试(Load Test):

  • 目标:验证系统在预期负载下的性能
  • 并发用户:日常峰值(如日常QPS × 2)
  • 持续时间:30分钟
  • 关注指标:响应时间、错误率

压力测试(Stress Test):

  • 目标:找到系统的性能瓶颈和极限
  • 并发用户:逐步增加至系统崩溃
  • 持续时间:直至系统崩溃
  • 关注指标:最大QPS、崩溃点

耐久测试(Endurance Test):

  • 目标:验证系统在长时间运行下的稳定性
  • 并发用户:日常峰值
  • 持续时间:24小时甚至更长
  • 关注指标:内存泄漏、连接泄漏、性能劣化

峰值测试(Spike Test):

  • 目标:验证系统应对突发流量的能力
  • 并发用户:瞬间从0飙升至峰值(如秒杀场景)
  • 持续时间:5-10分钟
  • 关注指标:系统恢复能力、排队机制

3.2 压测数据准备

测试数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 生成测试数据
import random
import string

def generate_random_user():
    return {
        "name": "".join(random.choices(string.ascii_letters, k=10)),
        "email": f"{random.randint(1000, 9999)}@example.com",
        "age": random.randint(18, 60),
    }

# 批量插入测试数据
users = [generate_random_user() for _ in range(100000)]
# 批量插入到数据库...

数据多样性:

  • 正常数据:符合业务规则的数据
  • 边界数据:最大/最小值、空值
  • 异常数据:超长字符串、非法字符(测试异常处理)

3.3 压测监控

系统监控:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# CPU监控
top -p $(pgrep java)

# 内存监控
free -m

# 磁盘I/O监控
iostat -x 1

# 网络监控
iftop

应用监控:

1
2
3
4
5
# JVM监控(Java)
jstat -gcutil <pid> 1000

# Go程序监控
curl http://localhost:6060/debug/pprof/heap

数据库监控:

1
2
3
4
5
6
-- MySQL
SHOW PROCESSLIST;
SHOW ENGINE INNODB STATUS;

-- PostgreSQL
SELECT * FROM pg_stat_activity WHERE state != 'idle';

四、全链路压测

4.1 全链路压测架构

┌────────────────────────────────────────────────────────┐
│                   压测平台                            │
│  ┌────────────┐  ┌────────────┐  ┌────────────┐      │
│  │ 压测控制   │  │ 数据隔离   │  │ 结果分析   │      │
│  └────────────┘  └────────────┘  └────────────┘      │
└────────────────────────────────────────────────────────┘
                        │
                        ▼
┌────────────────────────────────────────────────────────┐
│                   生产环境(影子流量)                 │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐ │
│  │ API网关  │─→│ 业务服务 │─→│ 数据库  │  │ 缓存    │ │
│  └─────────┘  └─────────┘  └─────────┘  └─────────┘ │
│       ↓            ↓            ↓            ↓        │
│   影子表       影子服务      影子库       影子缓存    │
└────────────────────────────────────────────────────────┘

4.2 数据隔离方案

方案1:影子库/表(推荐)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
-- 生产表
CREATE TABLE users (
    id BIGINT PRIMARY KEY,
    name VARCHAR(50),
    email VARCHAR(100)
);

-- 影子表(结构完全相同)
CREATE TABLE users_shadow (
    id BIGINT PRIMARY KEY,
    name VARCHAR(50),
    email VARCHAR(100)
);

路由配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 路由到影子库/表
func RouteToShadow() bool {
    // 从HTTP Header获取压测标识
    if r.Header.Get("X-Pressure-Test") == "true" {
        return true
    }
    return false
}

// SQL路由
func GetTableName(tableName string) string {
    if RouteToShadow() {
        return tableName + "_shadow"
    }
    return tableName
}

方案2:数据标记

1
2
3
4
5
6
7
8
-- 添加压测标识字段
ALTER TABLE users ADD COLUMN is_shadow TINYINT DEFAULT 0;

-- 压测数据标记
UPDATE users SET is_shadow = 1 WHERE id IN (...);

-- 查询时过滤
SELECT * FROM users WHERE is_shadow = 0;

4.3 消息隔离

1
2
3
4
5
6
7
// 压测消息发送到独立Topic
func PublishMessage(topic string, msg Message) error {
    if IsPressureTest() {
        topic = topic + "_shadow"
    }
    return mq.Publish(topic, msg)
}

4.4 缓存隔离

1
2
3
4
5
6
7
// 压测数据添加前缀
func GetCacheKey(key string) string {
    if IsPressureTest() {
        return "shadow:" + key
    }
    return key
}

五、压测结果分析

5.1 关键指标解读

响应时间分析:

目标:P95 < 500ms

结果:
- P50: 100ms ✅
- P95: 450ms ✅
- P99: 1200ms ❌

结论:
- 95%的用户体验良好
- 但长尾请求(1%)响应时间过长
- 需要优化P99,如增加缓存、优化慢查询

吞吐量分析:

目标:QPS > 1000

结果:
- 最大QPS: 800 ❌

瓶颈定位:
1. CPU使用率:60%(未满载)
2. 数据库连接池:满载
3. 慢查询:发现3条慢查询

优化方案:
1. 增加数据库连接池大小
2. 优化慢查询

错误率分析:

目标:错误率 < 0.1%

结果:
- 错误率:0.5% ❌

错误分类:
- 502 Bad Gateway: 60%(后端服务超时)
- 503 Service Unavailable: 30%(服务熔断)
- 400 Bad Request: 10%(参数错误)

优化方案:
1. 增加超时时间
2. 扩容后端服务
3. 参数校验

5.2 瓶颈定位方法

CPU瓶颈:

1
2
3
# 火焰图分析
perf record -F 99 -p <pid> -g -- sleep 60
perf script | ./FlameGraph/stackcollapse-perf.pl | ./FlameGraph/flamegraph.pl > flame.svg

内存瓶颈:

1
2
3
4
5
# JVM堆转储
jmap -dump:format=b,file=heap.hprof <pid>

# 分析(MAT工具)
# 查找内存泄漏、大对象

I/O瓶颈:

1
2
3
4
5
# 磁盘I/O分析
iostat -x 1

# 网络I/O分析
iftop

数据库瓶颈:

1
2
3
4
5
6
-- 慢查询分析
SHOW FULL PROCESSLIST;
SELECT * FROM mysql.slow_log ORDER BY query_time DESC LIMIT 10;

-- 锁等待分析
SELECT * FROM information_schema.innodb_locks;

六、生产环境容量评估

6.1 生产流量评估

方法1:基于历史数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import pandas as pd

# 读取历史访问日志
df = pd.read_csv('access.log', names=['timestamp', 'url', 'status'])

# 计算每日QPS
df['timestamp'] = pd.to_datetime(df['timestamp'])
df['date'] = df['timestamp'].dt.date
daily_qps = df.groupby('date').size() / 86400

# 计算峰值QPS
df['hour'] = df['timestamp'].dt.hour
hourly_qps = df.groupby(['date', 'hour']).size() / 3600
max_qps = hourly_qps.max()

print(f"平均QPS: {daily_qps.mean():.2f}")
print(f"峰值QPS: {max_qps:.2f}")
print(f"峰谷比: {max_qps / daily_qps.mean():.2f}")

方法2:基于业务增长预测

1
2
3
4
5
6
7
# 假设月增长率20%
current_qps = 1000
monthly_growth = 0.20

# 预测6个月后的QPS
future_qps = current_qps * (1 + monthly_growth) ** 6
print(f"6个月后预计QPS: {future_qps:.2f}")

6.2 容量规划决策

当前系统:
- 日常QPS: 1000
- 峰值QPS: 3000
- 单机QPS: 500

容量规划:
1. 日常容量: 1000 / 500 × 1.5 = 3台
2. 峰值容量: 3000 / 500 × 1.5 = 9台
3. 弹性伸缩: 部署9台,平时3台,峰值时自动扩容到9台

6.3 自动扩缩容策略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# Kubernetes HPA配置
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-app
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
      - type: Percent
        value: 50
        periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Percent
        value: 10
        periodSeconds: 60

七、实战案例

7.1 案例:电商秒杀系统压测

背景:

  • 秒杀活动:1000件商品
  • 预期QPS:50000
  • 响应时间要求:P95 < 500ms

压测方案:

  1. 三阶段压测:

    • 预热:1000 QPS × 5分钟
    • 峰值:50000 QPS × 10分钟
    • 降温:1000 QPS × 5分钟
  2. 数据隔离:

    • 影子表:orders_shadow
    • 影子缓存:redis:shadow
    • 压测标识:HTTP Header X-Pressure-Test: true
  3. 监控指标:

    • QPS、响应时间、错误率
    • CPU、内存、网络
    • 数据库慢查询、连接池

压测结果:

指标          目标       实际       状态
QPS           50000      48000      ❌
P95延迟       500ms      380ms      ✅
错误率        <1%        0.5%       ✅
CPU使用率      <70%       85%        ❌

优化方案:

  1. 扩容应用实例(CPU不足)
  2. 优化Redis连接池配置
  3. 增加本地缓存

优化后结果:

QPS: 60000 ✅
P95延迟: 350ms ✅
CPU使用率: 65% ✅

7.2 案例:API网关压测

背景:

  • 日活用户:100万
  • 人均日请求:20次
  • 高峰QPS:预计5000

压测脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import http from 'k6/http';
import { check } from 'k6';

export let options = {
  stages: [
    { duration: '2m', target: 100 },
    { duration: '5m', target: 1000 },
    { duration: '5m', target: 5000 },
    { duration: '2m', target: 0 },
  ],
};

export default function () {
  const res = http.get('https://api.example.com/v1/products');
  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 200ms': (r) => r.timings.duration < 200,
  });
}

压测发现的问题:

  1. 数据库连接池耗尽

    • 症状:大量 Connection pool exhausted 错误
    • 原因:连接池大小设置为50,不足以支撑5000 QPS
    • 解决:增加到200
  2. 慢查询

    • 症状:P99响应时间 > 2000ms
    • 原因:缺少索引
    • 解决:添加索引
  3. Redis超时

    • 症状:Redis响应时间 > 500ms
    • 原因:Redis单线程处理不过来
    • 解决:使用Redis Cluster

八、最佳实践

8.1 压测注意事项

  1. 数据隔离:使用影子库/表,避免污染生产数据
  2. 逐步加压:避免突发流量导致雪崩
  3. 实时监控:时刻关注系统状态,及时发现问题
  4. 提前准备:准备好回滚方案,遇到问题立即回滚
  5. 错峰压测:避开业务高峰期,选择凌晨进行

8.2 容量规划建议

  1. 预留buffer:容量预留20-50%
  2. 弹性伸缩:使用自动扩缩容,根据流量动态调整
  3. 定期评估:每季度重新评估容量,提前扩容
  4. 成本优化:使用竞价实例、预留实例降低成本
  5. 降级预案:准备降级方案,确保核心功能可用

8.3 压测流程清单

  • 确定压测目标(QPS、响应时间、错误率)
  • 编写压测脚本
  • 准备测试数据
  • 配置数据隔离(影子库/表)
  • 配置监控(系统、应用、数据库)
  • 执行压测
  • 分析结果
  • 定位瓶颈
  • 优化
  • 复测验证

容量规划和压测是保障系统稳定性的基石。只有经过充分压测的系统,才能在生产环境中从容应对流量高峰。