简介:wrk 4.1.0 Linux版性能测试工具包,面向后端开发、运维及性能测试人员,用于在高并发短连接场景下快速评估Web服务器的吞吐量、延迟与稳定性。工具本身基于C语言与线程模型,轻量易用,可独立压测,也可配合JMeter、ab等工具形成分层测试方案。压缩包采用7z格式,整体大小约168.72MB,文件总数暂未标注,内含wrk源码、Linux环境编译配置及基础使用说明,文件类型以源码和文档为主,目前暂无更细明细,便于直接下载后编译运行。已有586人学习浏览,具备一定实战参考价值。资料除工具本体外,还围绕线程数、连接数、测试持续时间、自定义请求头及Lua脚本定制等核心参数展开说明,并给出总请求数、吞吐量、响应时间等关键结果指标的解读方法,能够帮助读者快速掌握从简单压测到复杂场景模拟的完整路径,适用于服务端容量评估、性能对比与调优实践。
1. wrk 4.1.0 on Linux:为什么压测工具里我第一个推它
做后端服务的人迟早要面对一个灵魂拷问:你的接口到底能扛多少并发?线上出问题之前,有没有办法先摸到性能天花板?我见过不少人拿着 JMeter 配半个小时的线程组、监听器,结果压测脚本比业务代码还复杂。其实在 Linux 环境下,有一个更轻、更直接、结果更干净的工具,就是 wrk。这个标题里的 wrk 4.1.0,是目前 GitHub 上最主流的一个版本,一条命令就能对 HTTP 接口发起高并发压测,能打出几万乃至几十万的 QPS 数据,对于定位接口瓶颈、验证服务调优效果来说足够用。它不像 AB 那样只擅长简单 GET,也不像 Locust 那样需要 Python 写场景,wrk 的价值在于:用极其简单的命令行参数,配合一个可选的 LuaJIT 脚本,就能模拟出接近真实的业务请求模型。
适合谁用?开发、测试、运维,只要你在 Linux 上跑服务,需要快速验证接口性能,都值得花半小时把这套流程走一遍。注意,wrk 是单机压测工具,它用异步事件驱动模型,能用一个进程打满几千连接,但它不能像分布式压测平台那样模拟多地域的真实用户,这是它的边界。下面我们直接在 Linux 上把它跑起来。
2. 从源码编译 wrk 4.1.0:一条命令装完,但依赖得过关
2.1 为什么不用包管理器直接安装
常见做法是直接用 apt install wrk 或 yum install wrk,但这样装的版本往往不是 4.1.0,而且老发行版里的 wrk 可能停留在 4.0.x,某些参数行为不一致。这个标题既然锁定了 wrk 4.1.0,就按源码编译的方式来讲,这也是社区里最常用的做法。4.1.0 相比 4.0.x 修正了一些 http parser 的边界问题,在长 URL、特殊字符和 chunked 响应上表现更稳,做压测的时候少一些“工具本身出错”的干扰。
源码编译其实很快,因为 wrk 是用 C 写的,只依赖 OpenSSL 和 LuaJIT 的运行库。如果你的机器上没有装过编译工具链,先执行下面这段:
# Ubuntu / Debian 系 sudo apt update sudo apt install -y build-essential libssl-dev git # 如果是 CentOS / Rocky Linux # sudo yum groupinstall "Development Tools" # sudo yum install -y openssl-devel git逻辑上这步是把 gcc、make、OpenSSL 头文件和 git 装好。wrk 的 Makefile 会在编译时自动找 OpenSSL 的 libssl.so,所以 libssl-dev 或 openssl-devel 必须装,否则你会在链接阶段遇到 libssl.so: cannot find 这类错误,这是新手最容易卡住的第一道坎。参数说明:build-essential 在 Ubuntu 里是一组包,包含 gcc 和 make;libssl-dev 提供 sha256 等加密函数的头文件和符号链接。
2.2 编译与验证:4.1.0 这个目录名是怎么来的
拿到源码的方式,常见两种:一种是从 GitHub 直接 git clone,另一种是下载 tar 包后传到服务器上。以 git clone 方式举例:
git clone https://github.com/wg/wrk.git wrk-4.1.0 cd wrk-4.1.0 makegit clone 会拉取最新源码,当前官方仓库的标签就在 4.1.0 上,所以目录名里的 4.1.0 其实是对应仓库 tag。make 命令做的是编译加链接,顺便把可执行文件放到这个目录下。如果你执行完没有任何 error 输出,那二进制就已经在当前目录里了。可以验证一下版本:
./wrk --version看到 wrk 4.1.0 后,再把二进制放到系统的 PATH 里,这样任意目录下都能直接用 wrk:
sudo cp wrk /usr/local/bin/这里的要诀是:不要把整个仓库目录往 PATH 里塞,而是只复制这一个可执行文件。wrk 是单二进制程序,运行时不依赖源码目录里的任何东西,这点比很多需要设置 LD_LIBRARY_PATH 的压测工具干净。如果你遇到了权限问题,检查一下当前用户对 /usr/local/bin 的写权限,或者直接改用 sudo install -m 755 wrk /usr/local/bin/wrk。
编译过程在两核小机器上大约一分钟内完成。如果你用的是一台只有 512MB 内存的云主机,make 的时候可能会因为内存不足被 OOM killer 干掉,那就先加 swap 或换台规格高一点的机器编译完再拷二进制过去,这个二进制放在别的同发行版 Linux 机器上也能直接跑。
2.3 一条最小压测命令:先确认工具真的能用
装好之后,拿一个你自己搭的 HTTP 服务试一下。比如本机有个 Nginx 或随便一个 Spring Boot 应用,我们压一个最简单的 GET 接口:
wrk -t4 -c100 -d10s http://127.0.0.1:8080/api/ping这条命令的意思是:用 4 个线程,保持 100 个并发连接,持续压 10 秒,目标地址是本地服务的 ping 接口。参数说明:-t 是线程数,一般跟 CPU 核数持平或略多;-c 是总的连接数,不是每秒连接数,wrk 会尽可能在这 4 个线程里均分这 100 个连接;-d 是压测时长,10s 算一个比较稳的起步值,既能看到稳定 QPS,又不会等太久。
跑完你会看到一份报告,报告里有 Latency 分布、每秒请求数(Requests/sec)和每秒传输字节数(Transfer/sec)。第一次跑的时候,如果看到 Requests/sec 是几千甚至上万,别急着高兴,先看看 Latency 的 Avg 是不是已经高到了离谱的程度。如果 Avg 延迟只有几毫秒,QPS 却有上万,那你机器的网络协议栈和服务端都处于健康状态。
新手容易踩的第一个坑在这里:wrk 默认压测的是 HTTP/1.1,并且会带上 Connection: keep-alive 头,这意味着一个连接上会连续发多个请求。如果你想测的是短连接场景,也就是每个请求都新建 TCP 连接,那需要在请求头里显式改成 Connection: close。这个改动对 QPS 影响极大,因为握手开销完全不一样,我们在避坑章节再展开。
到了这一步,minimal 流程已经跑通了。但 wrk 比较出名的能力其实在参数组合和 LuaJIT 脚本上,只靠刚才那条命令去压真实业务接口,会得出偏理想化的数据。别急,接下来我们把参数掰开揉碎讲一遍,教你怎么针对不同场景调整出可信的测试方案。
3. 压测参数怎么设:从并发模型到结果解读,一次讲透
3.1 线程数与连接数:不是越大越好,背后是事件循环逻辑
wrk 的性能模型基于 epoll 或 kqueue,每个线程维护一个事件循环,连接均匀分布在多个线程上。所以 -t 和 -c 的关系,决定了每个线程要管多少个 socket。
如果服务端配置了很高的 keep-alive 超时,而你用的 -c 又特别大,那么这些连接会一直占着服务端的连接池,压测出来的 QPS 反而会下降。常见现象是:把 -c 从 100 调到 1000,QPS 先涨后跌,看到一个明显的拐点。这个拐点不是服务端的极限,而是你的压测线程因为维护太多空闲连接导致事件循环调度开销增大。
我一般这样选:先看机器核数,-t 设成 CPU 物理核数,最多乘 2。然后从 -c 小的开始试探,比如 -c 50,压 10 秒记录一次 QPS;再 -c 100、-c 200、-c 500,每次压完看 QPS 曲线。找到 QPS 开始不再上升的那个 -c,就是这台压测机能打出的有效负载范围。不是说每次都要做这么精细的试探,但对一个生产接口做正式压测时,这组数据值得花两分钟跑一遍。
这段“试探”过程 wrk 本身没有自动递增能力,所以推荐的做法是写一个 for 循环脚本:
for c in 50 100 200 500 1000; do wrk -t4 -c$c -d10s http://127.0.0.1:8080/api/ping | grep "Requests/sec" donegrep 只提取每秒请求数,方便对比输出。这里的观察点是:QPS 随 -c 增长的趋势。如果你看到 -c 200 比 -c 100 反而下降,说明服务端在 100 并发左右已经过载了,继续加压只是把请求堆积在队列里,响应时间变长,QPS 上不去。
3.2 压测时长和超时设置:短压看峰值,长压看稳定性
-d 参数决定总压测时长。wrk 内部会在指定时间内尽可能多地打请求,不适合用“总共发 N 个请求”这种思维来理解它。短压比如 10s,看到的是峰值吞吐能力,因为服务端刚开始接收请求,各种缓存还没受太大压力。如果你想评估长时间运行下的内存泄漏或连接泄漏,应该把 -d 拉到 300s 甚至 600s。
还有一种情况是压测接口慢,每次响应都要 3 秒以上,这时候 10s 的压测可能只发出几十个请求,统计意义不大。遇到这种接口,先单独 curl 确认延迟,再决定 -d 要不要加长。wrk 没有显式的“每个请求超时”参数,它的超时控制靠的是 socket 层面的超时,默认没有最顶层的截止时间,所以慢接口会拉长整个压测的实际执行时间。比如 -d 10s 会被解释为“持续打 10 秒”,但如果某些请求一直挂着,wrk 会在 10 秒后停止新建请求,再等已有请求返回完毕。
这里推荐一个实战参数组合:
wrk -t8 -c400 -d60s --timeout 5s http://127.0.0.1:8080/api/query--timeout 5s 表示单个连接在 5 秒内没有响应就算超时。这个参数非常重要,尤其是在压测数据库查询类接口时,一旦服务端出现慢查询,连接会越积越多,最后压测机和目标机之间全是半开连接。设一个合理的超时能让你更早看到失败率,而不是让压测卡在那里。
还有一点容易看漏:wrk 的 --timeout 是针对连接级别的,不是请求级别的。在 keep-alive 模式下,一个连接上连续发多个请求,只要连接上还有数据流动,它不会被判定超时。所以如果服务端每个请求都慢慢吞吞但一直没有完全断开,wrk 会一直等在那里。这种情况,压测报告里的超时数可能为 0,但 Latency 的 Avg 已经高到几百毫秒,得结合延迟分布一起判断。
3.3 结果报告怎么读:QPS 不是唯一标准,延迟分位数才是
wrk 默认给出的报告包含 Latency 的 Avg、Stdev、Max、+/- Stdev,以及 Requests/sec。但真正在做容量评估的时候,延迟分布比平均值重要得多。平均值容易被少数慢请求拉高,但你更想知道的是 p99 是多少,也就是 99% 的请求都在多少毫秒内完成。
wrk 默认不打印分位数,但这不意味着没得看。使用 wrk 的 LuaJIT 脚本可以采集完整的延迟样本,在压测结束后自行计算 p50、p95、p99。常见的做法是用一个名为 wrk2 的分支工具,它自带 p99 输出,但标题锁定了原版 wrk,所以我推荐用 Lua 脚本解决。
先看一个只改输出的简单 Lua 脚本,把它存为 stats.lua:
-- stats.lua local stats = require("stats") local function done(summary, latency, requests) print("p50: " .. stats.percentile(latency, 50)) print("p90: " .. stats.percentile(latency, 90)) print("p99: " .. stats.percentile(latency, 99)) end wrk = function() end注意:以上写法有错误,wrk 的 Lua 脚本里没有现成的 stats 模块,不能 require 一个不存在的库。真实做法是在脚本里自己维护一个数组,把每个请求的延迟记录下来,压测结束回调里排序计算。
-- delay_stats.lua local latencies = {} function wrk.response(status, headers, body) local elapsed = wrk.threads and "yes" or "no" end function done(summary, latency, requests) local arr = {} for i = 1, #latencies do arr[i] = latencies[i] end table.sort(arr) local n = #arr local function pct(p) if n == 0 then return 0 end local idx = math.floor(p / 100 * n + 0.5) if idx < 1 then idx = 1 end if idx > n then idx = n end return arr[idx] end print("p50: " .. pct(50) .. "ms") print("p95: " .. pct(95) .. "ms") print("p99: " .. pct(99) .. "ms") end逻辑说明:wrk 在每个请求完成时会回调 wrk.response,需要你在脚本里记录耗时。但上面这段还没有记录耗时,真正的耗时需要借助 wrk.thread 里的定时器或直接读取 wrk 内部延迟对象,原版 wrk 的 Lua 扩展接口并不提供单请求回调里直接取毫秒数的方法。这个坑非常深,也是很多人在网上搜“wrk p99 脚本”找不到可靠答案的原因。原版 wrk 的 Lua 上下文只暴露了 thread 的 get/stop 接口,以及 setup、init、delay、request、response、done 几个回调,但 response 回调里没有当前请求的耗时。
那真实项目里怎么取分位数?常见做法是:放弃用原版 wrk 脚本取延迟分布,直接看 wrk 输出的 Latency 分布直方图,这个图在 -d 结束后会自动打印,例如 Latency 的分布区间和累计百分比已经能看出大概。或者承接前面说的,用 wrk2 分支来测。这篇文章明确锁原版 wrk,所以这里我讲清楚边界:原版 wrk 的分位数需要额外解析 stdout 的直方图,不如直接用 wrk2。此部分不再深入。
3.4 用 LuaJIT 脚本模拟真实请求:POST、Header、Token,一个脚本说清
wrk 最大的爽点在于它能用 Lua 脚本定制请求体、请求头和请求频率,这让它不再只是 GET 压测器。
最常见的场景是压测一个需要登录态的 JSON 接口。假设你的接口要求 Header 里带 X-Token,请求体是 JSON:
-- post_json.lua local headers = {} headers["Content-Type"] = "application/json" headers["X-Token"] = "a1b2c3d4e5f67890" local body = '{"page":1,"size":20}' function wrk.request() wrk.headers = headers wrk.body = body wrk.method = "POST" wrk.path = "/api/list" return wrk.format("POST", wrk.path, headers, body) end逻辑说明:wrk.request 函数会在每个请求发送前执行,你可以动态修改 wrk 的 method、path、headers、body 字段,而 wrk.format 用来把这些字段拼成完整的 HTTP 请求报文。这里参数说明:wrk.headers 是一个表,wrk.format 的第三个参数直接传入表即可,不需要你自己拼字符串。
但如果每个请求的 token 不一样,比如要模拟不同用户并发访问,上面这个脚本就失效了。解决办法是结合 thread:set 和线程内计数器做简单随机:
-- dynamic_token.lua local tokens = {} function setup(thread) thread:set("token", tokens[1]) end function init(args) local f = io.open("tokens.txt", "r") for line in f:lines() do tokens[#tokens+1] = line end f:close() end function wrk.request() local idx = math.random(1, #tokens) local h = {} h["X-Token"] = tokens[idx] h["Content-Type"] = "application/json" return wrk.format("GET", "/api/profile", h, nil) end逻辑说明:init 函数在线程初始化时读一次 token 文件,然后 wrk.request 每次随机取一个,这样压测请求的 token 分布在 N 个用户上,更接近真实场景。注意这里的 math.random 是 LuaJIT 自带的,种子需要人工设置一下,否则每次压测的随机序列完全相同。建议在 setup 里加上 math.randomseed(thread:get("id") + os.time())。
每条代码后的参数说明和这个随机逻辑,都是正式压测里容易被忽略但影响结果的细节。写到这里,wrk 的基本能力已经覆盖了,接下来进入整个压测流程里最容易翻车的部分。
4. 避坑与排查:我在 wrk 压测中踩过的 5 个坑
4.1 最大打开文件数限制:莫名其妙报 too many open files
现象:wrk 命令看起来在跑,但终端里刷出大量 unable to create socket 报错,压测结果和预期相差十万八千里。
原因:Linux 默认进程可打开的文件描述符上限通常是 1024,而一个连接至少占一个 fd。你指定 -c 2000,但进程根本创建不了这么多 socket,连接建不满,QPS 自然上不去。
解决:执行 ulimit -n 查看当前限制。调大它的方式,常见两种:
# 临时调整,当前 shell 有效 ulimit -n 65535如果想永久生效,修改 /etc/security/limits.conf,在文件末尾加下面两行,然后重启会话:
* soft nofile 65535 * hard nofile 65535还有一点要注意的是:如果你是通过 systemd 运行的压测任务,那么 service 文件里也要加 LimitNOFILE=65535,否则 ulimit 改了也没用。这是我在容器和 CI 平台里踩过的坑,很隐蔽。
4.2 压测机和目标机在同一台机器:数据全是假的
现象:压测本机服务,QPS 高得离谱,但服务部署到测试环境后,用同样参数压测,QPS 掉了 80%。
原因:wrk 和目标服务共享同一个内核协议栈,socket 通信不经过物理网卡,TCP 握手开销大幅缩水。另外 CPU 资源也被 wrk 抢走一半,服务端实际获得的 CPU 远远少于独立部署的情况。
解决:把 wrk 部署到独立的机器上,至少与目标服务不要共享 CPU 核心。如果实在没有第二台机器,用 taskset 给 wrk 和服务分别绑核,例如:
# 绑 wrk 到 CPU0-1 taskset -c 0,1 wrk -t2 -c200 -d30s http://<server-ip>:8080/api/ping这样 wrk 不会去抢服务端所在的其他核。但要注意,这个方法只能缓解,不能完全模拟真实网络延迟。跨机房压测时,不要忽略网络带宽和延迟,wrk 的 Transfer/sec 会告诉你打满了多少带宽。
4.3 压测结果里完全没有失败请求,但接口明明报错
现象:wrk 的 Requests 数量在增长,Latency 也正常,但业务日志里看到大量 500 错误。
原因:wrk 默认只看 TCP 层的数据传输成功与否,不校验 HTTP 响应状态码。一个 5xx 响应在 wrk 眼里就是一次正常请求完成。
解决:在 Lua 脚本里检查状态码,并用 thread:stop 及时止损。示例如下:
function wrk.response(status, headers, body) if status ~= 200 then local ok, err = pcall(function() wrk.threads:stop() end) end end如果错误只统计不停压,可以用一个全局计数器:
local error_count = 0 function wrk.response(status, headers, body) if status >= 500 then error_count = error_count + 1 end end function done(summary, latency, requests) print("5xx count: " .. error_count) end这个脚本的价值在于压测结束后你能看到真实的业务失败率,而不是只关注 QPS 一个数字。注意 wrk.threads:stop() 会立刻停止所有线程,谨慎使用。
4.4 压测 HTTPS 接口时 CPU 飙到 100%,QPS 却不涨
现象:压测 HTTPS 接口,wrk 所在机器 CPU 打满,但 Requests/sec 远低于 HTTP 压测数据。
原因:TLS 握手和加密消耗了大量 CPU 资源。wrk 在压测 HTTPS 时默认使用单个 SSL 会话,但每次新连接都要重新握手。如果并发很大,TLS 握手开销就变成一个瓶颈。
解决:启用 wrk 的 TLS 会话复用参数。wrk 4.1.0 提供一个参数叫 -- 无?其实 wrk 没有这个参数,但你可以通过 Lua 脚本在 setup 阶段复用会话。不过社区更常用的方案是:压测 HTTPS 时提升服务端的 TLS 会话缓存时间,或者改用 HTTP/2? wrk 不支持 HTTP/2,所以都解决不了 TLS 握手实质成本。
正确思路是:把 TLS 终结放到 Nginx/HAProxy 层,wrk 压测时先压 HTTP 端口,验证业务逻辑,再单独压 TLS 终结层来评估证书和握手性能。如果你确实需要端到端压测 HTTPS,那就接受 CPU 被加密消耗的现实,并把 wrk 的 -t 数量提升到接近 CPU 核数。统计 CPU 使用率时,区分 user 和 sys 两块,若是高 sys 则多半是内核协议栈瓶颈,可以考虑优化网络参数。
4.5 压测结果不稳定,每次跑的数字天差地别
现象:同一台机器,同一条命令,隔 5 分钟再压一遍,Requests/sec 波动超过 30%。
原因:目标机上有类似的定时任务、日志清理、备份进程,甚至别的同事也在压测同一台机器。wrk 的结果对系统扰动极其敏感。
解决:压测前用无人值守的方式清场。先看目标机负载:
top -b -n1 | head -20 ss -sss -s 能看到当前 socket 统计,如果 TIME-WAIT 数量很大,说明上一次压测的连接还没完全释放,此时直接开始新一轮压测会受影响。建议每次压测间隔参数至少 30 秒。另外,把压测命令和结果输出到文件,方便对比多次执行的差异:
wrk -t4 -c200 -d30s --timeout 5s http://127.0.0.1:8080/api/ping > result_$(date +%s).txt加上时间戳,方便溯源。如果连续三次压测数值都稳定在 +-5% 范围内,这组数据才值得写进报告。
5. 压测数据的可信度验证与进阶技巧
wrk 给出的 Requests/sec 到底可不可信?我的习惯是:不做单一工具依赖。用 wrk 压测完,再用 curl 抽样验证单个请求的延迟,或者换个并发模型交叉验证。
交叉验证的常见做法是:wrk 跑一组,再用 curl 循环请求同一接口做对比测试:
time curl -s -o /dev/null -w "%{http_code} %{time_total}\n" \ http://127.0.0.1:8080/api/ping跑 10 次,如果 curl 的 time_total 和 wrk 的 Latency Avg 相差不大,数据的可信度就高。如果 wrk 显示的 Latency 是 5ms,但 curl 是 200ms,说明 wrk 的并发模式可能打到了不同服务节点或者服务端有连接相关的优化,需要进一步排查。
再看一个进阶技巧:wrk 本身不直接支持压测 WebSocket,但你可以用 LuaJIT 的请求回调模拟 WebSocket 的握手请求,验证握手阶段的性能。真实 WebSocket 的消息收发压测需借助其他工具,这一点要记住,不要把 wrk 用在不合适的场景里。
第四个进阶用法是压测结果自动出报告。我一般会把 wrk 的输出丢给后端分析,用 Go 或 Python 解析 Requests/sec 和 Latency,然后判定是否通过。这里分享一个极简的 Python 判断脚本思路:
import subprocess, sys output = subprocess.check_output([ "wrk", "-t4", "-c200", "-d10s", "http://127.0.0.1:8080/api/ping" ], text=True) for line in output.splitlines(): if "Requests/sec" in line: qps = float(line.split(":")[1].strip().split()[0]) if qps > 10000: print("PASS", qps) else: print("FAIL", qps)这个脚本的逻辑是:压测结束后解析每秒请求数,与阈值比较,决定本次性能回归是否通过。你可以挂在 CI 流水线里,每次代码上线前自动跑一遍基准接口的性能对比。
我个人的习惯是:压测报告只认两个指标组合——QPS + p99 延迟 + 错误率,缺一不可。QPS 再高,p99 延迟超过 1 秒,对用户体验来说依然是不可用服务;错误率再低但 QPS 上不去,说明系统在过度保护,也需要调优缓存和服务端线程池配置。
关于 wrk 4.1.0 这个版本,我的整体评价是:它是我见过的最快的轻量压测工具,没有之一。编译一次,到处可跑,对比 JS 生态的压测工具和 AB,它在并发模拟和请求自定义上的平衡做得最好。如果你的工作流里经常要验证 Linux 服务端的性能指标,把 wrk 练熟,省下的时间足够你去琢磨调优策略本身了。
希望这篇文章能帮你在 Linux 压测这条路上少走几步弯路,尤其是最后那节避坑清单,每条都是我实际翻过车的教训,值得收藏对照排查。下次压测前,先看一眼 ulimit、再看一眼压测机和目标机是否同机、最后确认脚本里有没有对状态码做校验,再跑压测不迟。
本文还有配套的精品资源,点击获取