性能测试场景设计实战——基准测试、负载测试、压力测试、稳定性测试
性能测试场景设计实战——基准测试、负载测试、压力测试、稳定性测试
适读人群:后端开发工程师、测试工程师、技术团队负责人 | 阅读时长:约15分钟 | 核心价值:深入理解四种性能测试场景的区别与目的,掌握每种场景的设计要点和判断标准
那次被问住的问题
2020年,我第一次主导双十一前的性能评估。负责人在评审会上问我:
"你做的是什么类型的性能测试?基准测试还是负载测试?压力测试结论是什么?稳定性测试做了吗?"
我当时懵了。我只是"开了一个JMeter脚本,200线程跑了30分钟",根本没想过这些区别。
那次评审没有通过。负责人给了我一句话:"性能测试不是跑脚本,是找答案。不同的问题需要不同类型的测试来回答。"
这句话我记到了现在。
四种测试类型,问的是四个不同的问题:
- 基准测试:系统的原始能力是多少?
- 负载测试:在预期流量下,系统能稳定运行吗?
- 压力测试:系统的极限在哪?超限后会怎样?
- 稳定性测试:系统能在正常负载下长期稳定运行吗?
一、基准测试(Baseline Test)
目的
建立系统性能的参考基线。后续所有优化和对比,都以这个基线为参照。
基准测试回答的问题是:在极低压力下,单次请求的原始性能是多少?
设计原则
- 虚拟用户数:1或极少(1-5)
- 目标:消除并发因素,测量接口本身的响应时间
- 思考时间:0(没有等待)
- 持续时间:能跑出稳定数据即可,通常5-10分钟
k6 基准测试脚本
// baseline_test.js
import http from 'k6/http';
import { check } from 'k6';
import { Trend } from 'k6/metrics';
const responseTrend = new Trend('response_time', true);
export const options = {
vus: 1, // 单用户
iterations: 500, // 跑500次,统计分布
thresholds: {
'http_req_duration': ['p(99)<200'], // 无并发下P99应该很低
},
};
export default function() {
const res = http.get('https://api.example.com/api/products/list', {
headers: { 'Authorization': 'Bearer test-token' },
});
check(res, {
'status 200': (r) => r.status === 200,
'has data': (r) => r.json('data').length > 0,
});
responseTrend.add(res.timings.duration);
}基准测试结论的使用
基准测试的结果是一把尺子:
| 指标 | 基准值(1并发) | 负载测试值(200并发) | 说明 |
|---|---|---|---|
| P50 | 45ms | 132ms | 并发导致2.9倍增长 |
| P99 | 89ms | 389ms | 尾部延迟放大明显 |
| TPS | N/A | 823/s | N/A |
如果负载测试里P99比基准值高出5倍以上,说明有严重的并发竞争(锁、连接池、慢查询),需要专门排查。
二、负载测试(Load Test)
目的
验证系统在预期业务量下的稳定性和性能表现。
负载测试回答的问题是:在正常业务流量下,系统能持续稳定运行吗?
如何确定预期负载
这是很多人搞不清楚的地方。预期负载不是"我随便设200并发",而是从实际数据推算出来的。
推算方法:
步骤1:从业务日志取峰值QPS
比如:某电商大促期间下单接口峰值QPS是800
步骤2:加上安全倍数
安全倍数通常是1.5-2倍(给突发流量留空间)
目标QPS = 800 × 1.5 = 1200
步骤3:换算为并发用户数(如果工具用线程模型)
并发 = QPS × 平均响应时间(秒)
如果P50是200ms,并发 = 1200 × 0.2 = 240线程k6 负载测试脚本
// load_test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
const errorRate = new Rate('error_rate');
const orderLatency = new Trend('order_latency', true);
export const options = {
scenarios: {
normal_load: {
executor: 'constant-arrival-rate',
rate: 1200, // 目标1200 RPS
timeUnit: '1s',
duration: '30m', // 持续30分钟
preAllocatedVUs: 300,
maxVUs: 600,
},
},
thresholds: {
// 所有阈值必须在整个30分钟内持续满足
'http_req_duration': ['p(99)<500', 'p(95)<300'],
'error_rate': ['rate<0.001'], // 错误率 < 0.1%
'order_latency': ['p(99)<1000'],
},
};
export default function() {
// 模拟真实用户行为:有浏览商品的,有搜索的,有下单的
const action = Math.random();
if (action < 0.6) {
// 60%用户只是浏览
browseProducts();
} else if (action < 0.85) {
// 25%用户搜索
searchProducts();
} else {
// 15%用户下单
createOrder();
}
}
function browseProducts() {
const res = http.get('https://api.example.com/api/products?page=1');
errorRate.add(res.status !== 200);
sleep(Math.random() * 3 + 1);
}
function searchProducts() {
const keywords = ['手机', '电脑', '耳机', '手表', '相机'];
const kw = keywords[Math.floor(Math.random() * keywords.length)];
const res = http.get(`https://api.example.com/api/search?q=${kw}`);
errorRate.add(res.status !== 200);
sleep(2);
}
function createOrder() {
const start = Date.now();
const res = http.post(
'https://api.example.com/api/order/create',
JSON.stringify({ productId: Math.floor(Math.random() * 10000) + 1 }),
{ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer token_${__VU}` } }
);
orderLatency.add(Date.now() - start);
errorRate.add(res.status !== 200 || res.json('code') !== 200);
}负载测试的判断标准
负载测试期间重点观察:
- TPS稳定性:TPS曲线是否平稳,还是上下波动
- P99趋势:30分钟内P99是否保持平稳,还是缓慢上涨
- 错误率:是否有偶发错误,还是持续稳定在0
- 资源消耗:CPU、内存、GC是否在合理范围内
关键判断:如果P99在后半段(比如第20-30分钟)比前半段高出20%以上,说明系统在负载下有资源积累问题(比如内存泄漏、连接池逐渐被占满),需要进行稳定性测试进一步验证。
三、压力测试(Stress Test)
目的
找到系统的性能边界,明确最大承载能力,理解系统在超限时的行为。
压力测试回答的问题是:系统最多能扛多少?超限后是优雅降级还是直接崩溃?
设计原则——阶梯加压
初始并发(通常是预期负载的50%)
↓ 每5分钟增加一档
每次增加量约为预期负载的25%
↓ 持续增加
找到:TPS开始下降或P99开始发散的拐点
↓
这个拐点的前一档就是系统的"安全最大容量"压力测试脚本
// stress_test.js
import http from 'k6/http';
import { check } from 'k6';
import { Rate } from 'k6/metrics';
const errorRate = new Rate('error_rate');
export const options = {
scenarios: {
stress_ramp: {
executor: 'ramping-arrival-rate',
startRate: 200, // 从200 RPS开始
timeUnit: '1s',
stages: [
{ duration: '5m', target: 200 }, // 200 RPS,预热5分钟
{ duration: '5m', target: 400 }, // 加压到400 RPS
{ duration: '5m', target: 600 }, // 加压到600 RPS
{ duration: '5m', target: 800 }, // 加压到800 RPS
{ duration: '5m', target: 1000 }, // 加压到1000 RPS
{ duration: '5m', target: 1200 }, // 加压到1200 RPS
{ duration: '5m', target: 1500 }, // 继续加压
{ duration: '5m', target: 2000 }, // 极限测试
{ duration: '5m', target: 200 }, // 降压,观察恢复
],
preAllocatedVUs: 500,
maxVUs: 2000,
},
},
};
export default function() {
const res = http.post(
'https://api.example.com/api/order/create',
JSON.stringify({ productId: __VU % 10000 + 1, quantity: 1 }),
{ headers: { 'Content-Type': 'application/json' } }
);
check(res, { 'success': (r) => r.status === 200 });
errorRate.add(res.status !== 200);
}压力测试结果解读
一个标准的压力测试结果可能是这样的:
| RPS | TPS实测 | P50 | P99 | 错误率 | 状态 |
|---|---|---|---|---|---|
| 200 | 198 | 45ms | 120ms | 0% | 正常 |
| 400 | 396 | 52ms | 145ms | 0% | 正常 |
| 600 | 598 | 68ms | 198ms | 0% | 正常 |
| 800 | 793 | 89ms | 280ms | 0% | 正常 |
| 1000 | 987 | 125ms | 420ms | 0.1% | 轻微压力 |
| 1200 | 1150 | 198ms | 780ms | 0.8% | 明显压力 |
| 1500 | 1180 | 450ms | 3200ms | 5.2% | 性能拐点 |
| 2000 | 980 | 1200ms | 8900ms | 23% | 超限,服务降级 |
结论:系统安全承载上限是1200 RPS,超过后P99迅速发散,错误率飙升。1000 RPS时性能良好,可以作为大促流量控制的上限参考。
最关键的观察:降压恢复阶段。当从2000 RPS降回200 RPS后,系统能否在5分钟内恢复到正常指标?如果不能(比如P99还是很高、线程一直在等待),说明系统有资源没有释放干净,需要排查。
四、稳定性测试(Soak Test / Endurance Test)
目的
发现长时间运行才会暴露的问题:内存泄漏、连接泄漏、慢慢积累的锁竞争。
稳定性测试回答的问题是:系统能在正常负载下运行数小时乃至数天而不退化吗?
设计原则
- 并发/RPS:使用正常业务负载的70-80%(不是最大值,目的不是找边界)
- 持续时间:最少4小时,完整测试8-24小时
- 重点监控:JVM堆内存(是否持续上涨)、Full GC次数、数据库连接池使用率、线程数
典型的稳定性问题现象
内存泄漏: JVM堆内存图像是"锯齿状向上倾斜"——每次GC后恢复,但基线不断升高。最终触发Full GC,Stop-The-World导致P99飙升,严重时OOM。
连接泄漏: 数据库连接池的active连接数缓慢增加,永远不降到0。最终连接耗尽,所有请求超时等待连接,TPS骤降。
ThreadLocal未清理: 每次请求往ThreadLocal里存了数据,用完没清理。线程池里的线程会一直持有旧请求的数据,内存无法回收。
稳定性测试监控指标
压测期间需要同时监控(以JVM应用为例):
JVM监控(每分钟记录):
- 堆内存使用量(heap used)
- 非堆内存(metaspace)
- GC次数和时长(Young GC / Full GC)
- 线程数(live threads)
- 类加载数(loaded classes)
连接池监控:
- active connections
- idle connections
- pending connections(等待获取连接的请求数)
业务指标(每分钟记录):
- TPS
- P99响应时间
- 错误率判断稳定的标准:8小时测试结束后,各项指标与第1小时相比,误差在10%以内。
四种测试类型关系总结
↑ 并发/RPS
稳定性测试 | (正常负载,长时间)
|
|
负载测试 | (预期负载,30分钟-2小时)
|
|
压力测试 |___________→ 时间
(阶梯加压找边界)
基准测试(贯穿始终,1并发的基线)踩坑实录
坑1:基准测试的数据集太小导致基线虚假
现象: 基准测试P99是15ms,但负载测试P99直接到了800ms。差距超过50倍,看起来并发影响极大。
原因: 基准测试时数据库只有几百条记录,全部缓存在Buffer Pool里,查询是纯内存操作。负载测试切换到了有5000万条数据的测试环境,基线完全不可比。
解法: 基准测试必须在与负载测试相同的数据环境下进行,否则基线没有参考价值。
坑2:压力测试忘测降压恢复阶段
现象: 压力测试找到了1200 RPS的拐点,测试结束就收工了。但上线后有几次流量突增超过1200 RPS,服务恢复后P99一直很高,过了30分钟才正常。
原因: 没有测试系统在超限后的自恢复能力。实际是连接池在压力期间有连接泄漏,降压后残留的坏连接一直占着池子里的名额,导致恢复很慢。
解法: 压力测试脚本最后必须加一个降压阶段,持续观察系统从过载恢复到正常的时间和过程。
坑3:稳定性测试中途JVM重启未被发现
现象: 稳定性测试跑了6小时,报告显示各项指标正常。但事后查日志发现:测试跑到第4小时,应用服务器触发了OOM,进程崩溃了,但Kubernetes自动重启了Pod,几秒内就恢复了,压测脚本没有报错。
原因: 自动重启掩盖了OOM的问题。从压测数据来看,第4小时有一小段时间错误率略高,但因为恢复太快被忽略了。
解法: 稳定性测试必须同时监控应用进程的生命周期,任何崩溃重启都是测试失败,不能被自动恢复掩盖。在Kubernetes环境里,监控Pod的restartCount变化。
总结
四种测试,四个问题:
- 基准测试:建立基线,后续对比的参照物
- 负载测试:验证预期负载下的稳定性
- 压力测试:找到性能上限和超限行为
- 稳定性测试:发现长期运行的隐患
每次大促前,至少要完成这四种测试,缺一不可。
