ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

wrk压测工具实战:线程模型、Lua脚本与避坑指南

wrk压测工具实战:线程模型、Lua脚本与避坑指南 简介wrk.tar.gz 内含已编译完成的 wrk 高性能 HTTP 压测工具解压即可在终端运行无需额外编译步骤。wrk 由 Steve Leach 创建凭借 LuaJIT 脚本支持在性能压测领域颇具口碑。它基于事件驱动与多线程模型单机即可模拟较大并发连接特别适合 Web 服务、API 接口、负载均衡器的性能测试与瓶颈定位。资源包整体约 214MB核心交付物为可直接执行的 wrk 程序同时包含一份工具详解说明系统梳理了常用参数连接数、持续时长、线程数、固定速率、自定义脚本的用法并给出 LuaJIT 脚本示例支持自定义请求模式、响应状态校验、延迟控制等复杂测试逻辑。通过脚本可动态生成请求 URL、根据响应状态决定是否中断测试使压测更贴近真实业务。此外文档还对每秒请求数、平均传输速率、延迟百分位等关键指标做了说明帮助读者正确解读压测结果。已有 258 人浏览学习若你正在搭建压测环境或优化服务吞吐能力这份即开即用的工具包可免去从源码编译的麻烦配合脚本扩展能够快速满足并发摸底、回归对比和容量评估等场景需求。1. wrk 压测工具编译好的包解压即用才是真的省事wrk 压测工具在从业者手里一直有点玄学同样的机器用 ab 压只能拉到几百 QPS换 wrk 能轻松翻十几倍但也有不少人卡在第一步——编译。openssl 头文件版本对不上、gcc 缺依赖、交叉编译环境没配好一个 tar.gz 折腾半天。这份资源是已经编译完的 wrk 二进制包解压即用省掉的正是最劝退的环境环节。适合三类人临时要压接口的运维、做网关性能对比的后端以及刚接触高并发压测、还没准备好折腾编译环境的新手。下面我会把 wrk 的线程模型、参数设置、Lua 脚本和常见坑逐个讲清楚保证你照着一路跑完不翻车。2. wrk 的高并发原理线程模型、连接复用与适用边界2.1 每个线程独立事件循环wrk 的并发模型不是线程越多越好wrk 本质上是单进程多线程的程序每个线程各自持有一个 epoll 事件循环线程之间不共享连接request 计数也是独立维护的。启动时-t指定线程数整个进程会创建这么多线程-c指定总连接数wrk 会把连接均匀分摊到各线程。所以-t4 -c200的含义就是 4 个线程总共 200 条连接每个线程维护 50 条。我一般习惯把-c设成-t的整数倍这样每个线程的连接数量可控epoll 调度的开销最小。如果你把-t直接拉高到几十反而会因为线程切换和锁竞争把压测机自己的 CPU 烧光结果里 QPS 上不去Latency 的 Stdev 却涨得离谱。wrk 的模型是连接持续复用请求走完一个循环后连接不关闭直接排队等下一个请求。这也是它和 ab 差距最大的地方。wrk 编译时通常要链接 OpenSSL 和 LuaJITLuaJIT 是后续跑 lua 压测脚本用的。如果只在 Linux 上跑动态链接版最常见的问题是 glibc 版本不一致这属于运行时环境问题下一章我会讲怎么在解压时顺手排查。这里先记住一个结论wrk 的线程数不是越多越好压测机 CPU 核数的两倍已经是很激进的设定了保守一点 4 到 8 线程足够覆盖绝大多数单机压测场景。2.2 keep-alive 连接复用wrk 高 QPS 的核心来源wrk 默认走 HTTP/1.1 keep-alive连接建好之后不会被Connection: close关闭一批请求全部复用同一条 TCP 连接。这意味着 DNS 解析、TCP 三次握手、TLS 握手这些一次性成本被摊薄到成千上万个请求里单请求开销只剩「发请求 等响应 解析」这三段。对比 ab 的阻塞式模型ab 每个连接在请求期间占住一个进程或线程连接处理完就关下一个请求再重建。在并发数不高的场景下两者差距不明显一旦并发到几百上千ab 的进程调度和连接重建开销直接拖垮压测机自身。这也是为什么同一个接口ab 可能只能压出 5000 QPSwrk 能压到 8 万 QPS 的原因。但注意这个模型的代价是wrk 测出来的 QPS 是「长连接稳态」下的数字不代表「每次新建连接」场景的真实表现。如果你的业务是短连接风格——比如每请求独立建连、服务端还禁用了 keep-alive——wrk 的数字会偏高因为它把握手成本从真实链路里抠掉了。遇到这种场景我一般直接用 ab 压或者干脆在 wrk 脚本里给请求加Connection: close头强制关闭连接复用。2.3 场景边界wrk 不适合什么wrk 不是万能的有三个场景我明确不建议硬上。第一个是 HTTP/2。wrk 官方只支持 HTTP/1.1压 HTTP/2 接口应该用 h2load它才是基于 nghttp2 的实现。wrk 对 HTTP/2 请求会直接退化成普通 HTTP/1.1测出来的延迟和吞吐都没有参考价值。第二个是分布式压测。wrk 是单机工具单机网卡和 CPU 是有上限的。压测机上软中断占比超过 40% 时加连接数已经没意义了这时候要么做压测机调优要么把压力拆分到多台机器上同步发。wrk 没有内置的分布式协调机制需要自己写脚本拉多台机器同步启动。第三个是精细限速。wrk 没有内置的 QoS 控制虽然部分新版本支持 rate 参数但老版本是不认的。如果需要精确控制 QPS 上限做容量摸底可以考虑 wrk2它专门支持固定速率模式。判断标准很简单要压「服务端到底能扛多少」用 wrk要压「在指定流量下服务端表现如何」用 wrk2。维度wrkabh2load并发模型epoll 事件驱动阻塞式多进程多线程 nghttp2连接管理默认 keep-alive 复用默认短连接支持多路复用HTTP/2不支持不支持原生支持Lua 动态脚本内置 LuaJIT无无适用场景长连接高并发短连接探测HTTP/2 网关3. 首次压测从解压到读懂一份报告3.1 tar tzf 先看包结构ldd 确认依赖再解压拿到 wrk.tar.gz我不会直接解压。先看包结构tar tzf wrk.tar.gz这一步能确认打包时放的是单纯二进制还是带源码目录免得解压到当前目录把文件撒得到处都是。看到包里是一个wrk可执行文件时我一般会建一个专门的目录再解压mkdir -p ~/tools/wrk tar xzf wrk.tar.gz -C ~/tools/wrk cd ~/tools/wrk ./wrk -v./wrk -v能正常输出版本号说明二进制和当前系统的 glibc、openssl 动态库能对上。我遇到过不少次./wrk直接报version GLIBC_2.29 not found原因是编译机器是旧发行版运行机器是新发行版动态链接库版本对不上。遇到这种情况先跑一条命令ldd ./wrk | head -20如果输出里libssl.so或libc.so后面跟着not found说明打包时没有带上运行库。这种二进制没法跨发行版裸跑优先找同发行版的编译包或者拿源码在目标机器上重新 make。动态链接的二进制逃不掉这个限制看 ldd 再动手能省十分钟排查时间。解压后我习惯把 wrk 放到 PATH 里ln -s ~/tools/wrk/wrk /usr/local/bin/wrk wrk -v这样后续跑压测不用每次都写全路径。3.2 第一次压测一条命令和报告里的每个数字wrk -t4 -c200 -d30s --latency http://127.0.0.1:8080/api/health参数拆解-t4是 4 个线程-c200是 200 个并发连接-d30s压 30 秒--latency在结束时打印完整的延迟分布。压测目标是本机回环地址域名解析都不需要最适合验证工具本身。报告输出长这样数字按你的机器会不同Thread Stats Avg Stdev Max /- Stdev Latency 8.42ms 3.17ms 99.24ms 86.14% Req/Sec 5.92k 1.01k 10.35k 73.50% Latency Distribution (HdrHistogram - Recorded Latency) 50.000% 7.83ms 75.000% 9.71ms 90.000% 12.55ms 99.000% 22.31ms 99.900% 45.09ms 99.990% 78.22ms 342176 requests in 30.00s, 57.84MB read Requests/sec: 11406.15 Transfer/sec: 1.93MB解读顺序先看最后一行Requests/sec这是全程平均吞吐也是最常写进报告的数字。再看Latency的 Avg 和 StdevStdev 超过 Avg 的 30% 说明延迟抖动大后面要找原因。最后对比 50% 和 99% 分位差50% 是 7.83ms99% 是 22.31ms差值不大说明服务端处理稳定如果 99.9% 突然飙到几百毫秒多半是触发了 GC 或后端排队。顶部 Thread Stats 里的Req/Sec是各线程每秒请求数的统计它跟最后的Requests/sec不是一回事前者是单线程的聚合分布后者是总吞吐均值。Latency Distribution来自 wrk 内嵌的 HdrHistogram录的是全样本延迟比只看 Avg 靠谱得多。这里有个细节50% 分位数是 7.83ms但 Avg 是 8.42ms两者接近说明延迟集中在均值附近没有明显的长尾。3.3 参数怎么配线程、连接、时长和超时之间的关系参数作用我的建议-t压测线程数从 2~4 起步不要超过压测机 CPU 核数的 2 倍-c总并发连接数先按 -t 的 50 倍试比如 -t4 配 200-d压测时长至少 30s30s 以下样本太少分位数不稳-T单请求超时时间默认 10s压慢接口时按业务调整-H自定义请求头支持重复传模拟 Host、User-Agent--latency打印延迟分布正式报告必加连接数不是越大越好。我见过有人直接-c10000压网关结果压测机先死了文件描述符打满、软中断把 CPU 吃光最后报告 QPS 比-c500还低。正确做法是阶梯压测-c从 100、200、500、1000 各跑一遍 30s记录 QPS 和 99% 延迟看曲线拐点。吞吐随连接数上涨后回落回落的那个点就是服务端的处理上限。-T参数很多人忽略它只影响单个请求的等待时间超时的请求会计入 Socket errors 的 timeout 分类并不会把整个压测停掉。压长尾接口时-T设太短会把慢请求全部变成 timeoutQPS 数字虚低设太长又会让压测时间被慢请求拖长。我一般按「业务方承诺的最慢响应时间」来设比如承诺 99% 在 500ms 内-T就设 2s留出余量。4. 压 POST 和业务脚本用 Lua 扩展出更接近生产的流量4.1 改用 POST JSONwrk 自带的 Lua 扩展点wrk 内置 LuaJIT这是它比 ab 强的一个重要原因。脚本不需要写完整生命周期只定义几个钩子函数。最简单的是直接覆盖全局变量wrk.method POST wrk.headers[Content-Type] application/json wrk.headers[Accept] application/json wrk.body {phone:13800138000,sms_code:1234}执行时用-s指到脚本文件wrk -t4 -c100 -d30s -s post.lua http://127.0.0.1:8080/api/sms/loginwrk.method是全局字符串wrk.headers是一个 tablewrk.body是字符串三个都作用于当前线程的后续请求。这样每个请求都会带同样的 JSON body。此脚本适合固定参数接口比如短信登录、固定 token 查询这类 body 不变的业务。这里有个容易犯的错误只要脚本里定义了request()函数每个请求都以函数返回值作为请求内容wrk.method这类全局变量就不再生效。两种写法不要混着用排查脚本问题时第一件事就是确认到底是「变量绑定」还是「函数生成」模式。4.2 动态参数与登录态request() 和 done 回调的完整用法真实业务压测里每个请求都用同一个手机号服务端可能直接走缓存。要用动态参数必须走request()函数function request() local id math.random(100000, 999999) local path string.format(/api/items/%d, id) return wrk.format(GET, path) endwrk.format是内置函数接收 method、path、headers、body 四个参数返回一个格式化好的请求。每次调用request()wrk 都会把这个返回值发出去。上面这段模拟的是按随机 id 查商品的场景连续打 30s 后服务端会命中不同的数据行更接近真实流量。注意一个细节wrk 每个线程有独立的 Lua 状态如果你用计数器counter counter 1来生成路径4 个线程会各自从 1 开始自增路径大量重复。用随机数或时间戳代替计数器是更稳妥的做法。这个坑我踩过不止一次压出来的 QPS 虚高因为服务端一直在命中同一个数据行。更复杂的登录态场景一般是先请求一次 login把返回的 token 捞出来后续请求加到 header 里local token function request() local headers {} if token ~ then headers[Authorization] Bearer .. token end return wrk.format(GET, /api/user/profile, headers) end function response(status, headers, body) if status 200 and headers[Set-Cookie] then token string.match(headers[Set-Cookie], token([^;])) end endresponse()回调里能拿到本次响应的 status、headers 和 body。注意 wrk 在没有定义response回调时会丢弃响应体一旦定义了回调body 就会保留给 Lua 读取代价是内存占用变大。高并发压测时如果响应体很大建议只提取 header不要在回调里频繁拼接 body 字符串。done回调负责汇总结果done function(summary, latency, requests) io.write(Total requests: .. summary.requests .. \n) io.write(QPS: .. math.floor(summary.requests / summary.duration * 1000000) .. \n) io.write(50th percentile: .. latency:percentile(50) .. ms\n) io.write(99th percentile: .. latency:percentile(99) .. ms\n) endsummary.duration的单位是微秒所以要乘以 1000000 换算成秒。latency:percentile(p)返回 HdrHistogram 里指定百分位的延迟值。这套「request 生成 response 提取 done 汇总」的组合是压测脚本里最常用的闭环。4.3 脚本调试小剂量验证语法再上量脚本写完后我从不直接拿 30s 任务跑先小剂量验证wrk -t1 -c1 -d2s -s post.lua http://127.0.0.1:8080/api/sms/login-t1 -c1表示单线程单连接2 秒钟就能跑完。这一步能验证三件事脚本语法有没有错、请求格式服务端认不认、响应状态码是不是预期值。如果服务端返回 400先看 body 里的 JSON 格式和 Content-Type别急着加大压力。wrk 的 Lua 报错信息会直接打到标准错误流比如attempt to index a nil value定位到具体行号。最常见的调试手法是在脚本里插io.write()打印变量值——要注意 wrk 脚本并发执行时打印的日志可能交错最好只在done回调里做汇总输出别在request()里打印每次请求的路径。5. 压测避坑指南五个最容易翻车的地方5.1 连接数开太高QPS 反而掉下来现象-c从 200 提到 2000QPS 不升反降Latency 的 Stdev 明显变大。原因连接数超过服务端 accept 队列或线程池处理能力后连接排队时间变长。Nginx 的worker_connections默认只有 1024连接暴增时 close 风暴触发内核协议栈开销全被放大。解决固定-t4-c按 100、200、500、1000 阶梯跑记录每个档位的 QPS找拐点。服务端多 worker 时先确认worker_connections配置别让单机压测直接压垮内核 socket 表。5.2 压测机先成了瓶颈CPU 飙爆延迟全部失真现象压测机 CPU 100%top 里软中断占了一大截报告里的 QPS 飘忽不定同一参数两次结果差 20%。原因wrk 的线程把网卡中断和 epoll 唤醒都压在一个物理 CPU 上网络包量太大时网络栈处理和应用层抢 CPU。解决先看压测机 CPU 是不是跑满。跑满就走多机分布式压别指望单机无限加连接。另外把-t调小线程数和 CPU 核数对齐减少上下文切换。网卡多队列中断绑定到不同核心是运维侧的事压测时先确认压测机硬件够用。5.3 keep-alive 被中间层回收Socket errors 里 read 上涨现象压测中途 Socket errors 里 read 或 write 数字突然上涨QPS 出现毛刺。原因wrk 每线程维护的连接数大于实际活跃使用量时空闲连接会被中间层按 keepalive timeout 回收。四层负载均衡的 idle timeout 经常是 60s 或 90s压测时长跨过这个值时会看到连接大量重建。解决-c总连接数按「线程数 × 单线程实际并发深度」配置不要无脑开大。压长连接服务时把-c与目标服务的 keepalive 上限对齐测试环境允许时直接把服务端keepalive_timeout调大。这个问题的本质是连接复用的生命周期跟 wrk 本身无关别在工具上浪费时间。5.4 Lua 脚本里的中文和转义请求体乱码现象wrk.body里写了一段中文 JSON服务端收到后乱码或者直接 HTTP 400。原因脚本文件不是 UTF-8 编码或者 Lua 解析字符串时把转义符号吃了。解决编辑器把 .lua 存成 UTF-8 无 BOMbody 里的中文尽量用\uXXXX转义不要直接贴中文。JSON 里的双引号用 Lua 字符串时要么单引号包整个 body要么内部用\转义。我通常写成wrk.body {name:\\u5f20\\u4e09}服务端反序列化 100% 不出错。新手踩这个坑最多查半天才发现是编码问题。5.5 CtrlC 中断压测报告里的分位数别当真现象压-d30s的任务跑到 10s 就 CtrlC 停了报告显示 Latency 很漂亮QPS 也高。原因wrk 的统计基于已经完成请求的样本。前 10s 里连接还在建立、缓存还在预热这阶段延迟偏高中断后剩下的样本集中在已完成快速请求上分位数自然偏好看。解决正式压测前先跑一个 5s 短任务预热看结果但别当正式数据只为把连接池和缓存热度建起来。再跑正式 30s 任务。CtrlC 只适合快速验证脚本语法不适合作为最终报告数据来源。需要看全量分布时一定等任务自然跑完然后看报告里 99% 和 99.9% 两行。6. 压测结果的可信度校验三个关键信号与一套对照动作报告到手先不急着写结论。这份编译好的 wrk.tar.gz 解压就能跑但跑出来的数字不一定可信。我一般先看三个信号Socket errors 是否归零、Latency 的 Stdev 与 Avg 的比值、两次同参数压测的 QPS 偏差。Socket errors 有四类connect、read、write、timeout。connect 失败指向目标端口不可达或 backlog 溢出read/write 失败多半是服务端主动断开连接和 keep-alive 超时有关timeout 是请求超时未响应和-T参数直接相关。errors 里任何一类不是 0报告数字都只能做参考不能当结论。Latency 的 Stdev 超过 Avg 的 30% 时我认定系统延迟不稳定不会直接拿 Avg 去跟别人比。这时加一个对照动作-c减半再跑一遍如果 Stdev 明显回落说明是高并发排队导致的抖动如果 Stdev 依旧很大往 GC、锁竞争、慢 SQL 方向查别在压测工具上浪费时间。最后是双跑对照同一个命令原样执行两遍QPS 差超过 5% 就查环境干扰比如压测机上的定时任务、目标服务在压测期间被别的请求污染。把 done 回调里输出的关键数字追加到文件对比起来最方便done function(summary, latency, requests) local f io.open(/tmp/wrk_result.log, a) f:write(math.floor(summary.requests / summary.duration * 1000000) .. .. latency:percentile(99) .. \n) f:close() end从那以后我每次压测完都强制自己先跑一遍同参数对照再回去翻 Socket errors 和 Stdev 这三行字——不看这三样就写结论多半是被机器的平均延迟骗了。希望帮到你。本文还有配套的精品资源点击获取
返回列表