☰
wrk压测实战全解析:从解压到Lua脚本,掌握QPS与延迟分析
2026/10/7 17:05:22 网站建设 项目流程

简介:wrk.tar.gz 提供已编译完成的 wrk 高性能 HTTP 压测工具,面向需要评估 Web 服务、API 接口或负载均衡器性能的开发运维人员。压缩包约 214.16 MB,文件总数与类型明细未在资源页展示,压缩包内包含可直接执行的 wrk 主程序,解压后免去编译环节即可运行。工具支持 -c、-t、-d、-s 等参数控制连接数、线程数、压力时长,并能加载 LuaJIT 脚本模拟复杂请求模式、校验响应状态或设置固定请求速率。wrk 运行后会输出每秒请求数、传输速率以及延迟的均值、分位数统计,帮助快速定位服务瓶颈或对比不同配置与版本的表现。目前已有 258 人浏览学习,适合中高级后端开发者及性能测试人员作为实用工具包。

1. 一个已经编译完的 wrk:拿到手就能直接开压

做后端的人收到一个wrk.tar.gz,还带着“已经编译完”这几个字,心里应该先有个判断:这包省掉的是 gcc、make、libssl-dev 这一整条编译链的坑。wrk 是单机 HTTP 压测工具里非常能打的一类,它不搞 JMeter 那种线程组建模,靠 epoll/kqueue 事件驱动,用少量线程挂起大量并发连接,几秒钟就能打出一个接口的真实吞吐和延迟分布。它能回答的问题很直接:这个服务上线后能扛多少 QPS,99% 延迟从哪个并发点开始恶化。适合后端开发做上线前验证、性能测试做回归、运维定位容量瓶颈。这篇把解压、参数、脚本和常见翻车点一条线讲完。

2. 把 tar.gz 变成可执行文件:解压、授权、验证一条线

2.1 拿到包先列清单,别急着解压

收到包别急着tar -xzf。我一般先列一遍内容,确认包内有没有可执行文件、附带文件、目录层级,这一步花十秒,能避免解压到一半才发现包本身损坏或者压缩层级不对。

tar -tzvf wrk.tar.gz

一列,包内通常就是一个wrk二进制,可能附带 README、LICENSE 这类说明文件。-tzvf四个参数拆开看:-t只列出文件名,-z说明这是 gzip 压缩格式,-v显示权限和大小等详细信息,-f指定文件名。这一步值得仔细看的是权限位:如果wrk那行显示-rwxr-xr-x,说明作者在打包前已经保留了执行权限;如果显示的是-rw-r--r--,解压后直接执行就会碰到Permission denied。

确认没问题再解压:

mkdir -p /opt/wrk tar -xzvf wrk.tar.gz -C /opt/wrk chmod +x /opt/wrk/wrk ln -sf /opt/wrk/wrk /usr/local/bin/wrk

-C指定解压目标目录;chmod +x是给可执行位兜底——即使包里权限没问题,解压到某些挂载了noexec的目录后依然没有执行权限,强制加一次不亏;ln -sf把二进制软链到/usr/local/bin,之后在任意目录敲wrk都能调用。

验证这步不能省。执行:

wrk --version wrk --help

正常能看到类似wrk 4.x [epoll]的版本行和完整参数说明。[epoll]表示当前环境用的是 epoll 多路复用,macOS 上会显示[kqueue];如果你看到的是[poll]或[select],说明内核支持不完整,很可能是容器或老镜像引入的,这种情况下后边压测能吃掉的并发会明显缩水,要先解决运行环境而不是怀疑 wrk 本身。

2.2 “已经编译完”省掉的是最玄学的一段路

自己从源码编译 wrk 并不难,但坑都在环境里。常见做法是从 wrk 的源码仓库 clone 到本地,然后cd进目录直接执行:

make

源码编译的三个高频翻车点:第一,缺少 OpenSSL 开发头文件,make在链接阶段报openssl/ssl.h: No such file or directory,需要先装 libssl-dev;第二,同一份代码在 OpenSSL 1.1 的机器上编译出来,拷到只有 OpenSSL 1.0 的老机器上,运行时报version OPENSSL_1_1 not defined,这一类“编译机器能过、运行机器不行”的问题,排查起来最耗时;第三,make 过程需要先把内置的 LuaJIT 编出来,如果编译器太老或者目录缺文件,报错位置经常在deps/里而不是 wrk 主代码,看起来莫名其妙。

