wrk-4.1.0 Linux服务器压测实战:编译、参数调优与Lua扩展
简介wrk 4.1.0版Linux性能测试资源包是一款面向Web服务的高性能HTTP基准测试工具适合后端开发、测试及运维人员对服务器进行高并发压测与瓶颈诊断。该工具采用C语言实现支持多线程、多连接并发请求并可利用Lua脚本定制复杂测试场景在短连接高并发场景中比JMeter和ab更显轻量高效。资源包为7z压缩格式体积约168.72MB内含源码及配套说明便于在Linux环境中编译部署。包内系统梳理了编译安装要点、常用参数如并发连接数、线程数、测试时长、典型压测命令、结果指标解读以及主流压测工具选型对比可帮助读者理解wrk运行机制并直接用于接口压测、容量评估与服务调优。当前已有586人学习浏览适合希望快速掌握轻量级压测工具的开发、测试与运维人员也适合作为团队性能评估的入门参考。1. wrk-4.1.0 在 Linux 上的定位服务器压测为什么首选它给一台刚交付的 Linux 服务器做性能摸底我先跑的工具不是 ab也不是 JMeter而是性能测试工具 wrk-4.1.0。wrk 用 C 写、基于 epoll 事件驱动单机就能在普通服务器上压出几十万级 QPS产物只有一个可执行文件配置成本比 JMeter 低一个数量级。它解决的核心问题是「这台服务器能扛多少并发、延迟长什么样」适合接口验收、容量评估和上线前后的回归对比。下面按 Linux 实际操作路径把编译、参数、Lua 扩展和常见坑过一遍给刚开始接触 wrk 的人一条能复现的路线。2. Linux 上编译 wrk-4.1.0从 git clone 到第一次压测2.1 为什么不用系统包管理器直接装版本与 Lua 支持的差别很多 Linux 发行版的仓库里要么没有 wrk要么版本停在 3.x 甚至更早。版本差异平时不显眼一旦要写 Lua 脚本就露馅老版本对 response 阶段和 done 阶段支持不完整个别发行版还把 LuaJIT 换成了纯 Lua 解释器压测时线程一多性能完全对不上号。wrk-4.1.0 是稳定线里功能最全的一个版本自带 LuaJIT支持自定义请求函数、逐请求响应处理和压测结束时的统计汇总这些能力不是每个打包版都会带上的。另一层原因更实际wrk 本身就是一个 C 项目依赖只有 gcc、make 和 openssl 开发头文件源码编译通常不到一分钟。与其跟包管理器较劲装个旧版不如直接拉官方仓库编一次拷到 /usr/local/bin 里之后所有机器都用同一个固定版本。下面的命令按 Ubuntu/Debian 写CentOS/RHEL 系把 apt 换成 yum install -y gcc make openssl-devel 就能对齐。2.2 源码编译全流程依赖、make 与可执行文件落地先装编译依赖三个包缺一不可sudo apt update sudo apt install -y build-essential libssl-dev gitbuild-essential 提供 gcc 和 makelibssl-dev 提供 HTTPS 压测需要的 openssl/ssl.h 头文件wrk 的 Makefile 默认开 SSL 支持缺它会在编译时报 fatal errorgit 用来拉源码。如果公司内网隔离不能直接 clone可以在一台能联网的机器上先把 wrk 源码 tar 打包传过去make 过程本身不依赖网络。拉取官方仓库并编译git clone https://github.com/wg/wrk.git cd wrk makemake 完成后当前目录下会出现一个名为 wrk 的可执行文件。wrk 的 Makefile 没有 install 目标所以习惯上手动拷贝到 PATH 里sudo cp wrk /usr/local/bin/ which wrk wrk --versionwrk --version 的输出里会带 4.1.0 和 epoll 字样。看到 epoll 说明编译时正确用上了内核的 I/O 多路复用机制而不是 select 兜底。这一步值得专门确认因为后面所有高并发表现都建立在这个基础上。如果输出里不是 4.1.0先检查 PATH 里是不是还有另一个旧版 wrk 在干扰常见位置是 /usr/bin/wrk。2.3 第一次压测就跑通最小命令与逐行输出解读编译完成后先拿本机服务试手。假设 8080 端口有个 HTTP 服务在跑wrk -t 4 -c 100 -d 10s http://127.0.0.1:8080/-t 4 表示开 4 个线程-c 100 表示同时保持 100 个并发连接-d 10s 表示持续压 10 秒。wrk 会把 100 个连接平分给 4 个线程每个线程独立跑一个 epoll 事件循环。第一次跑主要是验证工具本身没问题具体数值不重要。输出长这样Running 10s test http://127.0.0.1:8080/ 4 threads and 100 connections Thread Stats Avg Stdev Max /- Stdev Latency 45.30ms 22.10ms 120.00ms 70.12% Req/Sec 2.60k 350.00 3.20k 81.00% Latency Distribution 50% 38.00ms 75% 52.00ms 90% 68.00ms 99% 110.00ms 83331 requests in 10.00s, 78.42MB read Requests/sec: 8333.10 Transfer/sec: 7.84MB第一段 Thread Stats 的 Latency 行Avg 是所有请求的平均响应时间Stdev 是标准差/- Stdev 列表示落在平均值±标准差范围内的请求比例。Stdev 接近 Avg 一半以上说明延迟抖动明显不能只拿平均值下结论。第二段 Latency Distribution 是延迟分位数99% 这一行表示 99% 的请求在 110ms 内完成剩下 1% 的慢请求才是线上用户能感知到的尾巴。第三段 Requests/sec 是整个压测期间的总平均每秒请求数也就是平时说的 QPSTransfer/sec 是网络吞吐量。这里有个新手常犯的误读Latency Distribution 里的 99% 110ms 是延迟不是 QPSRequests/sec 才是 QPS。压测报告里如果只写「QPS 99% 是多少」基本是拿错了字段。另外-d 计时从连接建立开始算前几秒连接和数据都在热身所以正式压测建议至少 30 秒起步不然冷启动的波动会混进结果里。3. wrk 压测参数详解-t、-c、-d 怎么配才不翻车3.1 三个参数与并发模型线程、连接、时长的关系wrk 的并发模型跟 ab 不一样它不 fork 子进程而是起 t 个线程每个线程持有 c/t 个连接跑独立的非阻塞事件循环。所以 -t 不是越大越好线程数超过物理核数之后上下文切换成本反而吃掉一部分能力。常见做法是先看 nproc 拿到核数取物理核数或 2 倍核数作为 -t 的起点。参数配合上连接数要按被测服务的吞吐和延迟来估不能随手填。三者关系可以用下表归纳参数含义设置依据-t线程数物理核数或 2 倍物理核数-c并发连接数目标 QPS × 平均响应时间-d压测时长至少 30s包含热身期-c 不能整除 -t 时wrk 会尽量平均分配但线程间的连接数差异会导致负载不均最终输出里能看到某个线程的 Req/Sec 明显偏高、另一个偏低。所以一般把连接数设成线程数的整数倍。时长 -d 也常被忽略10 秒压测对 Java 应用来说可能 JIT 还没热完GC 抖动直接拉高延迟做上线前评估我一般至少跑 60 秒用长时间数据覆盖 GC 和连接池重建等周期性抖动。3.2 压测案例一个普通 Web 服务的参数取值过程假设要评估一个用户中心接口已知线上平均响应时间约 50ms希望它扛住 8000 QPS。先用经验公式粗估并发连接数并发连接数约等于目标 QPS 乘以平均响应时间也就是 8000 × 0.05 400 个连接。# 目标 QPS 8000平均响应时间 50ms并发连接数约 400 wrk -t 8 -c 400 -d 60s http://127.0.0.1:8080/api/user/info400 个连接分到 8 个线程每线程 50 个。这里要注意 keep-alive 的影响wrk 默认请求头里带 Connection: keep-alive所以 -c 对应的是「同时保持的连接数」不是「每秒新建连接数」。如果服务端没开 keep-alive响应里返回 Connection: close那实际压的就是每秒 TCP 建连能力TLS 场景下这个开销还会成倍放大连接数公式就不成立了。更稳妥的调参方式是从小往上加先 -c 50 跑一遍看延迟再把 -c 翻倍。如果延迟线性涨、QPS 几乎不动说明已到服务端并发上限如果延迟没变、QPS 跟着翻倍说明离瓶颈还很远。这样加两三轮就能在几分钟内摸到服务的真实承载边界而不是一次跑个超大参数把服务直接打挂。3.3 只看平均数是玄学延迟分位与每秒请求数的真实读法只看平均值是压测报告里最常见的坑。wrk 输出的 /- Stdev 列是落在平均值±标准差范围内的请求比例70% 这个值意味着三成请求的延迟明显偏离均值服务的真实体验并不稳定。Latency Distribution 要一行行看50% 和 99% 之间差多少倍是判断稳定性的直接指标。两者差 3 倍以上说明存在明显的慢请求拖尾这种尾巴通常来自 GC、锁竞争或者连接池排队。还有一层容易踩的坑是前后对比时参数不一致。前一次 -t 4 -c 100后一次 -t 8 -c 400QPS 涨了也分不清是服务变强了还是并发加满了。要对比就得锁死参数并且记录负载机配置、被压服务版本和网络环境缺一项数据都没法复现。压测结果是个范围不是个点对比时看趋势不要纠结单次几 percent 的浮动。两个容易混的指标再单独拎一下Requests/sec 是压测机视角的总吞吐只要压测机不成为瓶颈它可以近似看作服务端承载上限Transfer/sec 是网络吞吐如果这个值很高但 Requests/sec 偏低问题大概率出在响应体太大或带宽受限上。4. 用 Lua 脚本扩展 wrk-4.1.0POST 请求、鉴权头与参数化4.1 为什么必须用 Luawrk 默认没有 -P 参数和请求体文件wrk 的命令行参数只有 -t、-c、-d 和几个辅助项没有 -P 传 POST 参数、没有 -F 传请求体文件这是它刻意保持的简洁。要压不同类型的接口就得靠 --script 指定的 Lua 脚本扩展。wrk 内置 LuaJIT每个线程启动时各持一个独立的 Lua VM脚本在启动阶段执行一次之后每个请求触发一次 request 函数。这个模型决定了两个事实第一脚本里定义的变量只在线程内部共享跨线程的全局计数器、累计值都不能依赖它第二脚本顶层的变量赋值只执行一次适合放固定 header 和静态 body。常见误用是拿全局变量做总请求计数最后结果会比真实请求数小 t 倍。这类血泪案例不止一次出现写脚本时先记住线程边界。4.2 POST 压测脚本完整示例method、headers、body 的顺序-- post.lua: 对登录接口做 POST 压测 wrk.method POST wrk.headers[Content-Type] application/json wrk.headers[User-Agent] wrk-load-test wrk.body {username:admin,password:123456}这些赋值发生在脚本顶层wrk 启动时读取一次。wrk.headers 是个 table多次赋值用方括号加字符串键wrk.body 直接放 JSON 字符串不需要额外转义。然后运行wrk -t 4 -c 200 -d 60s --script post.lua http://192.168.1.50:8080/api/login脚本文件路径放在 --script 后面、URL 前面顺序不能反。如果脚本语法有错wrk 启动时会直接报 load error不会进入压测循环这一点比运行到一半再失败要好排错得多。很多教程把 wrk.method 写在 request 函数里新手照着抄就会出现每个请求临时改 header 的性能损耗。我的习惯是顶层赋值一次request 函数里只覆盖真正变化的字段比如动态 body。4.3 模拟真实用户动态参数、Token 注入与多阶段切换-- dynamic.lua: 每个请求携带不同的 username local users {user_a, user_b, user_c, user_d} local idx 0 function request() idx (idx % #users) 1 local body {username: .. users[idx] .. ,password:123456} wrk.body body return wrk.format() endrequest 函数每个请求都会被调用这里用取模循环让 4 个 username 轮换模拟不同账号并发而不是所有请求共用一个账号。wrk.format() 会按当前的 wrk.method、wrk.headers 和 wrk.body 拼出完整 HTTP 请求文本并返回。如果对性能有极致要求可以先拼好请求字符串缓存起来但动态参数场景下每次重新拼更合理。带鉴权的接口可以用 init 函数从命令行接收 token-- authed.lua: 从命令行参数读 token local token function init(args) token args[1] end function request() wrk.headers[Authorization] Bearer .. token return wrk.format() endTOKEN$(curl -s -X POST http://192.168.1.50/api/login -d {user:admin} | jq -r .token) wrk -t 4 -c 200 -d 60s --script authed.lua http://192.168.1.50/api/orders -- $TOKENinit(args) 是 wrk 保留的初始化函数不能改名字。-- 后面的内容会作为 args 数组传进 Lua 脚本这样 token 不写死在脚本文件里也不会把鉴权信息提交到代码仓库。多阶段压测同样可以用脚本控制脚本里记录启动时间用 os.time() 判断当前处于哪个阶段前 30 秒压登录接口、后 30 秒压业务接口。不过 os.time() 精度只到秒粗粒度切换够用要求精确的阶段划分还是拆成两个脚本各跑一次更稳。5. wrk-4.1.0 常见问题与排查编译、连接、负载机的 5 个坑wrk 本身不是黑匣子多数问题出在编译环境、连接参数和 Lua 脚本三个地方。下面 5 条是接口压测和容量评估里反复出现的真实案例按现象、原因、解决的顺序写照着自己排查基本都能对上号。5.1 编译报错找不到 openssl/ssl.h现象执行 make 时终端刷出一大片 fatal error: openssl/ssl.h: No such file or directory编译中断。原因wrk 的 Makefile 默认开启 SSL 支持编译时需要 libssl-dev 头文件而最小化安装的 Linux 往往没装这个开发包。装运行时库openssl不会自动带上头文件必须显式安装 -dev 版本。解决装依赖后重编。Ubuntu/Debian 执行 sudo apt install -y libssl-devCentOS 执行 yum install -y openssl-devel。装完不用清理缓存直接在 wrk 目录下重新 make 即可。5.2 Cannot assign requested address客户端端口耗尽现象-c 开到很大时wrk 报 Cannot assign requested address或者某个线程的输出里出现大量 Connection reset。原因客户端端口范围不够。Linux 默认 ip_local_port_range 是 32768-60999最多撑约 2.8 万个四元组短连接建立和关闭都很快时TIME_WAIT 状态占满整个端口池新连接就建不起来了。解决压测前放宽端口范围并确认复用配置sysctl -w net.ipv4.ip_local_port_range1024 65535 sysctl -w net.ipv4.tcp_tw_reuse1 sysctl -w net.ipv4.tcp_fin_timeout15更根治的方案是让被压服务支持 keep-alive。wrk 默认请求头带 Connection: keep-alive但如果服务端响应带回 Connection: close连接还是会用完即关客户端继续积累 TIME_WAIT。压测前用 curl -v 确认服务端的连接行为再决定是否要在连接复用上做文章。5.3 压测机 CPU 被打满QPS 结果失真现象被压服务 CPU 只有 30%wrk 所在机器的 CPU 已经满核QPS 数字却纹丝不动。原因负载机自身成了瓶颈。wrk 的线程忙在内核协议栈和事件循环里测出来的根本不是服务端能力。单机 wrk 能压多高有个天花板跟机器网卡、CPU 主频和内核配置都有关。解决先测一下本机的上限——用 wrk 去压一个只返回固定字符串的极简 HTTP 服务看能跑多少 QPS。如果正式压测结果与这个上限接近就该换更强的负载机、适当降低 -t 减少线程切换或者把被压服务迁到另一台机器上。内网压测时负载机和服务端最好用千兆以上网卡直连避开交换机限速这类隐藏变量。5.4 HTTPS 自签证书压测失败wrk 没有跳过校验的参数现象wrk 压 HTTPS 地址时握手阶段就失败报错指向证书校验服务端用的是自签证书或内网私有 CA。原因wrk-4.1.0 没有像 curl -k 那样提供跳过证书校验的开关。SSL 握手时证书校验失败直接断连不是参数写错的问题。解决内网压测最常见的做法是绕开 TLS——用 Nginx 做 HTTPS 终结把 443 转发到内网 80 端口wrk 直接压 HTTP 地址或者给被压服务临时配一张内网信任的证书把证书链装进 wrk 所在机器的 ca-certificates 目录。压 TLS 本身不是 wrk 的强项如果业务压测必须走加密链路建议换 Gatling 或 JMeter别在这里耗时间。5.5 Lua 脚本报 attempt to concatenate a nil value现象Lua 脚本执行时报 attempt to concatenate a nil value (field body)而且只在特定请求出现。原因request 函数里做字符串拼接时某个变量是 nil。常见于分支条件漏掉默认值或者 init 里没初始化变量就直接进了 request。wrk 的报错会指向 request 函数的拼接行往上游找第一个 nil 变量就是根因。解决拼接前做兜底赋值local payload wrk.body or local body {key: .. payload .. }如果变量来自命令行参数先在 init(args) 里检查 args[1] 是否为 nil 并给默认值。这类问题定位本身不难难在压测脚本里这种隐藏分支往往不会被短时间压测触发所以写脚本时就把默认值逻辑补全别留给线上压测去暴露。6. 压测数据可信度怎么验证三次取中位数与瓶颈定位技巧6.1 三次运行取中位数Socket errors 不为 0 直接作废压测数据不是跑一次就能当结论的。我现在的习惯是先跑三轮再决定信不信for i in 1 2 3; do wrk -t 4 -c 200 -d 30s http://127.0.0.1:8080/ | grep -E Requests/sec|Latency Distribution|Socket errors echo --- run $i --- done三次结果取中位数作为这次压测的代表值。Socket errors 只要不是 0这批数据就得标注或作废errors 来自 reset、timeout 和连接失败混入失败请求会压低延迟、虚增 QPS 可信度。就算三轮全跑完数据波动也很正常性能评估是个范围不是个点我一般用中位数 ±10% 作为报告的置信区间。每次压测都留原始输出和参数记录文件名按服务名加日期后续回溯性能问题时能省掉大量猜测。6.2 QPS 上不去时反推看延迟曲线、连接状态和错误日志QPS 上不去时反推顺序是固定的先看压测机 CPU 和网络是否先满了再看服务端 CPU、内存和 I/O最后看连接状态和服务端日志。wrk 输出里 Latency 随 -c 增加线性上涨、QPS 不动基本是服务端并发饱和Transfer/sec 长期贴着网卡上限但 QPS 上不去说明响应体太大、带宽成了瓶颈。用 ss -s 确认服务端连接队列里有没有大量 TIME_WAIT再翻 nginx error.log 里的 499/502能区分到底是连接层拒绝还是应用层超时。这套排查顺序我踩过不少次反向弯路先是盯着服务端日志看半天最后发现是压测机网卡先满了。从那之后我养成的习惯是压测前先在本机跑一遍天花板测试确认负载机余量足够才开始正式压测。希望这几条 wrk-4.1.0 经验能帮你在下次压测时少踩两个坑。本文还有配套的精品资源点击获取