1. 压力测试到底在测什么:从一次线上事故说起
很多人第一次接触压力测试,是因为线上出了事。我印象很深的一次,某服务在白天流量正常时一切平稳,到了晚上某个活动开始,接口响应时间从 80ms 一路飙到 3 秒,最后整个服务雪崩。事后复盘发现,问题不在代码逻辑,而在于没人知道这套系统到底能扛多少并发。压力测试要回答的,就是这个问题:系统在多大的压力下会开始变慢、出错、崩溃,以及崩溃前有哪些征兆。
压力测试(Stress Testing)本质上是给系统施加超出正常范围的负载,观察它的行为表现。它和性能测试(Performance Testing)、负载测试(Load Testing)经常被混着说,但侧重点不同。负载测试关注的是"在预期负载下系统表现如何",压力测试关注的是"把系统压到极限甚至压垮,看它怎么反应"。打个比方,负载测试是看一辆车在正常载重下跑高速稳不稳,压力测试是往车上不断加货,直到悬挂变形、轮胎冒烟,看它先坏哪个部件。
这件事的价值在于三点。第一,找到容量上限,你知道系统能扛 5000 QPS 还是 50000 QPS,扩容和限流才有依据。第二,暴露瓶颈位置,是数据库连接池不够、是线程池打满、还是某个下游接口拖垮了整条链路。第三,验证容错与恢复能力,系统被压垮后能不能自动恢复,限流降级策略是否真的生效。
适合读这篇内容的人,包括刚接手性能测试任务的测试工程师、需要给系统做容量规划的后端开发、以及负责线上稳定性的运维同学。哪怕你之前没做过压力测试,只要跟着思路走,也能搭出一套能用的压测方案。下面我会从工具选型、脚本编写、场景设计、指标解读、瓶颈定位到避坑经验,一步步拆开讲。
2. 压测工具怎么选:别一上来就上重型武器
2.1 三类工具的适用边界
压测工具大致分三类,选错了会白白浪费时间。
第一类是轻量命令行工具,比如 ab、wrk、hey。它们的特点是上手极快,一条命令就能发压,适合快速验证单个接口的吞吐能力。缺点是场景能力弱,做不了复杂的参数化、关联、思考时间模拟。我通常用 wrk 做第一轮摸底,几十秒就能知道这个接口大概什么水平。
第二类是脚本化压测平台,代表是 JMeter、Locust、k6。JMeter 基于 GUI,功能全但偏重;Locust 和 k6 用代码写脚本,灵活度高,适合放进 CI 流程。这类工具能模拟真实用户行为链路,支持参数化、断言、分布式压测,是绝大多数项目的首选。
第三类是云原生压测服务,按量付费,能快速拉起大规模压测集群。适合临时需要几十万并发、又不想自己维护机器的情况。但成本要算清楚,长时间压测费用不低。
| 工具 | 脚本方式 | 分布式 | 适合场景 | 学习成本 |
|---|---|---|---|---|
| wrk | 命令行/Lua | 弱 | 单接口快速摸底 | 低 |
| ab | 命令行 | 无 | 简单 GET 压测 | 极低 |
| JMeter | GUI/XML | 强 | 复杂业务链路 | 中 |
| Locust | Python | 强 | 代码化、CI 集成 | 中 |
| k6 | JavaScript | 强 | 云原生、脚本化 | 中 |
2.2 选型时最容易忽略的两个点
第一个点是压测机自身的瓶颈。很多人压测时发现 QPS 上不去,以为是服务端问题,结果一查是压测机 CPU 跑满了、或者端口耗尽。压测机的配置、网络带宽、文件描述符上限,都会成为隐形天花板。经验做法是:先用小并发确认压测机能跑出预期流量,再逐步加压。
第二个点是协议支持。如果你的接口是 gRPC、WebSocket 或者自定义 TCP 协议,很多轻量工具直接不支持。这时候要么选支持该协议的工具,要么自己写压测客户端。我见过有人硬用 HTTP 工具去压 gRPC 服务,测出来的数据完全没参考价值。
提示:选工具前先明确三件事——压什么协议、要模拟多复杂的场景、压测机资源够不够。这三点定了,工具基本就定了。
2.3 我的默认组合
实际项目里,我一般用"wrk 摸底 + Locust 做正式压测"的组合。wrk 花五分钟确认接口基线,然后用 Locust 写完整的用户行为脚本,跑阶梯加压和持续压测。Locust 的好处是脚本就是 Python 代码,参数化、断言、自定义指标都很自然,而且分布式部署简单,加 worker 就能扩压测能力。
3. 压测脚本编写:让虚拟用户像真人一样操作
3.1 从单接口到业务链路
新手写压测脚本,往往只压一个接口,比如GET /api/user/1。这样测出来的数字好看,但没意义,因为真实用户不会只调一个接口。真实场景是:登录 → 拉取列表 → 查看详情 → 提交操作,中间还有思考时间。
以 Locust 为例,一个贴近真实的脚本大概长这样:
from locust import HttpUser, task, between import random class ShopUser(HttpUser): wait_time = between(1, 3) # 模拟用户思考时间 def on_start(self): # 每个虚拟用户启动时先登录 resp = self.client.post("/api/login", json={ "username": f"user{random.randint(1, 10000)}", "password": "test123" }) self.token = resp.json()["token"] @task(5) def browse_list(self): self.client.get("/api/products?page=1", headers={"Authorization": self.token}) @task(2) def view_detail(self): pid = random.randint(1, 1000) self.client.get(f"/api/products/{pid}", headers={"Authorization": self.token}) @task(1) def submit_order(self): self.client.post("/api/orders", json={"product_id": random.randint(1, 1000), "count": 1}, headers={"Authorization": self.token})这里有几个关键设计。wait_time模拟用户操作间隔,没有它压测就是机器疯狂刷接口,和真实流量差距巨大。@task(5)里的数字是权重,表示浏览列表的操作比下单频繁 5 倍,这符合真实业务比例。on_start里做登录,保证每个虚拟用户有独立会话。
3.2 参数化与数据隔离
压测数据不能全用同一个用户、同一个商品 ID,否则会撞上缓存和锁,测出来的不是真实性能。参数化要做到:用户维度隔离、数据维度分散。
用户维度隔离,就是每个虚拟用户用不同的账号登录,避免会话互相干扰。数据维度分散,就是查询和操作的对象 ID 要随机分布,覆盖不同的数据分片和缓存 key。
但这里有个坑:如果参数化数据是提前生成的固定集合,多轮压测会反复命中同样的数据,缓存命中率虚高。更接近真实的方式是让数据有一定随机性,或者每轮压测前重置数据状态。
3.3 断言与失败处理
压测不只是看 QPS,还要看成功率。脚本里必须加断言,判断返回码和业务状态。比如下单接口返回 200 不代表成功,可能 body 里是{"code": 500, "msg": "库存不足"}。
with self.client.post("/api/orders", json=payload, catch_response=True) as resp: if resp.status_code != 200: resp.failure(f"HTTP {resp.status_code}") elif resp.json().get("code") != 0: resp.failure(f"业务失败: {resp.json().get('msg')}")catch_response=True让你能手动标记成功或失败。如果不加断言,压测报告里成功率 100%,但实际上全是业务错误,这种数据会误导决策。
注意:压测脚本里的断言要和真实监控口径一致。线上怎么判断请求成功,压测就怎么判断,否则两边数据对不上。
4. 压测场景设计:阶梯、峰值、浸泡各有各的用法
4.1 阶梯加压找拐点
阶梯加压是最常用的场景。从低并发开始,每隔一段时间增加一批用户,观察 QPS 和响应时间的变化。理想情况下,QPS 随并发上升,响应时间保持平稳;当并发超过某个点,QPS 不再增长甚至下降,响应时间陡增,这个点就是性能拐点,也就是系统的容量上限。
具体做法:从 50 并发开始,每 2 分钟加 50,一直加到 500 或直到错误率超过阈值。记录每个阶段的 QPS、P95 响应时间、错误率。拐点通常出现在响应时间开始非线性上升的位置。
4.2 峰值冲击测弹性
峰值冲击模拟突发流量,比如秒杀开始、活动开抢。做法是在很短时间内(比如 10 秒)把并发从 0 拉到很高,维持几分钟,再快速降下来。这个场景主要验证:系统的自动扩容跟不跟得上、限流策略会不会误伤、队列会不会积压。
我遇到过系统在阶梯加压下表现良好,但一遇峰值冲击就大量超时,原因是连接池扩容有延迟,突发流量瞬间把等待队列打满。这种问题只有峰值场景能暴露。
4.3 浸泡测试抓内存泄漏
浸泡测试(Soak Testing)是用中等压力持续跑很长时间,几小时到几十小时。它不追求高并发,而是看系统在长时间运行下会不会出现内存泄漏、连接泄漏、日志撑爆磁盘等问题。
判断内存泄漏的方法很直接:观察压测期间服务进程的内存曲线。如果内存持续上升且不回落,即使 GC 后也降不下来,基本就是泄漏。这类问题在短时间压测里完全看不出来。
| 场景 | 加压方式 | 持续时间 | 主要目的 |
|---|---|---|---|
| 阶梯加压 | 逐步递增 | 10-30 分钟 | 找容量拐点 |
| 峰值冲击 | 瞬间拉高 | 5-15 分钟 | 测弹性与限流 |
| 浸泡测试 | 中等恒定 | 数小时以上 | 抓泄漏与稳定性 |
| 混合场景 | 按业务比例 | 按需 | 贴近真实流量 |
4.4 场景比例要贴近真实
如果只做单一场景,测出来的容量是偏乐观的。真实流量是混合的:读多写少、不同接口比例不同。设计混合场景时,要参考线上真实的接口调用比例。这个比例可以从网关日志或 APM 系统里统计出来,按比例配置到压测脚本的 task 权重里。
5. 压测指标怎么看:别只盯着 QPS
5.1 四个核心指标
压测报告里最重要的四个指标是:QPS、响应时间、错误率、并发数。它们之间的关系不是孤立的。
QPS 是每秒处理的请求数,反映吞吐能力。响应时间是请求从发出到收到响应的时间,通常看平均值、P95、P99。错误率是失败请求占比。并发数是同时在压的虚拟用户数。
关键认知是:QPS 和响应时间要一起看。同样 1000 QPS,P99 是 100ms 和 P99 是 2s,系统健康度天差地别。只看 QPS 会掩盖长尾延迟问题。
5.2 为什么平均值会骗人
平均值是最容易误导人的指标。假设 100 个请求里 99 个是 10ms,1 个是 5s,平均值才 60ms,看起来很好,但那个 5s 的请求对用户体验是灾难。所以一定要看分位数,P95 表示 95% 的请求在这个时间以内,P99 表示 99% 的请求在这个时间以内。
线上 SLA 通常按 P99 或 P999 来定。压测时如果只报平均值,等于把最慢的那部分用户藏起来了。
5.3 错误率的分类
错误率不能只看总数,要分类。大致分几种:连接超时、读超时、HTTP 5xx、业务错误码、连接被拒绝。不同错误指向不同问题。连接被拒绝通常是服务端 backlog 满了;读超时可能是下游慢;5xx 是服务端异常。压测报告里把错误分类列出来,定位问题会快很多。
提示:压测时同步打开服务端的监控面板,把 QPS、响应时间、CPU、内存、GC、连接数放在一起看,单看压测工具的数字会漏掉很多信息。
6. 瓶颈定位:从压测数据反推系统短板
6.1 先看资源层
压测中 QPS 上不去,第一步看服务端资源。CPU 是否打满、内存是否吃紧、磁盘 IO 是否饱和、网络带宽是否跑满。用 top、vmstat、iostat 这些基础命令就能快速判断。
如果 CPU 打满,要看是用户态还是内核态。用户态高通常是业务计算密集;内核态高可能是频繁系统调用或网络软中断。如果 CPU 没满但 QPS 上不去,问题往往在等待——等数据库、等下游、等锁。
6.2 再看中间件与依赖
服务端资源正常,就往依赖看。数据库是最常见的瓶颈:慢查询、连接池不够、锁竞争。压测时观察数据库的活跃连接数、慢查询日志、锁等待。
连接池配置是个高频坑。比如服务端线程池 200,数据库连接池只有 20,那 180 个线程都在等连接。压测时表现为响应时间随并发上升而线性增长,但数据库 CPU 并不高。
6.3 最后看应用内部
应用内部常见瓶颈:线程池打满、GC 频繁、锁竞争、日志同步写。GC 问题在压测中很典型,表现为响应时间周期性抖动,配合 GC 日志能看到 Full GC 频繁触发。
定位这类问题,APM 工具或者火焰图很有用。火焰图能直观看到 CPU 时间花在哪个方法上,是排查热点代码的利器。
6.4 一个完整的排查链路示例
假设压测发现 QPS 卡在 3000 上不去,响应时间从 50ms 涨到 800ms。排查顺序:
- 看服务端 CPU,发现只有 40%,排除计算瓶颈。
- 看数据库,活跃连接数打满,慢查询增多。
- 查慢查询,发现某个列表查询没走索引,全表扫描。
- 加索引后重压,QPS 升到 8000,响应时间回落到 60ms。
这个链路说明:压测的价值不只是给个数字,而是逼出平时隐藏的问题。很多慢查询在低流量下不显现,一压就暴露。
7. 实操中的坑与经验:这些文档里不会写
7.1 压测环境不等于生产环境
最常见的错误是在一套缩水环境上压测,然后拿结果去推算生产容量。压测环境如果机器配置、数据量、网络拓扑和生产不一致,结果没有参考价值。理想情况是在生产同构环境压测,或者至少保证关键配置一致。
如果只能用缩水环境,要明确标注结论的适用范围,别直接线性放大。系统容量往往不是线性的,数据量一大,索引效率、缓存命中率都会变。
7.2 压测数据要预热
冷启动状态下压测,前几分钟的数据不可信。JIT 编译、缓存预热、连接池建立都需要时间。正确做法是先低并发跑几分钟预热,等各项指标稳定后再开始正式加压和记录。
7.3 别在业务高峰期压测生产
如果确实要在生产环境压测,务必避开业务高峰,并且做好限流和熔断兜底。压测流量要能识别和隔离,避免污染真实用户数据。我见过压测往生产库写了几万条测试订单,清理起来非常麻烦。
7.4 监控要提前对齐
压测前确认监控能覆盖:应用层指标、中间件指标、系统层指标、链路追踪。压测过程中实时观察,压测后能回溯。如果压测时才发现某个关键指标没采集,这轮压测基本白做。
7.5 记录每轮压测的配置
压测要可复现。每轮压测记录:脚本版本、并发数、持续时间、环境配置、数据状态、结果指标。否则过几天回头看数据,根本不知道当时是什么条件跑出来的。
| 常见坑 | 表现 | 应对 |
|---|---|---|
| 压测机瓶颈 | QPS 上不去但服务端很闲 | 先验证压测机能力 |
| 无思考时间 | 数据虚高 | 加 wait_time |
| 数据不隔离 | 缓存命中虚高 | 参数化分散数据 |
| 冷启动 | 前几分钟数据差 | 先预热再记录 |
| 环境缩水 | 结论不可用 | 同构环境或标注范围 |
8. 把压测变成常规能力:持续压测与容量基线
压测做一次不难,难的是让它持续产生价值。我的做法是把压测脚本纳入代码仓库,和业务代码一起维护。每次大版本发布前,跑一轮基准压测,对比历史基线。如果 QPS 下降超过 10% 或 P99 上升超过 20%,就阻断发布,先查原因。
容量基线是另一件值得做的事。把系统在不同并发下的 QPS、响应时间、资源占用记录下来,形成一张容量表。这样线上流量增长时,你能提前判断还有多少余量,什么时候该扩容。
持续压测的关键是自动化和可比性。脚本要能一键跑,环境要尽量稳定,指标口径要统一。做到这三点,压测才不是一次性任务,而是稳定性保障的常规手段。
最后分享一个我踩过的坑:有次压测报告显示系统能扛 2 万 QPS,上线后实际到 8000 就开始超时。排查发现压测时用的是固定的一批热点数据,缓存命中率接近 100%,而真实流量数据分散,缓存命中率只有 60%。从那以后,我压测时一定让数据分布贴近真实,宁可数字难看一点,也要保证结论可信。压测的意义从来不是跑出一个漂亮的数字,而是提前知道系统会在哪里出问题。