简介:wrk.tar.gz 内含已编译完成的 wrk 高性能 HTTP 压测工具,解压即可在终端运行,无需额外编译步骤。wrk 由 Steve Leach 创建,凭借 LuaJIT 脚本支持在性能压测领域颇具口碑。它基于事件驱动与多线程模型,单机即可模拟较大并发连接,特别适合 Web 服务、API 接口、负载均衡器的性能测试与瓶颈定位。资源包整体约 214MB,核心交付物为可直接执行的 wrk 程序,同时包含一份工具详解说明,系统梳理了常用参数(连接数、持续时长、线程数、固定速率、自定义脚本)的用法,并给出 LuaJIT 脚本示例,支持自定义请求模式、响应状态校验、延迟控制等复杂测试逻辑。通过脚本可动态生成请求 URL、根据响应状态决定是否中断测试,使压测更贴近真实业务。此外,文档还对每秒请求数、平均传输速率、延迟百分位等关键指标做了说明,帮助读者正确解读压测结果。已有 258 人浏览学习,若你正在搭建压测环境或优化服务吞吐能力,这份即开即用的工具包可免去从源码编译的麻烦,配合脚本扩展能够快速满足并发摸底、回归对比和容量评估等场景需求。
1. wrk 压测工具:编译好的包,解压即用才是真的省事
wrk 压测工具在从业者手里一直有点玄学:同样的机器,用 ab 压只能拉到几百 QPS,换 wrk 能轻松翻十几倍;但也有不少人卡在第一步——编译。openssl 头文件版本对不上、gcc 缺依赖、交叉编译环境没配好,一个 tar.gz 折腾半天。这份资源是已经编译完的 wrk 二进制包,解压即用,省掉的正是最劝退的环境环节。适合三类人:临时要压接口的运维、做网关性能对比的后端,以及刚接触高并发压测、还没准备好折腾编译环境的新手。下面我会把 wrk 的线程模型、参数设置、Lua 脚本和常见坑逐个讲清楚,保证你照着一路跑完不翻车。
2. wrk 的高并发原理:线程模型、连接复用与适用边界
2.1 每个线程独立事件循环:wrk 的并发模型不是线程越多越好
wrk 本质上是单进程多线程的程序,每个线程各自持有一个 epoll 事件循环,线程之间不共享连接,request 计数也是独立维护的。启动时-t指定线程数,整个进程会创建这么多线程;-c指定总连接数,wrk 会把连接均匀分摊到各线程。所以-t4 -c200的含义就是 4 个线程,总共 200 条连接,每个线程维护 50 条。
我一般习惯把-c设成-t的整数倍,这样每个线程的连接数量可控,epoll 调度的开销最小。如果你把-t直接拉高到几十,反而会因为线程切换和锁竞争把压测机自己的 CPU 烧光,结果里 QPS 上不去,Latency 的 Stdev 却涨得离谱。wrk 的模型是连接持续复用:请求走完一个循环后,连接不关闭,直接排队等下一个请求。这也是它和 ab 差距最大的地方。
wrk 编译时通常要链接 OpenSSL 和 LuaJIT,LuaJIT 是后续跑 lua 压测脚本用的。如果只在 Linux 上跑动态链接版,最常见的问题是 glibc 版本不一致,这属于运行时环境问题,下一章我会讲怎么在解压时顺手排查。这里先记住一个结论:wrk 的线程数不是越多越好,压测机 CPU 核数的两倍已经是很激进的设定了,保守一点 4 到 8 线程足够覆盖绝大多数单机压测场景。
2.2 keep-alive 连接复用:wrk 高 QPS 的核心来源
wrk 默认走 HTTP/1.1 keep-alive,连接建好之后不会被Connection: close关闭,一批请求全部复用同一条 TCP 连接。这意味着 DNS 解析、TCP 三次握手、TLS 握手这些一次性成本被摊薄到成千上万个请求里,单请求开销只剩「发请求 + 等响应 + 解析」这三段。
对比 ab 的阻塞式模型:ab 每个连接在请求期间占住一个进程或线程,连接处理完就关,下一个请求再重建。在并发数不高的场景下两者差距不明显,一旦并发到几百上千,ab 的进程调度和连接重建开销直接拖垮压测机自身。这也是为什么同一个接口,ab 可能只能压出 5000 QPS,wrk 能压到 8 万 QPS 的原因。
但注意,这个模型的代价是:wrk 测出来的 QPS 是「长连接稳态」下的数字,不代表「每次新建连接」场景的真实表现。如果你的业务是短连接风格——比如每请求独立建连、服务端还禁用了 keep-alive——wrk 的数字会偏高,因为它把握手成本从真实链路里抠掉了。遇到这种场景,我一般直接用 ab 压,或者干脆在 wrk 脚本里给请求加Connection: close头,强制关闭连接复用。
2.3 场景边界:wrk 不适合什么
wrk 不是万能的,有三个场景我明确不建议硬上。
第一个是 HTTP/2。wrk 官方只支持 HTTP/1.1,压 HTTP/2 接口应该用 h2load,它才是基于 nghttp2 的实现。wrk 对 HTTP/2 请求会直接退化成普通 HTTP/1.1,测出来的延迟和吞吐都没有参考价值。
第二个是分布式压测。wrk 是单机工具,单机网卡和 CPU 是有上限的。压测机上软中断占比超过 40% 时,加连接数已经没意义了,这时候要么做压测机调优,要么把压力拆分到多台机器上同步发。wrk 没有内置的分布式协调机制,需要自己写脚本拉多台机器同步启动。
第三个是精细限速。wrk 没有内置的 QoS 控制,虽然部分新版本支持 rate 参数,但老版本是不认的。如果需要精确控制 QPS 上限做容量摸底,可以考虑 wrk2,它专门支持固定速率模式。判断标准很简单:要压「服务端到底能扛多少」用 wrk,要压「在指定流量下服务端表现如何」用 wrk2。
| 维度 | wrk | ab | h2load |
|---|---|---|---|
| 并发模型 | epoll 事件驱动 | 阻塞式多进程 | 多线程 + nghttp2 |
| 连接管理 | 默认 keep-alive 复用 | 默认短连接 | 支持多路复用 |
| HTTP/2 | 不支持 | 不支持 | 原生支持 |
| Lua 动态脚本 | 内置 LuaJIT | 无 | 无 |
| 适用场景 | 长连接高并发 | 短连接探测 | HTTP/2 网关 |
3. 首次压测:从解压到读懂一份报告
3.1 tar tzf 先看包结构,ldd 确认依赖再解压
拿到 wrk.tar.gz,我不会直接解压。先看包结构:
tar tzf wrk.tar.gz这一步能确认打包时放的是单纯二进制还是带源码目录,免得解压到当前目录把文件撒得到处都是。看到包里是一个wrk可执行文件时,我一般会建一个专门的目录再解压:
mkdir -p ~/tools/wrk tar xzf wrk.tar.gz -C ~/tools/wrk cd ~/tools/wrk ./wrk -v./wrk -v能正常输出版本号,说明二进制和当前系统的 glibc、openssl 动态库能对上。我遇到过不少次./wrk直接报version GLIBC_2.29 not found,原因是编译机器是旧发行版,运行机器是新发行版,动态链接库版本对不上。遇到这种情况先跑一条命令:
ldd ./wrk | head -20如果输出里libssl.so或libc.so后面跟着not found,说明打包时没有带上运行库。这种二进制没法跨发行版裸跑,优先找同发行版的编译包,或者拿源码在目标机器上重新 make。动态链接的二进制逃不掉这个限制,看 ldd 再动手能省十分钟排查时间。解压后我习惯把 wrk 放到 PATH 里:
ln -s ~/tools/wrk/wrk /usr/local/bin/wrk wrk -v这样后续跑压测不用每次都写全路径。
3.2 第一次压测:一条命令和报告里的每个数字
wrk -t4 -c200 -d30s --latency http://127.0.0.1:8080/api/health参数拆解:-t4是 4 个线程,-c200是 200 个并发连接,-d30s压 30 秒,--latency在结束时打印完整的延迟分布。压测目标是本机回环地址,域名解析都不需要,最适合验证工具本身。
报告输出长这样,数字按你的机器会不同:
Thread Stats Avg Stdev Max +/- Stdev Latency 8.42ms 3.17ms 99.24ms 86.14% Req/Sec 5.92k 1.01k 10.35k 73.50% Latency Distribution (HdrHistogram - Recorded Latency) 50.000% 7.83ms 75.000% 9.71ms 90.000% 12.55ms 99.000% 22.31ms 99.900% 45.09ms 99.990% 78.22ms 342176 requests in 30.00s, 57.84MB read Requests/sec: 11406.15 Transfer/sec: 1.93MB解读顺序:先看最后一行Requests/sec,这是全程平均吞吐,也是最常写进报告的数字。再看Latency的 Avg 和 Stdev,Stdev 超过 Avg 的 30% 说明延迟抖动大,后面要找原因。最后对比 50% 和 99% 分位差:50% 是 7.83ms,99% 是 22.31ms,差值不大说明服务端处理稳定;如果 99.9% 突然飙到几百毫秒,多半是触发了 GC 或后端排队。顶部 Thread Stats 里的Req/Sec是各线程每秒请求数的统计,它跟最后的Requests/sec不是一回事,前者是单线程的聚合分布,后者是总吞吐均值。
Latency Distribution来自 wrk 内嵌的 HdrHistogram,录的是全样本延迟,比只看 Avg 靠谱得多。这里有个细节:50% 分位数是 7.83ms,但 Avg 是 8.42ms,两者接近说明延迟集中在均值附近,没有明显的长尾。
3.3 参数怎么配:线程、连接、时长和超时之间的关系
| 参数 | 作用 | 我的建议 |
|---|---|---|
| -t | 压测线程数 | 从 2~4 起步,不要超过压测机 CPU 核数的 2 倍 |
| -c | 总并发连接数 | 先按 -t 的 50 倍试,比如 -t4 配 200 |
| -d | 压测时长 | 至少 30s,30s 以下样本太少,分位数不稳 |
| -T | 单请求超时时间 | 默认 10s,压慢接口时按业务调整 |
| -H | 自定义请求头 | 支持重复传,模拟 Host、User-Agent |
| --latency | 打印延迟分布 | 正式报告必加 |
连接数不是越大越好。我见过有人直接-c10000压网关,结果压测机先死了:文件描述符打满、软中断把 CPU 吃光,最后报告 QPS 比-c500还低。正确做法是阶梯压测:-c从 100、200、500、1000 各跑一遍 30s,记录 QPS 和 99% 延迟,看曲线拐点。吞吐随连接数上涨后回落,回落的那个点就是服务端的处理上限。
-T参数很多人忽略:它只影响单个请求的等待时间,超时的请求会计入 Socket errors 的 timeout 分类,并不会把整个压测停掉。压长尾接口时,-T设太短会把慢请求全部变成 timeout,QPS 数字虚低;设太长又会让压测时间被慢请求拖长。我一般按「业务方承诺的最慢响应时间」来设,比如承诺 99% 在 500ms 内,-T就设 2s,留出余量。
4. 压 POST 和业务脚本:用 Lua 扩展出更接近生产的流量
4.1 改用 POST + JSON:wrk 自带的 Lua 扩展点
wrk 内置 LuaJIT,这是它比 ab 强的一个重要原因。脚本不需要写完整生命周期,只定义几个钩子函数。最简单的是直接覆盖全局变量:
wrk.method = "POST" wrk.headers["Content-Type"] = "application/json" wrk.headers["Accept"] = "application/json" wrk.body = '{"phone":"13800138000","sms_code":"1234"}'执行时用-s指到脚本文件:
wrk -t4 -c100 -d30s -s post.lua http://127.0.0.1:8080/api/sms/loginwrk.method是全局字符串,wrk.headers是一个 table,wrk.body是字符串,三个都作用于当前线程的后续请求。这样每个请求都会带同样的 JSON body。此脚本适合固定参数接口,比如短信登录、固定 token 查询这类 body 不变的业务。
这里有个容易犯的错误:只要脚本里定义了request()函数,每个请求都以函数返回值作为请求内容,wrk.method这类全局变量就不再生效。两种写法不要混着用,排查脚本问题时,第一件事就是确认到底是「变量绑定」还是「函数生成」模式。
4.2 动态参数与登录态:request() 和 done 回调的完整用法
真实业务压测里,每个请求都用同一个手机号,服务端可能直接走缓存。要用动态参数,必须走request()函数:
function request() local id = math.random(100000, 999999) local path = string.format("/api/items/%d", id) return wrk.format("GET", path) endwrk.format是内置函数,接收 method、path、headers、body 四个参数,返回一个格式化好的请求。每次调用request(),wrk 都会把这个返回值发出去。上面这段模拟的是按随机 id 查商品的场景,连续打 30s 后服务端会命中不同的数据行,更接近真实流量。
注意一个细节:wrk 每个线程有独立的 Lua 状态,如果你用计数器counter = counter + 1来生成路径,4 个线程会各自从 1 开始自增,路径大量重复。用随机数或时间戳代替计数器,是更稳妥的做法。这个坑我踩过不止一次,压出来的 QPS 虚高,因为服务端一直在命中同一个数据行。
更复杂的登录态场景,一般是先请求一次 login,把返回的 token 捞出来,后续请求加到 header 里:
local token = "" function request() local headers = {} if token ~= "" then headers["Authorization"] = "Bearer " .. token end return wrk.format("GET", "/api/user/profile", headers) end function response(status, headers, body) if status == 200 and headers["Set-Cookie"] then token = string.match(headers["Set-Cookie"], "token=([^;]+)") end endresponse()回调里能拿到本次响应的 status、headers 和 body。注意 wrk 在没有定义response回调时会丢弃响应体,一旦定义了回调,body 就会保留给 Lua 读取,代价是内存占用变大。高并发压测时如果响应体很大,建议只提取 header,不要在回调里频繁拼接 body 字符串。
done回调负责汇总结果:
done = function(summary, latency, requests) io.write("Total requests: " .. summary.requests .. "\n") io.write("QPS: " .. math.floor(summary.requests / summary.duration * 1000000) .. "\n") io.write("50th percentile: " .. latency:percentile(50) .. "ms\n") io.write("99th percentile: " .. latency:percentile(99) .. "ms\n") endsummary.duration的单位是微秒,所以要乘以 1000000 换算成秒。latency:percentile(p)返回 HdrHistogram 里指定百分位的延迟值。这套「request 生成 + response 提取 + done 汇总」的组合,是压测脚本里最常用的闭环。
4.3 脚本调试:小剂量验证语法,再上量
脚本写完后我从不直接拿 30s 任务跑,先小剂量验证:
wrk -t1 -c1 -d2s -s post.lua http://127.0.0.1:8080/api/sms/login-t1 -c1表示单线程单连接,2 秒钟就能跑完。这一步能验证三件事:脚本语法有没有错、请求格式服务端认不认、响应状态码是不是预期值。如果服务端返回 400,先看 body 里的 JSON 格式和 Content-Type,别急着加大压力。
wrk 的 Lua 报错信息会直接打到标准错误流,比如attempt to index a nil value,定位到具体行号。最常见的调试手法是在脚本里插io.write()打印变量值——要注意 wrk 脚本并发执行时,打印的日志可能交错,最好只在done回调里做汇总输出,别在request()里打印每次请求的路径。
5. 压测避坑指南:五个最容易翻车的地方
5.1 连接数开太高,QPS 反而掉下来
现象:-c从 200 提到 2000,QPS 不升反降,Latency 的 Stdev 明显变大。
原因:连接数超过服务端 accept 队列或线程池处理能力后,连接排队时间变长。Nginx 的worker_connections默认只有 1024,连接暴增时 close 风暴触发,内核协议栈开销全被放大。
解决:固定-t4,-c按 100、200、500、1000 阶梯跑,记录每个档位的 QPS,找拐点。服务端多 worker 时先确认worker_connections配置,别让单机压测直接压垮内核 socket 表。
5.2 压测机先成了瓶颈:CPU 飙爆,延迟全部失真
现象:压测机 CPU 100%,top 里软中断占了一大截,报告里的 QPS 飘忽不定,同一参数两次结果差 20%。
原因:wrk 的线程把网卡中断和 epoll 唤醒都压在一个物理 CPU 上,网络包量太大时,网络栈处理和应用层抢 CPU。
解决:先看压测机 CPU 是不是跑满。跑满就走多机分布式压,别指望单机无限加连接。另外把-t调小,线程数和 CPU 核数对齐,减少上下文切换。网卡多队列中断绑定到不同核心是运维侧的事,压测时先确认压测机硬件够用。
5.3 keep-alive 被中间层回收:Socket errors 里 read 上涨
现象:压测中途 Socket errors 里 read 或 write 数字突然上涨,QPS 出现毛刺。
原因:wrk 每线程维护的连接数大于实际活跃使用量时,空闲连接会被中间层按 keepalive timeout 回收。四层负载均衡的 idle timeout 经常是 60s 或 90s,压测时长跨过这个值时会看到连接大量重建。
解决:-c总连接数按「线程数 × 单线程实际并发深度」配置,不要无脑开大。压长连接服务时,把-c与目标服务的 keepalive 上限对齐,测试环境允许时直接把服务端keepalive_timeout调大。这个问题的本质是连接复用的生命周期,跟 wrk 本身无关,别在工具上浪费时间。
5.4 Lua 脚本里的中文和转义:请求体乱码
现象:wrk.body里写了一段中文 JSON,服务端收到后乱码,或者直接 HTTP 400。
原因:脚本文件不是 UTF-8 编码,或者 Lua 解析字符串时把转义符号吃了。
解决:编辑器把 .lua 存成 UTF-8 无 BOM;body 里的中文尽量用\uXXXX转义,不要直接贴中文。JSON 里的双引号用 Lua 字符串时要么单引号包整个 body,要么内部用\"转义。我通常写成:
wrk.body = '{"name":"\\u5f20\\u4e09"}'服务端反序列化 100% 不出错。新手踩这个坑最多,查半天才发现是编码问题。
5.5 Ctrl+C 中断压测:报告里的分位数别当真
现象:压-d30s的任务,跑到 10s 就 Ctrl+C 停了,报告显示 Latency 很漂亮,QPS 也高。
原因:wrk 的统计基于已经完成请求的样本。前 10s 里连接还在建立、缓存还在预热,这阶段延迟偏高;中断后剩下的样本集中在已完成快速请求上,分位数自然偏好看。
解决:正式压测前先跑一个 5s 短任务预热,看结果但别当正式数据,只为把连接池和缓存热度建起来。再跑正式 30s 任务。Ctrl+C 只适合快速验证脚本语法,不适合作为最终报告数据来源。需要看全量分布时,一定等任务自然跑完,然后看报告里 99% 和 99.9% 两行。
6. 压测结果的可信度校验:三个关键信号与一套对照动作
报告到手先不急着写结论。这份编译好的 wrk.tar.gz 解压就能跑,但跑出来的数字不一定可信。我一般先看三个信号:Socket errors 是否归零、Latency 的 Stdev 与 Avg 的比值、两次同参数压测的 QPS 偏差。
Socket errors 有四类:connect、read、write、timeout。connect 失败指向目标端口不可达或 backlog 溢出;read/write 失败多半是服务端主动断开连接,和 keep-alive 超时有关;timeout 是请求超时未响应,和-T参数直接相关。errors 里任何一类不是 0,报告数字都只能做参考,不能当结论。
Latency 的 Stdev 超过 Avg 的 30% 时,我认定系统延迟不稳定,不会直接拿 Avg 去跟别人比。这时加一个对照动作:-c减半再跑一遍,如果 Stdev 明显回落,说明是高并发排队导致的抖动;如果 Stdev 依旧很大,往 GC、锁竞争、慢 SQL 方向查,别在压测工具上浪费时间。
最后是双跑对照:同一个命令原样执行两遍,QPS 差超过 5% 就查环境干扰,比如压测机上的定时任务、目标服务在压测期间被别的请求污染。把 done 回调里输出的关键数字追加到文件,对比起来最方便:
done = function(summary, latency, requests) local f = io.open("/tmp/wrk_result.log", "a") f:write(math.floor(summary.requests / summary.duration * 1000000) .. " " .. latency:percentile(99) .. "\n") f:close() end从那以后,我每次压测完都强制自己先跑一遍同参数对照,再回去翻 Socket errors 和 Stdev 这三行字——不看这三样就写结论,多半是被机器的平均延迟骗了。希望帮到你。
本文还有配套的精品资源,点击获取