"我们的系统能支撑多少人同时在线?"这个问题几乎所有业务方都问过,但它其实不是一个合格的需求——"在线"的人和"正在发请求"的人是两回事,而后者才是服务器关心的。一个一万用户在线的论坛,实际同时在请求的用户可能只有几十个。
压力测试的价值就在于把这类模糊的问题翻译成可度量的数字:在目标响应时间下系统能承载多少 QPS,超过多少 QPS 之后延迟开始劣化,瓶颈在哪一层。这篇文章讲清楚压测的核心概念(尤其是 Little's Law 这个能把所有概念串起来的公式)、五款主流工具的定位与选择、wrk 的实战用法与结果解读、压测环境和流程的正确做法、怎么判断瓶颈在哪一层、常见的坑(包括必须强调的一条底线),以及一份压测报告应该包含什么。
一、五个核心概念与它们之间的关系
响应时间(Response Time / Latency)
从发出请求到收到完整响应的耗时。必须注意两件事:
第一,永远不要用平均值。 平均值会被大量快请求掩盖掉长尾。正确做法是看分位数:
| 指标 | 含义 | 为什么用它 |
|---|---|---|
| P50(中位数) | 50% 的请求快于此值 | 典型体验 |
| P95 | 95% 的请求快于此值 | 大部分用户的体验下限 |
| P99 | 99% 的请求快于此值 | 最差 1% 用户的体验,通常也是最先出问题的地方 |
| P99.9 | 极长尾 | 大流量系统才需要关注 |
如果 100 个请求里 99 个是 20ms、1 个是 5 秒,平均值只有 70ms 看起来很好,但那 1% 的用户体验到的是 5 秒白屏。这就是为什么 SLA 通常用 P99 而不是平均值来承诺。
第二,区分客户端耗时与服务端耗时。 用户感受到的时间包含 DNS 解析、TCP 握手、TLS 握手、网络传输、服务端处理、以及页面的资源加载。压测工具的测量点决定了它度量的是哪一段。
QPS / TPS
- QPS(Queries Per Second):每秒处理的请求数。Web 场景常用。
- TPS(Transactions Per Second):每秒完成的事务数,一个事务可能包含多个请求。交易类场景常用。
日常讨论中两者经常混用,重要的是明确计数口径——是对所有接口的请求计数,还是只对某个关键接口计数。
并发用户数 / 并发连接数
这是最容易和 QPS 混淆的概念。两者不是一回事,也不存在倍数关系,它们通过响应时间联系在一起。
Little's Law:把所有概念串起来的公式
这是排队论里最基础也最有用的一条定律:
L = λ × W
其中:
L = 系统中平均的请求数(并发数)
λ = 请求的到达速率(QPS)
W = 每个请求在系统中的平均停留时间(响应时间)
三个量知道任意两个就能算第三个。举例:
例1:QPS = 1000,平均响应时间 = 50ms = 0.05s
并发数 L = 1000 × 0.05 = 50
→ 每秒一千的请求,平均每个停留 50 毫秒,所以任意时刻系统里平均只有 50 个请求
例2:某系统支持 10000 并发连接,响应时间 200ms
QPS = 10000 / 0.2 = 50000
例3:目标 QPS 5000,希望响应时间控制在 100ms 以内
需要的最小并发数 = 5000 × 0.1 = 500
→ 压测时并发至少要压到 500 才能达到目标 QPS
这个公式的实际用途:
- 把"能支持多少并发"的宣传翻译成实际 QPS(很多宣传只说并发数不说响应时间,本身没有意义)。
- 计算压测该用多少并发才能达到目标 QPS(例 3)。这是设置压测参数的关键。
- 反过来验证结果是否合理:如果并发 100、响应时间 500ms,那 QPS 必然是 200 左右,测出 2000 一定有问题。
错误率与吞吐量的关系
错误率包括:HTTP 5xx、4xx(视场景)、连接失败、超时。错误率上升往往发生在到达容量上限之后——这一点很重要,因为很多人在压测时看 QPS 上不去了就认为到了瓶颈,但更明确的信号是错误率开始抬头的那个点。
二、工具选型:五款工具的定位
| 工具 | 类型 | 优点 | 局限 | 适合场景 |
|---|---|---|---|---|
| ab | 命令行 | 系统自带概率高,一条命令就跑 | 只能测单个 URL,不支持场景脚本,HTTP/1.1 keepalive 支持有限,高并发时自身可能是瓶颈 | 快速看单个接口的基本能力 |
| wrk | 命令行 | 单机性能极强(多线程 + epoll),支持 Lua 脚本定制请求 | Windows 需自行编译,Lua 脚本有一定学习成本 | 命令行压测的首选 |
| JMeter | GUI + CLI | 功能全面,场景编排强,有丰富的监听器和插件,支持分布式 | 资源占用大(Java),单机并发能力有限,XML 配置繁琐 | 复杂业务流程、需要团队协作和完整报告 |
| locust | Python 代码 | 用 Python 写场景,分布式支持好,有 Web UI | Python GIL 导致单 worker 性能有限,需要多进程 | 场景复杂、团队熟悉 Python、需要分布式施压 |
| k6 | JavaScript 代码 | 性能好(Go 实现),脚本用 JS,原生支持阈值断言,适合接入 CI | 生态相对年轻 | 希望在 CI 里跑自动化性能回归 |
选择建议:
- 只想快速知道某个接口能撑多少 QPS → wrk。
- 要模拟真实的业务流程(登录 → 浏览 → 加购 → 下单) → JMeter、locust 或 k6。
- 想接入 CI 做性能回归,防止代码变慢 → k6 或 locust(都支持用代码定义场景和阈值)。
- 临时摸底,机器上什么都没装 → ab。
不建议用 wrk 或 ab 去做复杂业务压测,因为它们只能对 URL 发请求,无法处理 token 获取、动态参数、关联这些真实场景里必需的东西。用工具做它擅长的事。
三、wrk 实战
安装:
# RHEL、Rocky、CentOS
sudo dnf install -y git gcc make openssl-devel
git clone https://github.com/wg/wrk.git && cd wrk && make
sudo cp wrk /usr/local/bin/
# Debian、Ubuntu
sudo apt install -y build-essential libssl-dev git
git clone https://github.com/wg/wrk.git && cd wrk && make
sudo cp wrk /usr/local/bin/
基础用法:
wrk -t4 -c200 -d60s --latency http://target.example.com/api/list
参数说明:
| 参数 | 含义 | 建议 |
|---|---|---|
| -t | 压测线程数 | 建议等于压测机 CPU 核数,或不指定(默认按核数) |
| -c | 保持的 HTTP 连接数(注意不是"用户数") | 关键参数,见下文 |
| -d | 测试时长 | 至少 60s,最好 3~5 分钟 |
| --latency | 输出详细的延迟分布百分比 | 强烈建议加上 |
| -s | 加载 Lua 脚本 | 定制请求、处理动态参数 |
| --timeout | 超时阈值 | 建议显式设置,默认很长 |
| -R | 限速模式,指定目标 QPS(部分版本支持) | 需要稳定施压节奏时用 |
-c 的确切含义是"每个线程建立并保持的 TCP 连接总数",wrk 的总连接数是 -c 的值(在所有线程间分配),而不是 -t × -c。这一点和另一些小工具不同,容易搞错。
看懂输出结果:
Running 1m test @ http://target.example.com/api/list
4 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 48.21ms 21.33ms 402.15ms 92.14%
Req/Sec 1.04k 112.35 1.82k 88.32%
Latency Distribution
50% 45.12ms
75% 52.30ms
90% 61.44ms
99% 95.28ms
249183 requests in 1.00m, 61.24MB read
Socket errors: connect 0, read 0, write 0, timeout 12
Requests/sec: 4153.05
Transfer/sec: 1.02MB
逐行解读:
Latency行:延迟的平均值、标准差、最大值、以及落在 ±1 标准差内的比例。标准差大说明延迟不稳定(可能有 GC、锁竞争或部分请求走了慢路径)。Req/Sec行:每个线程的 QPS。注意这是每线程的,乘以-t约等于总 QPS。Latency Distribution:关键行。上面例子里 P50 是 45ms 而 P99 是 95ms,差距约 2 倍,还算健康。如果 P99 是 P50 的 10 倍,说明长尾严重,要查慢路径(比如缓存没命中时的查库逻辑)。requests in 1.00m:总请求数。用总请求数 / 60 得到的平均值与下面的Requests/sec应当一致。Socket errors:必须盯着这一列。如果 connect/read/write/timeout 不为 0,说明已经有请求失败了。很多压测报告里 QPS 很高但错误率 20%,这样的数字没有意义。Requests/sec:这台机器在这个并发下达到的 QPS。
交叉验证 Little's Law:上面例子里 QPS 是 4153、平均延迟 48ms,那么平均并发 = 4153 × 0.048 ≈ 199,正好等于 -c 200。这是符合预期的,说明系统没有严重排队。
用好 Lua 脚本(动态参数、POST、鉴权场景必备):
-- post.lua
wrk.method = "POST"
wrk.headers["Content-Type"] = "application/json"
wrk.headers["Authorization"] = "Bearer xxxxxx"
local counter = 0
request = function()
counter = counter + 1
local body = string.format('{"sku_id":%d,"qty":1}', 10000 + (counter % 100))
return wrk.format("POST", "/api/order", nil, body)
end
-- 可选:延迟波动、只对特定响应做處理
response = function(status, headers, body)
if status ~= 200 then
-- 统计非 200 响应
end
end
-- 可选:输出自定义统计
done = function(summary, latency, requests)
io.write("自定义统计:总请求 " .. summary.requests .. "\n")
end
wrk -t4 -c200 -d120s -s post.lua --latency http://target.example.com/api/order
四、压测环境与流程
这一节的重要性不亚于工具本身,因为大多数无效的压测败在方法而不是工具上。
环境要求
| 要求 | 说明 | 违反后果 |
|---|---|---|
| 与生产同配置 | 同样的 CPU、内存、磁盘类型、内核版本、中间件版本与配置 | 结论完全不可用 |
| 独立部署 | 不与生产共享任何资源(尤其是数据库、缓存、网络出口) | 把生产打挂 |
| 数据规模一致 | 数据量、数据分布要接近生产 | 缓存命中率和索引表现完全不同 |
| 网络条件可控 | 压测机与被测机之间延迟稳定 | 延迟数据失真 |
关于"必须隔离环境"这一点要单独强调:绝对不要把压测流量打到生产库。 原因不是道听途说的风险,而是非常具体的后果:
- 生产库会被写脏数据,清理成本极高甚至无法完全回滚。
- 可能触发生产库的连接数、锁、主从延迟问题,直接影响真实用户。
- 可能造成 binlog 暴涨、磁盘写满,引发更大范围的故障。
- 可能触发生产环境的自动扩容或限流策略,产生意外成本或副作用。
如果不得不在生产环境做压测(比如全链路压测),必须满足:使用影子库/影子表(压测数据写入独立的存储)、流量染色以便中间件和监控识别、明确的时间窗口和中止预案、以及业务方的书面同意。这是一个完整的工程方案,不是简单地多开几个压测线程。
标准流程
第一步:确定目标。 先明确"什么叫合格"。例如:目标 QPS 5000,P95 响应时间 < 200ms,错误率 < 0.1%。没有目标的压测只能得到一堆无法判断好坏的数字。
第二步:预热(warm-up)。 这是最常被跳过的步骤。JVM 需要预热(JIT 编译、类加载)、缓存需要预热(Redis、数据库 buffer pool、CDN)、连接池需要建立。没预热的第一次压测结果通常会显著偏低,甚至差几倍。
# 先低压力跑 1~2 分钟预热
wrk -t2 -c50 -d120s http://target/api/list
# 再正式压测
wrk -t4 -c200 -d300s --latency http://target/api/list
第三步:逐步加压。 不要一上来就 10000 并发。按几何级数递增:100 → 200 → 400 → 800 → 1600,每档稳定跑 2~5 分钟。这样能得到完整的"并发 vs QPS / 延迟"曲线,才能找到拐点。
第四步:每档同时采集服务端资源。 这一点极其关键——只看压测工具的输出,你只知道"上不去了",不知道"为什么上不去"。
# 压测同时在服务端运行
vmstat 1 > /tmp/vmstat.log &
pidstat -u 1 > /tmp/cpu.log &
iostat -x 1 > /tmp/iostat.log &
sar -n DEV 1 > /tmp/net.log &
# 应用侧同步观察
curl -s http://127.0.0.1/status # Nginx stub_status
jstat -gcutil JAVA_PID 1000 # JVM GC 情况
第五步:记录与分析。 把每档的 QPS、P95/P99、错误率、以及服务端资源数据整理成表格。
第六步:复现验证。 关键结论要能重复跑出来至少两次,避免把偶然当成结论。
五、怎么判断瓶颈在哪一层
压到 QPS 上不去的时候,用下面的对照表找方向:
| 现象 | 瓶颈位置 | 下一步动作 |
|---|---|---|
| QPS 涨不上,应用 CPU 已接近 100% | 应用计算能力 | perf top 找热点函数,优化算法或水平扩容 |
| CPU 不高但 Load 很高 | IO 或锁 | vmstat 看 b 列、iostat -x 看 await |
| await 明显升高、磁盘 IO 打满 | 磁盘 | 查慢 IO 来源:数据库刷盘、日志、swap;考虑 SSD |
| 网络带宽达到网卡上限 | 网络带宽 | 加压缩、CDN 卸载静态资源、升级网卡 |
| 出现大量 TIME_WAIT / 端口耗尽 | 网络参数或代理配置 | 配 upstream keepalive、扩大端口范围、tcp_tw_reuse |
| 数据库连接池打满、等待时间长 | 数据库连接池或慢查询 | 查慢 SQL、加索引;考虑扩大连接池或读写分离 |
| Redis 延迟升高、命中率下降 | 缓存层 | 查大 key、热 key、内存淘汰策略、网络往返 |
| 错误率随并发上升,响应很快 | 限流/队列/连接被拒 | 查 backlog、连接数限制、限流配置、线程池队列 |
| 加机器后 QPS 没有线性提升 | 存在串行或共享瓶颈 | 通常是数据库、共享缓存、或分布式锁 |
最后两条特别值得注意。 "错误率上升但响应很快"说明请求是被拒绝而不是被慢处理出来的,方向应该在连接队列、限流、线程池配置上,而不是去优化代码性能。"加了机器 QPS 不涨"说明系统里有一个无法水平扩展的环节,通常是数据库的写瓶颈或者某个全局锁。
用一个简单的观察法定位:看四类资源里哪个先到达饱和或接近上限。 资源都没到上限但 QPS 上不去 → 问题在应用内部或依赖服务;某一类资源到顶了 → 那就是瓶颈。
六、常见坑与注意事项
坑一:客户端先成为瓶颈。 压测机自己先撑不住,导致压出来的数据是客户端的上限而不是服务端的。判断方法:压测时看压测机的 CPU(top,如果是 wrk 应该接近但不超过核数×100%)、看 wrk 输出的 Req/Sec 是否随 -t 增加而线性增长。如果增加并发 QPS 不再上升,同时压测机 CPU 已经满了,那就是客户端瓶颈。 解决办法:换更高配的压测机、多台压测机分布式施压、或者换用性能更好的工具(k6 的每连接开销比 JMeter 低很多)。
坑二:没预热。 第一次压测的结果通常明显偏低。JVM 应用的 JIT 编译要几千次调用后才生效,类加载和连接池建立都在头几分钟。必须预热后再开始计数。
坑三:没清理缓存层的影响。 有些场景下缓存会掩盖真实的性能问题(第一次冷启动期间很慢,之后全命中内存,测出的数字很好看)。反过来,如果压测数据每次请求不同(如随机 ID),又会全部落到数据库,测出的结果偏悲观。要明确自己的压测意图:测的是"热数据下的能力"还是"冷数据下的能力",并在报告里写清楚数据分布。
坑四:数据不具备代表性。 全部请求打同一个 ID,测出来的是"缓存全命中"的极限值。要用符合真实分布的数据(比如按二八分布的 ID 范围、多元的混合接口)。
坑五:时长太短。 跑 10 秒就下结论,很可能没覆盖一次 GC 周期、没触发一次缓存淘汰、没等到连接池稳定。建议每档至少 2~5 分钟。
坑六:压到生产服务上。 已在第四节强调。这里再明确一次底线:禁止把压测流量打到生产数据库。 需要生产数据时,先把数据脱敏后同步到独立环境。
坑七:忽略了超时设置。 wrk 默认超时较长,如果服务端某些请求卡住 30 秒,这段时间是这个连接的浪费。显式设置 --timeout 2s 能让结果更贴近真实的用户容忍度。
坑八:在共享网络/跳板机上压测。 网络经过 NAT、代理、VPN 都会引入额外延迟和连接数限制。压测机应该尽量靠近被测服务(同机房内网),并且压测结果要注明"网络路径"。
七、压测报告应该包含什么
一份能被别人复用的压测报告,至少要有这些内容:
第一部分:环境说明。 被测服务的硬件配置(CPU 型号与核数、内存、磁盘类型)、软件版本(OS 内核、Nginx、JDK、数据库版本)、关键配置参数(worker 数、连接池大小、JVM 堆大小)、部署拓扑、数据量与数据分布。
第二部分:压测方法。 工具与版本、压测机配置与所在网络位置、预热方式、加压策略(每档并发与时长)、请求分布(哪些接口按什么比例)、超时设置。
第三部分:结果数据。 核心是一张分档表:
| 并发数 | QPS | 平均延迟 | P95 | P99 | 错误率 | 应用 CPU | 磁盘 await | 备注 |
|---|---|---|---|---|---|---|---|---|
| 100 | 1950 | 51ms | 68ms | 92ms | 0% | 35% | 2ms | 稳定 |
| 200 | 3900 | 52ms | 70ms | 98ms | 0% | 62% | 2ms | 线性提升 |
| 400 | 7400 | 54ms | 78ms | 130ms | 0.02% | 91% | 3ms | CPU 接近饱和 |
| 800 | 8100 | 98ms | 210ms | 480ms | 0.5% | 98% | 3ms | 拐点,延迟显著劣化 |
配合折线图(X 轴并发,Y 轴分别为 QPS 和 P99)会更直观。
第四部分:结论与瓶颈分析。 明确回答开始设定的目标:目标是否达成、最大可承载 QPS 是多少(在什么响应时间约束下)、瓶颈在哪一层、瓶颈的证据是什么(要引用第三部分的数字)。
第五部分:建议。 具体的下一步:如果要支撑双倍流量该怎么做(优化哪条 SQL、加几台机器、要不要上缓存),以及预计能提升多少。
第六部分:原始数据与复现方式。 压测命令的完整列表、脚本、原始数据文件位置。别人要能照着跑出同样的结果。
要不要写"最大并发数"这样的单一数字?建议写成"在 P95 < 200ms、错误率 < 0.1% 的约束下,系统承载 XXX QPS"。脱离响应时间谈 QPS 和脱离 QPS 谈并发数,都是没有意义的数字。
常见问题
并发用户数和 QPS 到底怎么换算?
用 Little's Law:并发数 = QPS × 响应时间(秒)。反过来 QPS = 并发数 ÷ 响应时间。两者没有固定的倍数关系,取决于响应快慢。响应 10ms 的系统,100 并发就是 10000 QPS;响应 1 秒的系统,100 并发只有 100 QPS。这也是为什么只说"支持十万并发"是不可信的,必须同时给出响应时间。
为什么我压测出来的 QPS 很低,但服务器 CPU 才 20%?
典型的原因有几个:一是客户端瓶颈(压测机自己先满了);二是连接池或线程池太小,请求在排队而机器很闲;三是存在串行等待(分布式锁、同步调用外部接口);四是压测的请求里有大量重定向或静态资源请求。用服务端的同时能运行的 CPU/IO/网络数据采集来确认是哪种。
该用什么并发数开始压?
用 Little's Law 反推。目标 QPS 是 5000、期望响应 100ms,那最小并发就是 500。实际压测时从目标值的一半开始,逐步增加到 2~3 倍找拐点。不要凭感觉乱设。
wrk 的 -c 是每线程还是总共?
总共。wrk 的 -c 参数指定的连接数会在 -t 个线程之间分配,不是相乘关系。这点容易和某些其他工具的语义搞混。
测试环境配不到生产那么大的数据量怎么办?
这是常见困境,也正因如此要在报告里明确标注差异。补救办法:至少保持数据分布的特征(比如索引选择性、冷热比例),或者用倍数缩容但保持业务对象的比例关系。同时对"数据量会影响结论"的部分(比如全表扫描、缓存命中率)做单独说明。这些数据对于大促前的容量规划是必须谨慎外推的。
能在生产环境直接压测吗?
原则上不建议,尤其是禁止打到生产数据库。确有做全链路压测需求的,需要完整的工程方案:影子库影子表、流量染色与识别、明确的时间窗口、实时可中止的开关、以及各业务方的协调。不要在没有这些准备的情况下用 wrk 对着生产域名跑。
压测跑多久合适?
预热至少 1~2 分钟(JVM 应用建议 3~5 分钟),每个并发档位稳定跑 2~5 分钟。一份完整的分档压测通常需要 30 分钟到 1 小时。跑 10 秒的数据基本没有参考价值。
到了压测拐点之后应该做什么?
先确认瓶颈在哪一层(参考第五节对照表),然后按优先级处理:修掉明显的低效(缺索引、重复查询、日志刷盘)→ 减少不必要工作(缓存、异步)→ 调整配置把资源用够(连接池、worker 数)→ 最后才考虑扩容。改完之后要重新压测验证效果,形成"改前 vs 改后"的数字对比。
总结
- 五个核心指标里最重要的是理解 Little's Law:并发数 = QPS × 响应时间。它把三个概念连在一起:能根据目标 QPS 算出该用多大并发施压,也能把"支持十万并发"的宣传翻译成有意义的数值。脱离响应时间谈并发或 QPS 都没有意义。
- 响应时间必须用 P95/P99 而不是平均值。平均值会被大量快请求掩盖长尾,而用户体验崩塌正是从长尾开始的。同时对服务端延迟要区分测量点(是否含 DNS、TLS、网络传输)。
- 工具按场景选:wrk 适合命令行快速测单接口(多线程 + epoll,性能强,支持 Lua 脚本);JMeter 适合复杂业务场景和团队报告;locust 适合 Python 团队和分布式施压;k6 适合接入 CI 做性能回归;ab 只适合临时摸底。不要用 wrk/ab 去压需要 token 和动态参数的业务流程。
- wrk 的
-c是总连接数(在-t个线程间分配,不是相乘)。输出里必须看Latency Distribution(P50 与 P99 的差距反映长尾)和Socket errors(不为 0 说明已有失败,QPS 再高也无意义)。结果可用 Little's Law 交叉验证:并发 ≈ QPS × 平均延迟。 - 环境要求:与生产同配置、独立部署、数据规模接近、网络可控。绝对禁止把压测流量打到生产数据库,那会造成脏数据、连接打满、binlog 暴涨等实际故障。生产全链路压测必须有影子库、流量染色和中止预案。
- 流程要点:明确"合格标准" → 预热(JVM JIT 和缓存预热不做等于白测)→ 逐步加压(几何级数递增找拐点)→ 每档同步采集服务端资源(只凭工具输出无法知道瓶颈在哪)→ 记录分析 → 复现验证至少两次。
- 瓶颈判定看"哪类资源先饱和":应用 CPU 满算一档;IO 或锁导致 Load 高而 CPU 闲;await 高查磁盘;TIME_WAIT 爆发查代理 keepalive 与端口;连接池打满查慢 SQL;加机器 QPS 不涨说明有串行或共享瓶颈(通常是数据库写或全局锁)。
- 报告要包含六部分:环境说明(配置、版本、数据量)、压测方法(工具、加压策略、数据分布)、结果数据表(含 P95/P99 与错误率和资源列)、结论与瓶颈证据、具体建议、原始数据与复现方式。结论写成"在 P95 < X ms 约束下承载 Y QPS"的形式。
企业QQ咨询