这也是“已经编译完”这个 tar.gz 的核心价值:它把编译环境从运行环境里剥离开了。你不需要在线上机器装一堆编译依赖,也不需要理解那些玄学的版本错配,拿到包解压就有一个能跑的二进制。代价是:这个二进制跟具体运行环境的 glibc、libssl 存在绑定关系,换机器就要重新验证。解压后先用ldd看一眼动态库依赖是个好习惯:

ldd ./wrk

输出里会列出 libc、libm、libssl、libpthread 这类动态库路径。如果某个库显示not found,说明运行环境缺依赖,用系统包管理器补上对应运行库即可,不需要重新编译。注意ldd只是查依赖存在性,真正确认能不能跑,要实际跑一次wrk --version。

2.3 目录布局与版本隔离:别让 PATH 里同时出现两个 wrk

生产环境常见的做法是把 wrk 放在独立目录,比如/opt/wrk/,而不是直接扔进/usr/bin后和系统包管理器装的另一份 wrk 混在一起。原因很实际:你手里这个 tar.gz 可能来自同事,也可能来自内部构建机,它的版本和系统自带的 wrk 不一定一致,两个版本同时出现在 PATH 里,后面排查“怎么结果不一样”能省去一半时间。

我一般会在wrk软链之外,把原始 tar.gz 留一份放在同一个目录里,再在目录里写一个README记录这个包是哪台机器、什么时间、什么环境编出来的。这不是形式主义——等几个月后你挂一个新的压测任务,发现性能数据和当时对不上时,这个 README 就是唯一的后悔药来源。至少记三行:编译机器的 glibc 版本、wrk 版本哈希、打包时间。

软链还有个隐患:如果/usr/local/bin里已经存在另一个版本的 wrk,ln -sf会直接覆盖软链,运行时会实际指向你新解压的这个版本。想保留两份做对比,就不要在/usr/local/bin做软链,直接维护wrk-old、wrk-new这样的双软链,按任务切换。

3. 最小压测命令:把 QPS 和延迟分布一次读出来

3.1 先看懂四个核心参数

wrk 的日常使用,大部分场景只需要-t、-c、-d三个参数,再加一个--latency输出延迟分布。

参数含义典型值说明
-t线程数CPU 核数或略高于核数线程不是越多越好,后面单独讲
-c并发连接数几百到几千模拟同时在线的连接数
-d压测时长30s、1m、2m太短结果不稳,太长浪费时间
--latency输出延迟分布开关不加它就没有 p50/p75/p99
-sLua 脚本路径post.lua见第 4 章
-H自定义请求头-H "Cookie: xx"可重复使用多次
-T超时时间5s设置后超时连接会计入 socket errors

3.2 跑一条真实的压测命令

假设本地已经起了一个 nginx,监听 8080,我们压它的/index.html:

wrk -t8 -c400 -d30s --latency http://127.0.0.1:8080/index.html

这条命令的意思:8 个线程、400 个并发连接、持续 30 秒,并且输出延迟分布。8 个线程是几核机器上比较通用的起步值;400 个连接用于模拟 400 个客户端同时保持连接;30 秒能覆盖大部分波动,也足够连接池和热点代码预热。

典型输出大致这样:

Running 30s test @ http://127.0.0.1:8080/index.html 8 threads and 400 connections Thread Stats Avg Stdev Max +/- Stdev Latency 2.34ms 0.87ms 18.42ms 88.76% Req/Sec 25.41k 2.13k 34.00k 90.30% Latency Distribution 50% 2.10ms 75% 2.87ms 90% 3.42ms 99% 5.91ms 6065672 requests in 30.00s, 1.34GB read Requests/sec: 202188.80 Transfer/sec: 45.55MB

这个输出就是压测报告的灵魂。从上到下看:Thread Stats是每个线程自己的延迟和请求速率统计,Avg是平均值,Stdev是标准差,Max是最大值,+/- Stdev表示有多少样本落在均值一个标准差内,这个值越高说明波动越小;Latency Distribution是全部请求的延迟分位,p99一行是重点关注。

3.3 五组指标怎么读

