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/
小提示: macOS 用户可以直接 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 结果解读与瓶颈分析

结果出来了,怎么看?我一般按三步走:

  1. 看吞吐量(Requests/sec)——这是第一印象,但别急着下结论。
  2. 看延迟分布(Latency Avg + Stdev)——如果 Avg 低但 Stdev 高,说明系统不稳定。
  3. 看错误数(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 实战的核心流程,你可以对照着梳理自己的压测步骤:

wrk 实战核心流程 ① 安装与编译 ② 基础用法(参数调优) ③ Lua 脚本编写 ④ 结果解读 ⑤ 瓶颈分析与优化 关键参数 -t 线程数 -c 并发连接数 -d 持续时间 核心指标 Requests/sec Latency Avg/Stdev Socket errors Lua 钩子 setup() / init() request() / response() done()

最后说一个我自己的习惯:压测结果一定要保存原始输出,别只看截图。因为很多时候你回头分析时,需要对比不同参数下的延迟分布,原始数据比截图有用得多。