简介:这是一份面向Linux平台的wrk 4.1.0高性能HTTP基准测试工具资源包,主要服务于Web后端开发、运维以及性能测试人员,用于评估高并发和短连接场景下服务器的吞吐量、响应时间与成功率。wrk本身用C语言编写,具备轻量、高效、易扩展等特点,与JMeter和ab相比,更适合快速完成压测与瓶颈定位。资源包采用7z格式压缩,大小约168.72MB,内部文件明细暂未列出;内容以工具详解为主线,涵盖编译部署方法、常用命令行参数、自定义请求头、Lua脚本扩展测试场景、结果关键指标解读,以及与其他主流压测工具的选型对比,能够帮助读者从环境准备到实际测试逐步掌握。目前已有586人学习下载,适合希望深入理解并发连接数、线程数、持续时间等参数对压测结果影响,并借助Lua脚本定制请求的开发与运维人员。
1. wrk 是什么:为什么我在 Linux 后端压测里首选它
我在 Linux 上做后端接口压测时,选型清单里的首选性能测试工具是 wrk。之前用 JMeter,装插件、配线程组、看聚合报告,光准备环境就得半小时;用 ab 又太粗,只有总请求数和平均响应时间,连接复用、延迟分布全看不出来。wrk 靠 epoll 和异步 I/O,用几个线程就能管理几千个连接,单机压到几万 QPS 很常见,所以不管是上线前做容量摸底,还是压一个接口看优化有没有效果,我都会先把 wrk 拉起来跑一轮。这份 wrk-4.1.0-linux 源码包,编译安装后用一条命令就能跑。正文把依赖、参数、Lua 扩展和常见坑都拆开讲,适合刚接触压测的后端开发,也适合要把压测固化成回归脚本的运维。
2. 安装与编译:老版本陷阱、三个依赖报错和第一次跑通
2.1 先确认系统里是不是已经装了一个老版本 wrk
很多发行版直接封装了 wrk。Ubuntu/Debian 系列执行 apt install wrk 就能装上,但仓库里的版本往往落后几个大版本,4.1.0 里修复的 TLS 握手问题、新增的 response 回调,在老版本里全都没有。拿到这份源码后第一步不是急着编译,而是先看本机已装的版本。
提示:执行 wrk -v,如果输出类似 Version 0.7.0 这种旧版本,建议直接用源码编译 4.1.0。两个版本的输出格式和 Lua 脚本行为差异很明显,回归脚本混着用会得到错误结论。
我在 Kali Linux 上就吃过这个亏,apt 装的 wrk 跑 Lua 脚本时 response 函数完全不执行,查了源码才发现是老版本行为。算上这个,编译阶段真正要防的是四类问题:老版本覆盖、缺 gcc、缺 make、缺 OpenSSL 头文件,下面逐个说。
2.2 编译依赖:缺哪个库,错误信息长什么样
wrk 的编译依赖其实只有三样:gcc、make、libssl-dev。wrk 会把 Lua 解释器编进二进制里,所以不需要单独装 Lua;唯一的外部库是 OpenSSL,用来支持压测 HTTPS 接口时做 TLS 握手。
Debian/Ubuntu/Kali 系:
sudo apt-get install -y gcc make libssl-devCentOS/Rocky 系:
sudo yum install -y gcc make openssl-devel依赖没装齐时,编译错误比配置错误好认得多,我遇到过的三类报错:
- 报 gcc: command not found,缺 gcc,装上再编译。
- 报 make: command not found,缺 make,同样先装。
- 报 fatal error: openssl/ssl.h: No such file or directory,缺 libssl-dev 或 openssl-devel。最常踩的是这个,别怀疑代码问题。
wrk 没有 configure 脚本,官方包直接就是 Makefile 组织。看到压缩包里没有 configure 文件别慌,不需要执行 ./configure。
2.3 编译、安装和版本验证
tar zxf wrk-4.1.0-linux.tar.gz cd wrk-4.1.0 make -j"$(nproc)" sudo make install wrk -v逻辑说明:tar 解包后直接 make,-j"$(nproc)" 让编译任务跑满所有逻辑核,编译会生成 src/wrk 可执行文件;make install 默认放进 /usr/local/bin,这样 PATH 里直接能调用。如果不想动系统目录,也可以只执行 make,然后把 src/wrk 手动拷贝到任意目录并加入 PATH。wrk -v 输出 Version 4.1.0 [wrk] 就说明编译成功。整个二进制是静态带上 Lua 解释器的,复制到另一台同架构 Linux 上也能跑,不用带一堆依赖。
2.4 第一次压测:把输出每一行读明白
装好之后对本地 Web 服务跑一条最小命令:
wrk -t4 -c100 -d20s --latency http://127.0.0.1:8080/参数含义:-t4 表示开 4 个线程,-c100 表示维持 100 个并发连接,-d20s 表示持续 20 秒,--latency 会让 wrk 在结束时打印延迟百分位表。压测结束后输出类似这样:
Running 20s test @ http://127.0.0.1:8080/ 4 threads and 100 connections Thread Stats Avg Stdev Max +/- Stdev Latency 9.80ms 4.20ms 96.00ms 86.94% Req/Sec 6.40k 480.00 7.80k 79.40% Latency Distribution 50% 8.50ms 75% 11.60ms 90% 15.20ms 99% 32.10ms 512640 requests in 20.00s, 76.45MB read Requests/sec: 25632.00 Transfer/sec: 3.82MB逐行说明:Thread Stats 是每个线程内部统计求和后的分布,Latency 的 Avg/Stdev/Max 反映响应时间,Req/Sec 是每线程每秒处理请求数;Latency Distribution 在加 --latency 后才出现,50% 对应 p50,99% 对应 p99,比只看平均值可靠得多。Requests/sec 是最终吞吐,Transfer/sec 是响应带宽。如果中途出现 connect/read/write/timeout 错误,这里会多一行 Socket errors: connect 0, read 0, write 0, timeout 0。第一次跑只要能同时看到 4 threads、请求数和 Requests/sec 三行,工具环境就算通了。
wrk 的适用边界也要说清楚。它只支持 HTTP/1.1,压 HTTP/2 接口会直接报错或退化到 HTTP/1.1;它不会保存响应体,只统计响应数据长度,所以别指望用 wrk 做响应内容校验。这两种场景我分别会用 h2load 和带断言的脚本去补。wrk 的定位就是在 Linux 上快速、高并发地压 HTTP/1.1 接口,选型选对工具,后面才不翻车。
3. 参数调优:-t/-c/-d 的配合逻辑和延迟分布读法
3.1 三个核心参数的配合逻辑
wrk 的命令行参数不多,真正决定压测形态的是 -t、-c、-d 三个。很多新手把 -t 当作“并发数”,直接 wrk -t10000 -c10000,然后看 wrk 自己先挂掉,这就是把线程和连接搞混了。
-t 是线程数,-c 是连接总数,wrk 会把连接平均分配到每个线程。设计压测参数时我一般按“核数”或“核数 × 2”来定线程数,比如 8 核机器用 -t8 或 -t16,再把连接数设为线程数的整数倍,比如 -t8 -c400,这样每个线程管 50 个连接。线程数开得比 CPU 核数多太多,wrk 自身会花大量时间在上下文切换上,测出来的不是服务上限,是 wrk 的上限。
常用参数表:
| 参数 | 作用 | 我的默认习惯 |
|---|---|---|
| -t/--threads | 压测线程数 | 逻辑核数或核数 × 2,不超过核数 × 4 |
| -c/--connections | 维持的连接总数 | 线程数的 10~50 倍,按目标并发量调整 |
| -d/--duration | 压测时长 | 30s 起步,慢接口 60s |
| -T/--timeout | 单个请求超时 | 默认 10s,慢接口适当调大防止误计超时 |
| -H/--header | 附加请求头 | 需要自定义头时用,可重复传 |
| -s/--script | Lua 脚本 | 需要 POST、鉴权、随机场景时用 |
-d 决定一次压测跑多久,太短结果不稳定。我一般先跑 10s 当预热,再正式跑 30s;如果接口偶发慢请求,比如最慢响应要 5s 才能回来,时长至少覆盖 60s,否则 p99 永远是截断的。
3.2 延迟分布:平均值会骗人,百分位才是真话
wrk 默认输出里的 Latency Avg/Stdev 很容易误导人。平均值会被大量低延迟请求拉低,一个尾巴上的 1.5s 慢请求在 100ms 的平均值上几乎看不出来,所以压测时一定要加 --latency,让 wrk 打印 Latency Distribution 那张百分位表。
读法上,我习惯重点看 99% 和 50% 的差距:比如 p50 = 8ms,p99 = 32ms,说明绝大多数请求响应健康;如果 p50 = 10ms,p99 = 900ms,说明长尾严重,接口或连接池可能在某些情况下退化了。只看 Requests/sec 高不高是不够的,吞吐高但 p99 烂,线上照样用户投诉。
还有一类情况要警惕:如果 Latency Distribution 里的 99% 数值接近 -d 设定的时长,说明有一部分请求在压测结束前还没跑完,p99 是被截断的,要把时长拉长再跑。wrk 4.1.0 的百分位表只打印到 99%,连 99.9% 都没有,想要更精确的长尾数据可以在 Lua 脚本里拿延迟统计对象自己算,这块在下一章讲。
3.3 压测机系统参数:ulimit、端口范围和 TIME_WAIT 复用
wrk 自身能管理的连接再多,也会被 Linux 的系统参数卡住。常见现象是压测跑到一半,wrk 报错误或连接失败,这不是被压服务挂了,而是客户端这边先撑不住了。
# 临时提高当前 shell 的文件描述符上限,注意要在 wrk 同一个终端里执行 ulimit -n 1048576 # 扩大可用客户端端口范围,短连接压测时非常关键 sudo sysctl -w net.ipv4.ip_local_port_range="1024 65535" # 开启 TIME_WAIT 复用,缩短端口回收时间(NAT 环境慎用) sudo sysctl -w net.ipv4.tcp_tw_reuse=1每条的作用:ulimit -n 决定进程能打开多少文件描述符,一个连接在 Linux 上至少要占一个 fd,默认 1024 上限意味着 -c 超过 1000 就会碰到瓶颈;ip_local_port_range 决定本地发起连接时可用的源端口范围,连接数多时不改会报 Cannot assign requested address;tcp_tw_reuse 让处于 TIME_WAIT 的端口更快被复用,但 NAT 后面开这个可能把旧连接的包复用到新连接上,业务安全优先的话不建议在生产压测机默认开启。这些配置只在当前机器当前启动的压测任务里有效,正式做压测时建议写进 /etc/sysctl.conf 再 sysctl -p 永久生效。
4. Lua 扩展:POST 请求、鉴权头和随机用户的四个模板
wrk 内置的参数只适合 GET 压测。实际压测基本都要登录态、POST 体、随机参数,这时候就要用 -s 指定 Lua 脚本。wrk 的脚本不是每个请求起一个虚拟机,每个线程共享一个 Lua state,函数是事件风格:request 在每次发请求前被调用,response 在每次收到响应后被调用,done 在压测结束时被调用一次。
4.1 最小脚本骨架:method、body、headers 三个全局量
-- post.lua:固定 POST 请求 wrk.method = "POST" wrk.headers["Content-Type"] = "application/json" wrk.body = '{"page":1,"size":20}'逻辑说明:wrk.method、wrk.headers、wrk.body 都是 wrk 暴露的全局变量,脚本加载时设置一次,之后每个请求都会带上。运行方式是 wrk -t4 -c100 -d30s -s post.lua http://127.0.0.1:8080/api/list。注意 wrk.headers 是 Lua table,和普通 Lua 一样,重复设置同一个 key 会覆盖。wrk 默认会带 User-Agent: wrk/4.1.0,想伪装客户端的话在脚本里覆写 wrk.headers["User-Agent"] 即可,这个动作在 header 表里是一行字符串,别在里面塞 \r\n,否则可能被对端解析成额外请求头。
4.2 带随机用户的压测:预读账号文件,别在 request 里读文件
压测带登录态的接口时,常见错误想法是在 request 函数里打开文件、按行读用户再关闭文件。request 每次请求都执行,压几百并发时每个线程都在做磁盘 IO,wrk 自己先变成了瓶颈,测出来的数字全废。正确做法是在脚本加载阶段一次性读入内存,request 里只做内存操作:
-- random_user.lua:从账号文件里随机取一组,带自定义 token 头 local users = {} local f = io.open("users.txt", "r") if f then for line in f:lines() do local uid, token = line:match("([^,]+),([^,]+)") if uid and token then users[#users + 1] = { uid = uid, token = token } end end f:close() end math.randomseed(os.time()) function request() local u = users[math.random(#users)] wrk.method = "GET" wrk.headers["X-Uid"] = u.uid wrk.headers["X-Auth-Token"] = u.token return wrk.format() end逻辑说明:users.txt 每行格式是 uid,token;脚本加载时用 io.open 读取,按逗号切分后放进 users 表;request 里 math.random(#users) 取一个随机用户,然后设置请求头和 method,wrk.format() 生成实际发送的请求。参数说明:math.randomseed(os.time()) 要放在脚本顶层,因为 wrk 每个线程都会执行一次脚本初始化,不设置随机种子的话,所有线程可能拿到同一串伪随机序列,压测看起来像同一个用户在重复刷。wrk.format() 可以传参覆盖 method、path、body、headers,在这里无参调用就沿用全局设置。
4.3 用 response 回调统计非 200 和精确长尾延迟
wrk 自带的输出不带 99.9% 百分位,也不统计非 200 请求。response 回调补上这两块:
-- track.lua:统计非 200 响应,并打印 p999 延迟 local bad = 0 local total = 0 function response(status, headers, body) total = total + 1 if status ~= 200 then bad = bad + 1 end end function done(summary, latency, requests) io.write(string.format("非200数量: %d / %d\n", bad, total)) io.write(string.format("p99.9: %.2fms\n", latency:percentile(99.9))) end逻辑说明:response 回调接收状态码、响应头和响应体,这里只对状态码做计数;done 回调在压测结束时拿到 summary 统计对象和 latency 统计对象。latency:percentile(99.9) 是 wrk 统计对象内置的百分位方法,可以算出输出里没有的 p99.9,这是把 wrk 结果接到质量门槛上的常用做法。注意 response 里的 body 在 wrk 4.1.0 中是字符串,响应体很大时不建议在这里做字符串拼接,会拖慢 wrk 线程;需要校验响应内容时只取 body:sub(1, 128) 截取头部就好。
4.4 每次请求动态改变 body 的写法
压测上传或需要时间戳参数的接口时,body 不能固定。wrk 允许每个请求重新生成 body:
-- dynamic_body.lua:每次请求生成带时间戳的 JSON local users = { "u1", "u2", "u3" } function request() local body = string.format('{"user":"%s","ts":%d}', users[math.random(3)], os.time()) return wrk.format("POST", "/api/report", nil, body) end逻辑说明:request 函数内用 string.format 生成新的 body,再通过 wrk.format 的第四个参数直接传给本次请求,不修改全局的 wrk.body,避免影响同一线程内其他请求。参数说明:wrk.format 的完整签名是 wrk.format(method, path, headers, body),headers 传入 nil 表示沿用全局 header,body 传入字符串就覆盖默认 body。注意 os.time 单位是秒,同一秒内高并发场景多次请求的时间戳可能相同;需要毫秒级唯一值,可以配合计数器或 math.random 拼一个递增位进去。
5. 避坑与排查:五个翻车现场和对应修正
5.1 连接与端口层面的两个高频坑
坑一:压到一半报 Cannot assign requested address。
现象:-c 开到几千后,wrk 跑几秒就中断,屏幕上出现大量 connect 错误,Requests/sec 行要么缺失要么低得离谱。
原因:客户端端口耗尽了。Linux 默认 ip_local_port_range 通常只有两万多个端口,每个连接都要占一个源端口,短连接压测下几秒钟就能把端口用完。
解决:压测前先执行 ulimit -n 1048576,再扩大端口范围 sysctl -w net.ipv4.ip_local_port_range="1024 65535",同时确认 wrk 所在机器和被测服务之间不是 NAT 环境。如果压测的是开启 keep-alive 的连接,连接建立一次后复用,这个问题基本不会出现。
坑二:压测 HTTPS 接口直接握手失败,错误全记在 connect 里。
现象:wrk 压 http 一切正常,改成 https:// 后大量 connect 错误,服务端访问日志里看不到几个请求。
原因:wrk 编译链接的 OpenSSL 版本太旧,或者跑在容器里缺证书,导致 TLS 握手被拒。
解决:先单独验证 openssl s_client -connect 目标地址:端口 -servername 域名 </dev/null,能正常输出证书链说明目标服务没问题,那就是 wrk 编译环境的 OpenSSL 太老,重装 libssl-dev 后重新编译 wrk-4.1.0,别用 apt 装的旧版。
5.2 Lua 脚本与结果可信度的三个坑
坑三:request 函数里做文件 IO,压测吞吐上不去。
现象:加了 Lua 随机用户脚本后,QPS 从 2 万掉到 2000,CPU 使用率很高但都在 iowait。
原因:request 每次请求都触发磁盘读,wrk 的核心优势是异步事件循环,任何阻塞调用都会让整个线程停下来等 IO。
解决:按 4.2 的做法,把用户数据在脚本加载阶段预读进内存。判断一只 Lua 脚本是否拖累 wrk 的方式很简单:跑 -c1 单连接看 QPS,再跑 -c100 看 QPS 是否线性上涨,不上涨就检查脚本里有没有阻塞调用。
坑四:加了 --latency 还是觉得延迟分布不可信。
现象:延迟表里 p50 和 p99 几乎一样,或者所有延迟低于 1ms,和一个 RT 几十毫秒的接口明显矛盾。
原因:两个常见误用叠加:一是压测和服务在同一台机器,网络栈和调度相互干扰;二是 wrk 默认 keep-alive,连接已建立的情况下测到的是连接复用时的响应时间,和用户侧每次新建连接的体验不是一回事。
解决:压测机和服务分开部署;再用 Lua 脚本里 wrk.headers["Connection"]="close" 压一次做对比,两个结果差得多就说明连接建立开销占大头,这时要看的是整体吞吐而不是单次延迟。
坑五:同一条命令两次压测结果差 20% 以上。
现象:连续执行两次相同命令,Requests/sec 前者 18000 后者 22000,连 p50 都变了。
原因:压测机上 CPU 频率波动、后台定时任务、wrk 线程被调度到不同核上,都会造成误差。压测是最容易被“玄学”数字骗的活,没有基线只有单次结果等于没测。
解决:同一组参数至少跑三次取中位数;正式压测前先跑 10s 预热;追求稳定的话用 taskset 把 wrk 绑定到固定 CPU 核上。下面这个命令循环三次,输出三次 QPS 取中位数:
for i in 1 2 3; do wrk -t4 -c100 -d30s http://127.0.0.1:8080/api/ping 2>/dev/null | grep "Requests/sec" | awk '{print $2}' done三次输出里的中间值就是这次压测的结论,单次数字不作为上线依据。
6. 把压测接进日常:结果提取、阈值卡点与回归门槛
wrk 的输出是给人看的,不是给脚本看的。我一般把压测接到发布流程里,先提取数字,再卡阈值。
6.1 用 shell 提取 Requests/sec
#!/usr/bin/env bash wrk -t4 -c100 -d10s --latency http://127.0.0.1:8080/api/ping > /tmp/wrk.log 2>&1 qps=$(grep "Requests/sec" /tmp/wrk.log | awk '{print $2}') echo "当前 QPS: ${qps}"grep 匹配输出里的 Requests/sec 行,awk 取第二列就是数值。wrk 的错误信息会输出到 stderr,2>&1 是为了把错误也留档方便排查。
6.2 阈值卡点:低于基线就失败
#!/usr/bin/env bash threshold=8000 qps=$(wrk -t4 -c100 -d10s http://127.0.0.1:8080/api/ping 2>/dev/null | grep "Requests/sec" | awk '{print $2}') # bc 做浮点数比较,qps 为空时直接判失败 if [ -n "$qps" ] && [ "$(echo "$qps >= $threshold" | bc)" -eq 1 ]; then echo "压测通过:QPS=${qps},未低于阈值 ${threshold}" else echo "压测未通过:QPS=${qps:-0},低于阈值 ${threshold}" exit 1 fi这里的 threshold 不要拍脑袋定,我一般拿历史三次正常压测结果的中位数打八折作为门槛。改完接口、调完 JVM 参数、动过内核参数,都先用同一份 wrk 命令压一遍,数字低于门槛直接拦住,别再往上线走。从那以后我每回动完服务端配置,都会强制走一遍这个压测脚本,把结果追加到历史文件里看趋势,翻车概率低了很多,希望帮到你。
本文还有配套的精品资源,点击获取