第一看Requests/sec,这是整体的吞吐量,也就是这台机器在给定并发下每秒能处理的请求数。第二看Latency Distribution里的p99,这是长尾用户的实际体验,平均延迟再好看,p99翻车就说明系统在大并发下开始排队。第三看Transfer/sec,它决定压测链路的带宽是否够,假如被测接口返回 1MB 的包,Transfer/sec会先触顶,这时不是服务的 QPS 不行,而是网卡带宽不够。第四看Socket errors,正常压测这一行不该出现,出现任何一个数字都要回头排查,详见第 5 章。第五看Thread Stats里的Req/Sec分布,如果 8 个线程之间的吞吐差异很大,说明 wrk 的连接分配不均匀,或者被测服务在某个核上存在热点。

提示:压测时间太短时,Latency Distribution里Max那一列会很不稳定,建议-d至少 30 秒起步;要看p99的规律,-d 1m更靠谱。

3.4 线程数和连接数怎么选:不是越大越好

线程数的选择逻辑:wrk 是事件驱动模型,线程主要负责事件循环和 Lua 回调,线程数一般取 CPU 逻辑核数的 1 到 2 倍。比如 4 核机器用-t4,8 核机器用-t8。线程数一旦超过核数,操作系统就开始做线程切换,压测机自己的 CPU 会先被打满,得出来的 QPS 是压测机极限而不是被测系统极限。很多人习惯性-t64,结果压出来数字反而比-t8低,这就是典型的自欺欺人。

连接数的选择逻辑:-c模拟的是“同时在线的连接数”,它跟“每秒请求数”是两个维度。HTTP keep-alive 场景下,400 个连接可以持续发请求,QPS 取决于每个连接的发包速率和服务端处理速度;如果被测接口是短连接场景(比如每次请求新建 TCP),连接数就不需要太大,反而是 wrk 的网络栈会成为瓶颈。我一般从-c200开始,逐步加到 400、800、1000,观察 QPS 和延迟的变化曲线。QPS 不再增长甚至开始下降的那个点,就是容量拐点。

调整时还要带上-T超时参数。不设置-T时 wrk 会一直等响应,接口慢不慢都会被计入延迟;设置-T 5s后,超过 5 秒的连接会被记为 timeout 错误,更接近真实用户行为——用户不会等一个接口十年。

4. Lua 脚本压测:POST、鉴权与动态参数的实战写法

4.1 为什么压测不能只靠固定 GET 请求

真实业务里很少有纯 GET 无参场景:下单、登录、查询,都要带 POST body、请求头、签名或动态参数。wrk 命令行只能通过-H设置固定请求头,body 也只能用固定值,这跟真实用户的差距太大。wrk 的 Lua 扩展能力就是为了解决这个问题存在的——你可以在脚本里改 method、改 header、改 body,也可以在每次请求前动态生成参数,还可以在结束后统计响应码分布。

4.2 最小 POST 脚本

wrk.method = "POST" wrk.headers["Content-Type"] = "application/json" wrk.body = '{"scene":"auth","mobile":"13900000000"}' function request() return wrk.format(nil, nil, nil, wrk.body) end

这段脚本里,wrk.method、wrk.headers、wrk.body是 wrk 脚本里的全局配置字段;request()函数在每个请求发出前被调用一次,必须返回一个合法的 HTTP 请求文本;wrk.format负责把 method、path、headers、body 拼成一个完整请求。这里的四个参数,前三个传nil表示沿用全局配置:method 用wrk.method,path 用命令行 URL,headers 用wrk.headers。保存为post.lua后运行:

wrk -t4 -c100 -d30s -s post.lua http://127.0.0.1:8080/api/order

注意命令行 URL 的路径会被作为 path 兜底。如果命令行 URL 带路径,脚本里wrk.format不传 path 就沿用这个路径,这是新手最容易混的地方。

4.3 动态参数:自增 ID 和 token 池

真实用户不会每次请求都用同一个 body。最常见的动态参数是自增 ID:

local counter = 0 function request() counter = counter + 1 local body = string.format('{"userId": %d}', counter) return wrk.format("POST", "/api/v1/query", {["Content-Type"]="application/json"}, body) end

这里有个重要的坑:wrk 每个线程有自己独立的 Lua 环境,counter在 4 个线程里是从 0 各自计数的,也就是说有 4 个线程会同时产生userId=1。如果被测接口对 userId 有去重逻辑,这就会误伤压测结果。解法是在setup阶段给每个线程分配不同的 ID 区间:

