网站服务器如何进行压力测试?并发用户、QPS与响应时间详解

"我们的系统能支撑多少人同时在线?"这个问题几乎所有业务方都问过,但它其实不是一个合格的需求——"在线"的人和"正在发请求"的人是两回事,而后者才是服务器关心的。一个一万用户在线的论坛,实际同时在请求的用户可能只有几十个。

压力测试的价值就在于把这类模糊的问题翻译成可度量的数字:在目标响应时间下系统能承载多少 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"的形式。