☰
wrk 压测工具实战:从解压到 Lua 脚本调优与避坑指南
2026/10/9 14:16:12 网站建设 项目流程

简介:wrk.tar.gz 是一份已编译完成的 HTTP 压测工具包,面向后端开发、运维及性能测试人员,用于评估 Web 服务器、API 服务与负载均衡器的吞吐能力与响应延迟。解压后可直接在终端运行,省去源码编译环节,适合需要快速开展基准测试与性能调优的场景。压缩包整体约 214.16MB,上游未提供文件总数与类型明细,核心为可执行的 wrk 程序,并支持通过 LuaJIT 脚本自定义请求逻辑、响应校验与延迟控制。工具可指定连接数、持续时间、线程数与固定请求速率,输出每秒请求数、传输速率及最小、最大、平均与多分位延迟等关键指标,便于横向对比不同配置或技术栈的性能表现。目前已有 258 人学习下载,适合希望快速上手压测、获取真实性能数据的开发者参考使用。

1. 拿到 wrk.tar.gz 别急着解压:先搞清楚它到底压的是什么

如果你正在做后端服务调优,或者需要给某个 HTTP 接口做一轮基准测试,大概率会碰到 wrk 这个名字。它是一款用 C 语言写的 HTTP 压测工具,基于事件驱动模型,单机就能把并发拉得很高,常被用来替代 ab 做更贴近真实场景的压力测试。这次拿到的是一份已经编译好的 wrk.tar.gz,省掉了从源码编译时处理 OpenSSL 依赖、LuaJIT 版本匹配这些麻烦事,解压后基本能直接跑。它适合谁?后端开发、运维、SRE,以及任何需要量化接口吞吐和延迟的人。但别急着解压,先确认一件事:这份包是在什么系统上编译的,glibc 版本对不对得上,否则你会在运行时报出看不懂的链接错误。

2. 解压与首次运行:从 tar.gz 到一条能跑通的命令

2.1 包内结构与可执行文件定位

拿到 wrk.tar.gz 后,先看包内结构。通常编译好的包会包含可执行文件 wrk、若干 Lua 脚本示例(比如 scripts/ 目录下的 post.lua、report.lua),以及 README 或 LICENSE。解压命令很简单:

# 创建独立目录,避免污染当前工作区 mkdir -p ~/tools/wrk && tar -zxvf wrk.tar.gz -C ~/tools/wrk # 进入目录查看内容 cd ~/tools/wrk && ls -lh

逻辑说明:-C指定解压目标目录,-z表示 gzip 解压,-x解压,-v显示过程,-f指定文件名。参数上没什么玄学,但要注意如果 tar.gz 内层还有一层目录,解压后路径会多一级,用ls确认 wrk 可执行文件的实际位置。常见做法是把它放到~/tools/wrk/下,再通过软链接挂到/usr/local/bin,这样任何路径下都能直接调用。

# 赋予执行权限(有些包解压后权限会丢) chmod +x ~/tools/wrk/wrk # 建立软链接,方便全局调用 sudo ln -sf ~/tools/wrk/wrk /usr/local/bin/wrk # 验证版本 wrk --version

如果wrk --version输出了版本号,说明可执行文件本身没问题。如果报No such file or directory,但文件明明存在,那多半是动态链接器路径不对,这个坑后面会专门讲。

2.2 一条最小可用压测命令的拆解

先跑一条最简单的命令,建立手感:

# -t 2 个线程,-c 10 个连接,持续 10 秒,压测本机 8080 端口 wrk -t2 -c10 -d10s http://127.0.0.1:8080/health

逻辑说明:-t是线程数,-c是并发连接数,-d是持续时间,最后跟目标 URL。wrk 会把连接分摊到各线程上,每个线程内部用 epoll 管理自己的连接。参数上,-t不建议超过 CPU 物理核心数,否则线程切换反而拖累吞吐;-c可以远大于-t,wrk 的设计就是少量线程扛大量连接。跑完后输出里几个关键指标:Requests/sec是吞吐,Latency是平均延迟,Stdev是延迟标准差,+/- Stdev是延迟的波动范围。新手最容易只看 Requests/sec,忽略 Stdev——如果 Stdev 很大,说明延迟抖动严重,这个吞吐数字参考价值就打折了。

