ARTICLE DETAIL

资讯详情

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

wrk压测实战全解析:从解压到Lua脚本,掌握QPS与延迟分析

wrk压测实战全解析:从解压到Lua脚本,掌握QPS与延迟分析 简介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 事件驱动用少量线程挂起大量并发连接几秒钟就能打出一个接口的真实吞吐和延迟分布。它能回答的问题很直接这个服务上线后能扛多少 QPS99% 延迟从哪个并发点开始恶化。适合后端开发做上线前验证、性能测试做回归、运维定位容量瓶颈。这篇把解压、参数、脚本和常见翻车点一条线讲完。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里已经存在另一个版本的 wrkln -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 errors3.2 跑一条真实的压测命令假设本地已经起了一个 nginx监听 8080我们压它的/index.htmlwrk -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 核机器用-t48 核机器用-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.methodpath 用命令行 URLheaders 用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。最常见的动态参数是自增 IDlocal 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 个线程会同时产生userId1。如果被测接口对 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) endsetup(thread)会在每个线程启动时被调用一次thread:get(idx)读取共享的 idxthread: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}) endsetup在每个线程启动时执行一次所以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))) endresponse(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_reusesysctl -w net.ipv4.ip_local_port_range1024 65535 sysctl -w net.ipv4.tcp_tw_reuse1注意这是压测机器的配置不要顺手改到线上业务机。调完后再跑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 donetee把输出同时打到终端和文件三次结果在三个文件里对比Requests/sec那行就能看出波动范围。6.3 压测机与被测机分开并先看一眼压测机我吃过一次亏把 wrk 放在被测服务器上压QPS 压到了瓶颈结果 CPU 曲线里分不清哪部分是服务消耗、哪部分是 wrk 消耗最后只能换机器重测白白浪费一晚。现在我的习惯是压测前先top看压测机空不空再确认两台机器之间带宽足够最后才动手。压测机不够干净结果就是垃圾进垃圾出。wrk 压测的真功夫不在跑命令而在让结果能复现、能对比。把上面三个习惯固化成流程后我基本不再为“这个数字可不可信”发愁。希望帮到你。本文还有配套的精品资源点击获取
返回列表