3. wrk 工具实战:安装、脚本编写与瓶颈分析
wrk 这个工具,说实话是我压测工具箱里用得最顺手的一个。它轻量、单机就能压出很高的并发,而且对 Lua 脚本的支持让它能处理不少定制化场景。这一章咱们就把它掰开揉碎,从安装到写脚本,再到看结果找瓶颈,一条龙走通。
3.1 wrk 安装与基础用法
安装 wrk 其实没什么门槛。我习惯在 Linux 上直接用源码编译,干净利落。
# 克隆源码
git clone https://github.com/wg/wrk.git
cd wrk
# 编译(依赖 gcc、make、openssl-dev)
make
# 把二进制扔到系统路径
sudo cp wrk /usr/local/bin/
brew install wrk。Windows 的话,我建议用 WSL2,原生体验最好。
装好之后,跑一个最简单的命令试试:
wrk -t12 -c400 -d30s http://127.0.0.1:8080/api/hello
这个命令的意思是:用 12 个线程、保持 400 个并发连接、持续压 30 秒。输出结果大概长这样:
Running 30s test @ http://127.0.0.1:8080/api/hello
12 threads and 400 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 45.62ms 12.34ms 198.00ms 75.23%
Req/Sec 723.45 89.21 1.02k 68.90%
259734 requests in 30.02s, 38.47MB read
Socket errors: connect 0, read 0, write 0, timeout 0
Requests/sec: 8651.23
Transfer/sec: 1.28MB
嗯,这里要注意几个关键字段:Latency 是延迟分布,Req/Sec 是每个线程的吞吐,最下面 Requests/sec 就是总吞吐。我在项目中遇到过不少团队只看最后那个数字,其实延迟的 Stddev(标准差)更能反映系统稳定性。
3.2 wrk 脚本编写(Lua 扩展)
wrk 默认只支持简单的 GET 请求。但实际业务哪有这么简单?登录要带 token,下单要传 JSON,这些都得靠 Lua 脚本搞定。
wrk 的 Lua 脚本有 3 个核心钩子:
| 钩子函数 | 触发时机 | 典型用途 |
|---|---|---|
setup() |
线程初始化时 | 读取全局配置、初始化数据 |
init() |
每个请求前 | 生成动态参数、设置请求头 |
response() |
收到响应后 | 校验状态码、提取数据 |
我写一个最常见的场景——带 Token 的 POST 请求脚本:
-- token.lua
wrk.method = "POST"
wrk.body = '{"username":"test","password":"123456"}'
wrk.headers["Content-Type"] = "application/json"
-- 从响应里提取 token
response = function(status, headers, body)
if status == 200 then
local json = require("cjson")
local data = json.decode(body)
-- 把 token 存到全局变量,供后续请求使用
wrk.headers["Authorization"] = "Bearer " .. data.token
end
end
然后这样调用:
wrk -t4 -c100 -d60s -s token.lua http://api.example.com/login
response() 里做复杂的 JSON 解析,结果压测时 CPU 全耗在 Lua 解释器上了,反而压不上去。建议只在 init() 里做一次解析,或者用 wrk.format() 预构造请求。
再给一个更实用的例子——从文件读取参数做参数化:
-- param.lua
local counter = 0
local params = {}
-- 从文件加载参数
function setup(thread)
local file = io.open("user_ids.txt", "r")
for line in file:lines() do
table.insert(params, line)
end
file:close()
end
function init()
counter = counter + 1
local uid = params[counter % #params + 1]
wrk.path = "/api/user/" .. uid
end
这样每个请求都会轮询不同的 user_id,模拟真实用户行为。说白了,Lua 脚本就是让 wrk 从「玩具」变成「武器」的关键。
3.3 wrk 结果解读与瓶颈分析
结果出来了,怎么看?我一般按三步走:
- 看吞吐量(Requests/sec)——这是第一印象,但别急着下结论。
- 看延迟分布(Latency Avg + Stdev)——如果 Avg 低但 Stdev 高,说明系统不稳定。
- 看错误数(Socket errors / Non-2xx)——有错误就说明系统扛不住了。
举个例子,假设压测结果是这样:
Latency 120.00ms 85.00ms 1.20s 60.00%
Req/Sec 450.00 120.00 800.00 55.00%
Requests/sec: 5400.00
Socket errors: connect 0, read 23, write 0, timeout 45
我会这么分析:
- 吞吐 5400 看起来还行,但延迟标准差 85ms 比平均值 120ms 还大,说明响应时间波动剧烈。
- 有 read 错误和 timeout,大概率是后端连接池满了或者数据库慢查询。
- Max 延迟到了 1.2s,这已经超出大部分接口的 SLA 了。
这时候我会建议:先看慢查询日志,再看连接池配置,最后检查是否有锁竞争。我在项目中遇到过类似情况,最后发现是 Redis 连接池太小,导致请求排队。
下面这张图总结了 wrk 实战的核心流程,你可以对照着梳理自己的压测步骤:
最后说一个我自己的习惯:压测结果一定要保存原始输出,别只看截图。因为很多时候你回头分析时,需要对比不同参数下的延迟分布,原始数据比截图有用得多。