提示:第一次跑建议先用-d5s短时间试水,确认目标服务能承受,再拉长到 30s 或 60s。压测时间太短,结果受预热影响大;太长又可能把测试机和服务机都拖进异常状态。

3. 参数调优与 Lua 脚本:让压测贴近真实业务

3.1 线程、连接、超时的组合逻辑

wrk 的参数不多,但组合起来有讲究。下面这张表是我常用的几组配置和适用场景:

参数组合适用场景注意点
-t4 -c100 -d30s中等并发接口基准确认测试机 CPU 核数 ≥ 4
-t8 -c1000 -d60s高并发吞吐摸底需调大测试机文件描述符上限
-t2 -c50 -d30s --timeout 2s延迟敏感型接口超时设短,快速暴露慢请求
-t4 -c200 -d30s --latency需要延迟分布开启详细延迟统计,输出更细

--timeout这个参数容易被忽略。默认情况下 wrk 对单次请求的超时比较宽松,如果目标服务有慢请求,不设超时会让压测结果被少数慢请求拉偏。常见做法是设成业务可接受的最大响应时间,比如 2s 或 5s。--latency则会打印延迟百分位(50%、75%、90%、99%),比只看平均值有用得多——平均值会被大量快请求掩盖,P99 才能暴露长尾。

# 开启延迟分布,观察 P99 wrk -t4 -c200 -d30s --latency --timeout 2s http://127.0.0.1:8080/api/list

逻辑说明:--latency让 wrk 在结束时输出延迟直方图统计,--timeout 2s表示单次请求超过 2 秒就计为超时。参数上,-c200配合-t4,平均每线程 50 个连接,这个比例在多数 Linux 机器上比较稳。如果发现Socket errors: connect数量上升,说明连接数超过了测试机或目标机的承载,需要往下调-c或检查ulimit -n。

3.2 用 Lua 脚本模拟 POST 与动态参数

wrk 内置了 LuaJIT,可以通过-s指定脚本,实现 POST 请求、动态参数、自定义报告。这是它比 ab 强的地方。下面是一个模拟 JSON POST 的脚本:

-- post_json.lua -- 定义请求方法、路径和 body wrk.method = "POST" wrk.path = "/api/submit" wrk.body = '{"id":123,"name":"test"}' wrk.headers["Content-Type"] = "application/json" -- 每个请求发出前调用,可动态改 body function request() -- 用随机数模拟不同 id,避免服务端缓存命中 local id = math.random(1000, 9999) wrk.body = string.format('{"id":%d,"name":"test"}', id) return wrk.format() end

逻辑说明:wrk.method、wrk.path、wrk.body、wrk.headers是全局配置,request()函数在每个请求前执行,返回wrk.format()生成最终请求。参数上,math.random需要配合math.randomseed(os.time())才能每次不同,否则随机序列固定。常见做法是在脚本顶部加一行math.randomseed(os.time())。另外,如果 body 里有中文,注意 Content-Length 的计算,wrk 会根据 body 字节数自动设置,但手动改 body 后要确保没有多余空格。

# 使用 Lua 脚本压测 wrk -t4 -c100 -d30s -s post_json.lua http://127.0.0.1:8080

逻辑说明:-s指定脚本路径,脚本里的wrk.path会覆盖命令行 URL 的路径部分,但主机和端口仍以命令行 URL 为准。参数上,脚本路径建议用绝对路径,避免相对路径在不同工作目录下找不到。如果脚本报语法错误,wrk 会直接退出并打印行号,按行号排查即可。

注意:Lua 脚本里的wrk.format()如果不带参数,会使用全局的 method、path、body、headers;如果带参数,比如wrk.format("GET", "/other"),则覆盖对应字段。这个细节在写多请求脚本时容易翻车。

4. 避坑与排查:那些让压测结果失真的细节

4.1 动态链接库缺失导致无法运行

