性能测试
性能测试是验证软件系统在特定负载下的响应速度、稳定性和可扩展性的测试类型。它帮助发现系统瓶颈,确保系统在生产环境中能够满足性能要求。
什么是性能测试?
性能测试通过模拟各种负载条件来评估系统的性能表现。不同于功能测试关注「系统能否正确工作」,性能测试关注「系统工作得有多好」。
性能测试的目的
| 目的 | 说明 |
|---|---|
| 发现瓶颈 | 识别系统中的性能限制因素 |
| 验证容量 | 确认系统能否满足预期的用户负载 |
| 评估稳定性 | 验证系统在长时间运行下的稳定性 |
| 优化指导 | 为性能优化提供数据支持 |
| 容量规划 | 为基础设施扩容提供依据 |
性能测试类型
负载测试(Load Testing)
负载测试在预期负载下验证系统性能,是最常见的性能测试类型。
目的:验证系统能否满足预期的用户负载和性能要求。
测试方法:
- 确定预期的用户数和请求量
- 逐步增加负载到目标值
- 保持负载一段时间
- 监控系统各项指标
- 分析性能数据
// K6 负载测试示例
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 100 }, // 2分钟内增加到100用户
{ duration: '5m', target: 100 }, // 保持100用户5分钟
{ duration: '2m', target: 0 }, // 2分钟内降到0
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95%请求响应时间小于500ms
http_req_failed: ['rate<0.01'], // 错误率小于1%
},
};
export default function () {
const res = http.get('https://api.example.com/users');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1); // 模拟用户思考时间
}
压力测试(Stress Testing)
压力测试在超过正常负载的情况下测试系统,目的是找到系统的极限和崩溃点。
目的:确定系统的性能极限,观察系统在超载时的行为。
测试方法:
- 从正常负载开始
- 持续增加负载直到系统崩溃或性能严重下降
- 记录系统崩溃点和崩溃前的性能数据
- 观察系统恢复能力
// K6 压力测试示例
export const options = {
stages: [
{ duration: '2m', target: 100 }, // 正常负载
{ duration: '5m', target: 100 }, // 维持正常负载
{ duration: '2m', target: 200 }, // 超过正常负载
{ duration: '5m', target: 200 }, // 维持高负载
{ duration: '2m', target: 300 }, // 继续增加
{ duration: '5m', target: 300 }, // 观察系统极限
{ duration: '2m', target: 400 }, // 继续施压
{ duration: '5m', target: 400 }, // 找到崩溃点
{ duration: '5m', target: 0 }, // 恢复阶段
],
};
尖峰测试(Spike Testing)
尖峰测试模拟突然的大幅负载增加,验证系统处理突发流量的能力。
目的:验证系统对突发流量的响应和恢复能力。
场景示例:
- 电商促销活动开始瞬间
- 限时抢购活动
- 热门文章突然爆火
// K6 尖峰测试示例
export const options = {
stages: [
{ duration: '2m', target: 100 }, // 正常负载
{ duration: '10s', target: 1000 }, // 突然增加到10倍
{ duration: '1m', target: 1000 }, // 维持高负载
{ duration: '10s', target: 100 }, // 快速下降
{ duration: '2m', target: 100 }, // 恢复正常
],
};
耐久性测试(Endurance Testing)
耐久性测试在长时间内维持稳定的负载,验证系统的稳定性和资源泄漏问题。
目的:发现内存泄漏、数据库连接泄漏、文件句柄泄漏等随时间累积的问题。
测试时长:通常持续数小时到数天。
// K6 耐久性测试示例
export const options = {
stages: [
{ duration: '5m', target: 100 }, // 启动阶段
{ duration: '24h', target: 100 }, // 持续24小时
{ duration: '5m', target: 0 }, // 结束阶段
],
};
容量测试(Volume Testing)
容量测试使用大量数据来验证系统的数据处理能力。
目的:验证系统处理大量数据时的性能表现。
关注点:
- 数据库数据量增长对查询性能的影响
- 文件存储容量对系统性能的影响
- 日志文件大小对性能的影响
可扩展性测试(Scalability Testing)
可扩展性测试验证系统在增加资源(水平扩展或垂直扩展)后的性能提升。
目的:评估系统的扩展能力和效率。
测试方法:
- 在基准配置下测试性能
- 增加资源(CPU、内存、服务器实例)
- 再次测试性能
- 分析性能提升与资源增加的比例
性能指标
响应时间指标
| 指标 | 说明 | 目标值 |
|---|---|---|
| 平均响应时间 | 所有请求的平均响应时间 | 通常 < 1秒 |
| P50 | 50%请求的响应时间 | 反映典型用户体验 |
| P90 | 90%请求的响应时间 | 大多数用户体验 |
| P95 | 95%请求的响应时间 | SLA 常用指标 |
| P99 | 99%请求的响应时间 | 关注尾部延迟 |
| 最大响应时间 | 最慢请求的响应时间 | 识别异常情况 |
// K6 响应时间阈值配置
export const options = {
thresholds: {
http_req_duration: [
'avg<200', // 平均响应时间小于200ms
'p(50)<150', // 50%请求小于150ms
'p(90)<300', // 90%请求小于300ms
'p(95)<500', // 95%请求小于500ms
'p(99)<1000', // 99%请求小于1000ms
],
},
};
吞吐量指标
| 指标 | 说明 | 单位 |
|---|---|---|
| RPS | 每秒请求数 | requests/second |
| TPS | 每秒事务数 | transactions/second |
| QPS | 每秒查询数 | queries/second |
| 带宽 | 数据传输速率 | MB/s |
资源利用率指标
| 指标 | 说明 | 警戒值 |
|---|---|---|
| CPU 使用率 | CPU 占用百分比 | > 80% 需关注 |
| 内存使用率 | 内存占用百分比 | > 85% 需关注 |
| 磁盘 I/O | 磁盘读写速率 | 接近上限需关注 |
| 网络 I/O | 网络带宽使用率 | > 70% 需关注 |
| 连接数 | 数据库/API 连接数 | 接近上限需关注 |
错误率指标
| 指标 | 说明 |
|---|---|
| HTTP 错误率 | 4xx/5xx 响应占比 |
| 超时率 | 超时请求占比 |
| 失败率 | 业务失败请求占比 |
性能测试工具
工具对比
| 工具 | 类型 | 特点 | 适用场景 |
|---|---|---|---|
| JMeter | GUI/CLI | 功能全面、插件丰富、支持多协议 | 传统企业应用 |
| K6 | CLI | JavaScript 脚本、云原生、CI 友好 | 现代云应用 |
| Locust | CLI | Python 脚本、分布式、易扩展 | Python 团队 |
| Gatling | CLI | Scala DSL、高性能、详细报告 | 高并发场景 |
| Vegeta | CLI | 简单命令行、HTTP 负载测试 | 快速基准测试 |
| Artillery | CLI | JavaScript、支持 WebSocket | 实时应用 |
JMeter 实战
安装配置
# 下载并解压
wget https://downloads.apache.org//jmeter/binaries/apache-jmeter-5.6.3.tgz
tar -xzf apache-jmeter-5.6.3.tgz
# 启动 GUI
./apache-jmeter-5.6.3/bin/jmeter.sh
# 命令行运行
./apache-jmeter-5.6.3/bin/jmeter.sh -n -t test.jmx -l results.jtl
测试计划配置
<!-- test.jmx 简化示例 -->
<ThreadGroup>
<stringProp name="ThreadGroup.num_threads">100</stringProp>
<stringProp name="ThreadGroup.ramp_time">60</stringProp>
<boolProp name="ThreadGroup.scheduler">true</boolProp>
<stringProp name="ThreadGroup.duration">300</stringProp>
</ThreadGroup>
<HTTPSamplerProxy>
<stringProp name="HTTPSampler.domain">api.example.com</stringProp>
<stringProp name="HTTPSampler.path">/users</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
</HTTPSamplerProxy>
性能测试脚本
// JMeter Groovy 脚本示例
import org.apache.jmeter.protocol.http.sampler.HTTPSampler
// 创建 HTTP 请求
def sampler = new HTTPSampler()
sampler.setDomain("api.example.com")
sampler.setPath("/api/users")
sampler.setMethod("GET")
sampler.addArgument("page", "1")
// 执行请求
def result = sampler.sample()
log.info("Response time: " + result.getTime() + "ms")
log.info("Response code: " + result.getResponseCode())
K6 实战
安装
# macOS
brew install k6
# Windows
choco install k6
# Linux
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C4914C31F145
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt update
sudo apt install k6
基础脚本
// basic-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 10, // 虚拟用户数
duration: '30s', // 测试持续时间
};
export default function () {
// GET 请求
const res = http.get('https://test-api.k6.io/public/crocodiles/');
// 断言检查
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 200ms': (r) => r.timings.duration < 200,
});
sleep(1); // 思考时间
}
复杂场景测试
// complex-scenario.js
import http from 'k6/http';
import { check, group, sleep } from 'k6';
import { Rate, Trend, Counter } from 'k6/metrics';
// 自定义指标
const myRate = new Rate('my_rate');
const myTrend = new Trend('my_trend');
const myCounter = new Counter('my_counter');
// 测试配置
export const options = {
scenarios: {
// 场景1:普通用户浏览
browsing: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '1m', target: 50 },
{ duration: '3m', target: 50 },
{ duration: '1m', target: 0 },
],
gracefulRampDown: '30s',
},
// 场景2:API 调用
api_calls: {
executor: 'constant-arrival-rate',
rate: 100, // 每秒100个请求
timeUnit: '1s',
duration: '5m',
preAllocatedVUs: 50,
maxVUs: 200,
},
},
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'],
http_req_failed: ['rate<0.01'],
},
};
// 测试数据
const BASE_URL = 'https://api.example.com';
export default function () {
group('用户流程', function () {
// 1. 登录
const loginRes = http.post(`${BASE_URL}/auth/login`, JSON.stringify({
username: 'testuser',
password: 'password123',
}), {
headers: { 'Content-Type': 'application/json' },
});
check(loginRes, {
'login successful': (r) => r.status === 200,
});
const token = loginRes.json('token');
const authHeaders = {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
};
sleep(1);
// 2. 获取用户信息
const userRes = http.get(`${BASE_URL}/user/profile`, authHeaders);
check(userRes, {
'profile fetched': (r) => r.status === 200,
});
myTrend.add(userRes.timings.duration);
sleep(2);
// 3. 获取订单列表
const ordersRes = http.get(`${BASE_URL}/orders`, authHeaders);
check(ordersRes, {
'orders fetched': (r) => r.status === 200,
'has orders': (r) => r.json('orders').length > 0,
});
myCounter.add(1);
myRate.add(ordersRes.status === 200);
});
}
数据驱动测试
// data-driven.js
import http from 'k6/http';
import { check } from 'k6';
import { SharedArray } from 'k6/data';
// 从文件加载测试数据
const users = new SharedArray('users', function () {
return open('./users.json').split('\n').map(JSON.parse);
});
export const options = {
iterations: users.length, // 每个用户数据执行一次
};
export default function () {
const user = users[__ITER]; // 当前迭代的数据
const res = http.post('https://api.example.com/login', JSON.stringify({
username: user.username,
password: user.password,
}), {
headers: { 'Content-Type': 'application/json' },
});
check(res, {
'login successful': (r) => r.status === 200,
});
}
Locust 实战
安装
pip install locust
测试脚本
# locustfile.py
from locust import HttpUser, task, between, events
from locust.runners import MasterRunner, WorkerRunner
import json
class WebsiteUser(HttpUser):
"""模拟网站用户行为"""
wait_time = between(1, 5) # 请求间隔1-5秒
def on_start(self):
"""用户开始时执行(登录)"""
response = self.client.post("/api/auth/login", json={
"username": "testuser",
"password": "password123"
})
if response.status_code == 200:
self.token = response.json().get("token")
else:
self.token = None
@task(3) # 权重为3
def view_products(self):
"""浏览商品列表"""
self.client.get("/api/products", name="/products")
@task(2) # 权重为2
def view_product_detail(self):
"""查看商品详情"""
product_id = 1 # 可以随机生成
self.client.get(f"/api/products/{product_id}", name="/products/[id]")
@task(1) # 权重为1
def add_to_cart(self):
"""添加到购物车"""
if self.token:
self.client.post(
"/api/cart/items",
json={"product_id": 1, "quantity": 1},
headers={"Authorization": f"Bearer {self.token}"},
name="/cart/items"
)
# 自定义事件处理
@events.test_start.add_listener
def on_test_start(environment, **kwargs):
"""测试开始时执行"""
if isinstance(environment.runner, MasterRunner):
print("测试开始...")
@events.test_stop.add_listener
def on_test_stop(environment, **kwargs):
"""测试结束时执行"""
if isinstance(environment.runner, MasterRunner):
print("测试结束...")
运行测试
# Web UI 模式
locust -f locustfile.py
# 无头模式
locust -f locustfile.py --headless -u 100 -r 10 -t 5m \
--host https://api.example.com \
--html report.html
# 分布式模式
locust -f locustfile.py --master # 主节点
locust -f locustfile.py --worker --master-host=master-ip # 工作节点
性能测试流程
1. 需求分析
明确性能测试的目标和范围:
- 预期用户数和并发量
- 响应时间要求
- 吞吐量要求
- 可接受的错误率
- 测试场景和业务流程
2. 测试计划
制定详细的测试计划:
- 测试类型选择
- 测试环境配置
- 测试数据准备
- 测试工具选择
- 测试时间安排
3. 测试设计
设计测试场景和用例:
- 用户行为建模
- 测试数据设计
- 性能指标定义
- 阈值设定
4. 环境准备
搭建独立的性能测试环境:
# 环境配置示例
performance_test_env:
servers:
- name: app-server
cpu: 8 cores
memory: 16GB
count: 2
- name: database
cpu: 4 cores
memory: 32GB
storage: 500GB SSD
- name: cache
type: redis
memory: 8GB
network:
bandwidth: 1Gbps
latency: < 1ms
monitoring:
- prometheus
- grafana
- ELK stack
5. 脚本开发
编写和调试测试脚本:
- 录制或编写测试脚本
- 参数化测试数据
- 添加断言和检查点
- 配置思考时间
6. 测试执行
执行测试并监控:
- 基准测试(确定基线)
- 负载测试
- 压力测试
- 收集性能数据
7. 结果分析
分析测试结果:
// K6 报告分析示例
// 输出:
// http_req_duration..............: avg=120.5ms min=45.2ms med=98.3ms max=2.5s p(90)=180.2ms p(95)=250.6ms
// http_req_failed.................: 0.5% ✓ 50 ✗ 9950
// http_reqs........................: 10000 333.333333/s
// iteration_duration..............: avg=1.2s min=0.5s med=1.1s max=5.2s
// iterations.......................: 10000 333.333333/s
// vus.............................: 100 min=100 max=100
// 分析要点:
// 1. 平均响应时间是否符合预期
// 2. P95/P99 是否在可接受范围
// 3. 错误率是否低于阈值
// 4. 吞吐量是否满足需求
8. 性能优化
根据分析结果进行优化:
常见瓶颈和优化方向:
| 瓶颈类型 | 症状 | 优化方向 |
|---|---|---|
| 数据库 | 慢查询、连接池耗尽 | SQL优化、索引、连接池配置 |
| 内存 | GC频繁、OOM | 内存泄漏修复、缓存策略 |
| CPU | 高CPU使用率 | 算法优化、并发处理 |
| 网络 | 高延迟、带宽不足 | CDN、压缩、连接复用 |
| 磁盘 | 高IO等待 | SSD、缓存、异步写入 |
性能测试最佳实践
1. 测试环境隔离
# 确保测试环境与生产环境隔离
# 使用独立的数据、独立的网络
PERF_ENV=performance pytest tests/performance/
2. 基准测试先行
// 在做任何优化之前,先建立基准
export const options = {
vus: 1,
duration: '1m',
};
// 记录基准性能
// 后续优化可以对比基准
3. 模拟真实用户行为
// ❌ 不好:没有思考时间
export default function () {
http.get('https://api.example.com/data');
http.get('https://api.example.com/users');
}
// ✅ 好:包含思考时间和真实流程
export default function () {
// 登录
http.post('https://api.example.com/login', ...);
sleep(randomIntBetween(1, 3)); // 思考时间
// 浏览
http.get('https://api.example.com/data');
sleep(randomIntBetween(2, 5)); // 更长的思考时间
// 操作
http.post('https://api.example.com/action', ...);
}
4. 使用真实数据量
# ❌ 不好:测试数据库只有100条数据
# 实际生产可能有百万条数据,性能表现完全不同
# ✅ 好:使用接近生产的数据量
# 使用数据生成工具创建测试数据
5. 监控系统资源
# Prometheus 监控配置
scrape_configs:
- job_name: 'node_exporter'
static_configs:
- targets: ['localhost:9100']
- job_name: 'app_metrics'
static_configs:
- targets: ['app-server:8080']
6. 持续性能测试
# .github/workflows/performance.yml
name: Performance Tests
on:
schedule:
- cron: '0 2 * * *' # 每天凌晨2点
workflow_dispatch:
jobs:
k6:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run K6 test
uses: grafana/k6-[email protected]
with:
filename: tests/performance/load-test.js
- name: Upload results
uses: actions/upload-artifact@v4
with:
name: k6-report
path: summary.json
常见问题
1. 测试结果不稳定
原因:网络波动、测试机资源不足、测试数据不一致
解决方案:
- 多次测试取平均值
- 使用专用测试机
- 确保测试数据一致
2. 测试环境与生产环境差异大
原因:硬件配置、数据量、网络环境差异
解决方案:
- 尽量使测试环境接近生产
- 使用容器化保证环境一致
- 记录环境差异并在分析时考虑
3. 无法模拟真实用户行为
原因:脚本过于简单、缺乏真实数据
解决方案:
- 分析生产日志了解用户行为
- 使用生产数据脱敏后作为测试数据
- 增加随机性和变化
总结
性能测试是确保系统质量的重要环节。关键要点:
- 明确目标:知道要测试什么,为什么测试
- 选择合适的测试类型:负载、压力、尖峰等
- 使用合适的工具:JMeter、K6、Locust 等
- 关注关键指标:响应时间、吞吐量、错误率
- 持续改进:建立基准,持续监控,不断优化