local startId = 0 local step = 1000000 function setup(thread) local idx = thread:get("idx") if not idx then idx = 0 end thread:set("idx", idx + 1) startId = idx * step end function request() startId = startId + 1 local body = string.format('{"userId": %d}', startId) return wrk.format("POST", "/api/v1/query", {["Content-Type"]="application/json"}, body) end

setup(thread)会在每个线程启动时被调用一次,thread:get("idx")读取共享的 idx,thread:set把下一个值写回去。第一个线程拿到 0,第二个拿到 1,依此类推,每个线程生成的 userId 落在自己的区间里,不会撞车。注意这里对共享 idx 的读写不是原子操作,存在很小的竞争窗口,但在线程启动阶段冲突概率极低,压测场景里完全够用。

4.4 鉴权接口:批量 token 轮流用

有鉴权的接口,最简单的方式是用-H "Authorization: Bearer xxx"带上单个 token。要模拟更真实的用户分布,可以准备一批 token 轮流使用:

local tokens = {} function setup(thread) local f = io.open("tokens.txt", "r") for line in f:lines() do tokens[#tokens + 1] = line end f:close() end function request() local token = tokens[math.random(1, #tokens)] return wrk.format("GET", "/api/user/profile", {["Authorization"] = "Bearer " .. token}) end

setup在每个线程启动时执行一次,所以tokens.txt会被每个线程各读一遍,4 个线程就有 4 份副本,这没问题。真正要注意的是math.random,不加math.randomseed的话,所有线程的随机序列可能一样,导致同一时间所有连接都在用同一个 token。建议在setup里加一句math.randomseed(os.time() + idx),种子不同,分布才会散开。

4.5 response 和 done:把错误率也打出来

只统计 QPS 和延迟还不够,压测里响应码分布同样关键。用response回调统计状态码:

local ok, bad = 0, 0 function response(status, headers, body) if status == 200 then ok = ok + 1 else bad = bad + 1 end end function done(summary, latency, requests) io.write(string.format("ok=%d bad=%d ratio=%.2f%%\n", ok, bad, bad * 100.0 / (ok + bad))) end

response(status, headers, body)在每次响应返回后被调用,这里只做计数,开销很小;done(summary, latency, requests)在压测结束后被调用一次,summary里有总请求数,latency是延迟统计对象,可以调用latency:percentile(99)输出 p99。需要注意,response回调里如果做字符串查找、JSON 解析这类重活,会把 wrk 的事件循环堵住,QPS 直接掉一半,需要注意控制。

5. 压测避坑指南:5 个常见问题的现象、原因与处理

5.1 解压后执行报 Permission denied

现象:./wrk: Permission denied,但是文件明明存在,ls -l也能看到它。

原因:tar 包是在 Windows 或某些没保留权限位的环境下打的,解压后 wrk 的权限是-rw-r--r--,没有执行权限;另一种可能是解压目录被挂载为noexec。

解决:先看权限ls -l wrk,是-rw-r--r--就直接chmod +x wrk;权限没问题却还报错,用mount | grep noexec查目录挂载参数,把包解压到/opt或/usr/local这类常规执行目录。不要试图用bash wrk或者python wrk硬跑,wrk 是原生二进制,不能靠解释器执行。

5.2 运行报“GLIBC_2.3x not found”

现象:./wrk: /lib/x86_64-linux-gnu/libc.so.6: version 'GLIBC_2.34' not found。

原因:编译这个 tar.gz 的机器 glibc 比当前运行机器新,wrk 动态链接了高版本 libc,老系统里自然找不到。这是“编译完的包”最典型的翻车点,也是它最黑匣子的地方——编译机上跑得好好的,拷到内网老机器上就挂。

解决:先ldd ./wrk查看动态库依赖,确认缺的是 libc 还是 libssl;如果是 glibc 版本不匹配,正解是找一台与运行环境 glibc 版本相同或更老的机器重新编译,再打一个 tar.gz。如果压测环境和运行环境差异很大,可以考虑静态编译版本,但 OpenSSL 也得一并静态,维护链路会变得复杂,我更倾向于维护多套编译产物,按 glibc 版本分目录存放,而不是指望一个包打天下。

注意:拿到任何“编译完”的二进制,第一时间用ldd和--version验证,别直接上压测。

5.3 并发拉高后 Socket errors 一片

现象:wrk 输出里出现Socket errors: connect 0, read 12, write 0, timeout 3,而且 QPS 怎么都上不去。

原因:read错误通常是被测服务端连接被重置或主动断开,常见于服务端文件描述符耗尽、accept queue 满了,或者服务端 keep-alive 超时太短;timeout错误则是响应超过了 wrk 的-T阈值;还有一类是压测机自己的端口耗尽,TIME_WAIT堆积导致新连接无法建立。

解决:先看服务端ulimit -n和ss -lnt的 accept queue,调大net.core.somaxconn;压测机方面,检查端口占用:

netstat -an | grep TIME_WAIT | wc -l

如果这个数字上万,说明端口号不够轮转。压测专用机可以放宽本地端口范围并开启tcp_tw_reuse:

sysctl -w net.ipv4.ip_local_port_range="1024 65535" sysctl -w net.ipv4.tcp_tw_reuse=1

注意这是压测机器的配置,不要顺手改到线上业务机。调完后再跑,Socket errors一般会归零。

5.4 压测结果忽高忽低:先怀疑压测机自己

现象:同一个命令连续跑三次,Requests/sec分别是 20w、12w、18w,拉曲线出来像锯齿。

原因:多数时候不是被测服务在波动,而是压测机自身状态不行。wrk 是 CPU 密集型的压测工具,压测机的 CPU 被占满或降频,QPS 就直接被打折;如果压测机还跑着 CI、日志采集这类定时任务,结果更是没法看。

解决:压测前先确认压测机空闲:

top vmstat 1 3

看us(用户态)和sy(内核态)占用率,sy过高说明系统调用和网络栈是瓶颈;再看 CPU 频率是否掉到基准值以下,笔记本和部分云主机跑久了会降频,需要关掉节能策略或换一台机器。线程数也别贪多,-t保持核数附近,多出来的线程只会增加上下文切换。

5.5 Lua 脚本里做重活,QPS 直接腰斩

现象:给 wrk 加了 Lua 脚本做响应校验后,吞吐量从 150k 掉到 70k,服务端 CPU 占用却不高。

原因:wrk 的 Lua 回调运行在线程的事件循环里,response回调每来一个响应就执行一次。如果回调里做了string.match正则解析、大 JSON 切分、写日志文件这些操作,事件循环被长时间占用,新的请求就排不上队。脚本越重,wrk 自身消耗就越大,最后测的是 wrk 自己而不是被测系统。

解决:response回调里只保留计数和简单的状态判断,任何复杂校验都放到压测结束后的离线分析里。如果确实需要采样,可以按概率抽样:

function response(status, headers, body) if math.random(100) <= 1 then -- 只对 1% 的响应做深入检查 if string.find(body, "error") then errSample = errSample + 1 end end end

抽样把事件循环的占用压到 1%,QPS 损耗基本可以忽略。记住:压测工具的使命是制造流量,不是做业务逻辑。

6. 让压测结果可信:三个我一直在用的执行细节

6.1 预热后再正式打

wrk 启动后的前几秒,连接在建立、缓存和连接池在预热,这部分的延迟和吞吐不能代表稳态。我一般先用低并发跑 10 秒预热:wrk -t4 -c50 -d10s URL,然后再上正式并发。预热结果不保存,只让它把链路跑热。

6.2 做三次取中位,不要取最大

单次压测结果受 GC、定时任务、网络抖动影响很大。我会把三次结果存成文件,取中位数作为汇报值,而不是挑最大的一次发出来——最大的一次好看,但那是运气,不是系统能力。

for i in 1 2 3; do wrk -t8 -c400 -d30s --latency http://127.0.0.1:8080/ \ | tee /tmp/wrk-run-$i.log done

tee把输出同时打到终端和文件,三次结果在三个文件里,对比Requests/sec那行就能看出波动范围。

6.3 压测机与被测机分开,并先看一眼压测机

我吃过一次亏:把 wrk 放在被测服务器上压,QPS 压到了瓶颈,结果 CPU 曲线里分不清哪部分是服务消耗、哪部分是 wrk 消耗,最后只能换机器重测,白白浪费一晚。现在我的习惯是:压测前先top看压测机空不空,再确认两台机器之间带宽足够,最后才动手。压测机不够干净,结果就是垃圾进垃圾出。

wrk 压测的真功夫不在跑命令,而在让结果能复现、能对比。把上面三个习惯固化成流程后,我基本不再为“这个数字可不可信”发愁。希望帮到你。

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

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

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

立即咨询