现象:wrk --version报error while loading shared libraries: libssl.so.1.1: cannot open shared object file。原因:编译时的 OpenSSL 版本和当前系统不一致,或者系统没装对应版本的运行库。解决:先ldd ~/tools/wrk/wrk看缺哪些库,然后安装对应包。如果系统包版本对不上,常见做法是找一份同发行版编译的 wrk,或者用LD_LIBRARY_PATH指向包内自带的库目录(如果包里有 lib 文件夹)。

4.2 文件描述符上限卡住并发

现象:-c1000时大量Socket errors: connect,吞吐上不去。原因:测试机默认ulimit -n可能是 1024,减去已用描述符,剩给压测的不到 1000。解决:临时调大ulimit -n 65535,或者写入/etc/security/limits.conf永久生效。调完后重新登录或source配置,再用ulimit -n确认。

4.3 压测机和目标机在同一台导致数据虚高

现象:本机压本机,Requests/sec 高得离谱,换到另一台机器压就掉一半。原因:同机压测走 loopback,没有网络开销,CPU 也互相抢。解决:压测机和目标机分开,至少用两台机器;如果只能同机,把结果当作相对参考,别当绝对容量。常见做法是压测机配置不低于目标机,避免压测机先成为瓶颈。

4.4 Lua 脚本里的全局变量污染

现象:脚本跑一段时间后内存上涨,或者请求参数错乱。原因:Lua 里没加local的变量会变成全局,多个请求间互相覆盖。解决:在request()函数内所有变量都加local,比如local id = math.random(...)。这个坑很隐蔽,短时间压测看不出来,长时间跑才会暴露。

4.5 忽略预热直接看结果

现象:-d10s的前几秒吞吐明显偏低,拉低整体均值。原因:服务端连接池、JIT 编译、缓存都没热。解决:正式压测前先跑一轮-d10s预热,丢弃结果,再跑正式轮次。或者直接把-d拉到 60s 以上,让预热占比变小。

5. 进阶技巧:用 --latency 和自定义报告定位长尾

wrk 自带的--latency已经能给出百分位,但如果你想在压测过程中实时看延迟分布,或者把结果输出成结构化数据,可以借助 Lua 脚本的response()和done()回调。下面这个脚本在结束时打印自定义的 P99 和错误计数:

-- report.lua -- 初始化计数器 local errors = 0 local latencies = {} function response(status, headers, body) -- 非 2xx 计为错误 if status < 200 or status >= 300 then errors = errors + 1 end end function done(summary, latency, requests) -- 输出错误数和延迟统计 io.write("非 2xx 响应数: ", errors, "\n") io.write(string.format("P50: %.2fms\n", latency:percentile(50) / 1000)) io.write(string.format("P99: %.2fms\n", latency:percentile(99) / 1000)) io.write(string.format("最大延迟: %.2fms\n", latency.max / 1000)) end

逻辑说明:response()在每个响应到达时调用,参数是状态码、响应头和 body;done()在压测结束时调用,summary包含总请求数、错误数等,latency对象提供percentile()和max等。参数上,latency:percentile(99)返回的是微秒,除以 1000 转成毫秒。常见做法是把这些输出重定向到文件,方便和上一轮对比。

# 带自定义报告压测,结果存文件 wrk -t4 -c200 -d60s --latency -s report.lua http://127.0.0.1:8080/api/list | tee result_$(date +%s).log

逻辑说明:tee把输出同时打到屏幕和文件,文件名带时间戳避免覆盖。参数上,-s report.lua和--latency可以同时用,--latency的输出和脚本done()的输出会先后打印。如果脚本里用了io.write,注意不要和 wrk 自带输出混在一起导致解析困难,建议脚本输出用固定前缀,比如[CUSTOM]。

验证方法上,我一般会做两轮对比:一轮不加脚本,一轮加脚本,确认Requests/sec差异在 5% 以内,说明脚本本身没引入明显开销。如果差异过大,检查response()里是不是做了耗时操作,比如字符串拼接或文件写入——这些在每请求回调里执行会严重拖慢压测。

从那以后我每次拿到编译好的压测工具,都强制先跑ldd确认依赖,再跑一轮短时预热,最后才看正式结果。这套习惯帮我省掉了至少三次“数据看起来很美,上线就崩”的后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询