4、wrk实战:安装wrk、编写Lua脚本模拟用户行为、压测单接口与多接口、结果解读

wrk 这工具,说白了就是个压测界的「瑞士军刀」。它不像 JMeter 那么重,也不像 ab 那么简陋。我最早接触它是在一个网关项目里,当时需要模拟上千个并发用户,wrk 用单机就跑出了 10 万 QPS,CPU 占用还不到 80%。嗯,从那以后,它就成了我压测工具箱里的常驻选手。

4.1 安装 wrk:三分钟上手

安装没什么门槛。我习惯用源码编译,因为能拿到最新版本。不过如果你赶时间,包管理器也能搞定。

# macOS
brew install wrk

# Ubuntu / Debian
sudo apt-get install build-essential libssl-dev git -y
git clone https://github.com/wg/wrk.git
cd wrk
make
sudo cp wrk /usr/local/bin/

# CentOS / RHEL
sudo yum groupinstall 'Development Tools'
sudo yum install openssl-devel
git clone https://github.com/wg/wrk.git
cd wrk
make
sudo cp wrk /usr/local/bin/
验证安装: 终端输入 wrk --version,看到版本号就说明成了。我见过有人编译完忘了 cp,结果一直报「command not found」——记得把二进制放到 PATH 里。

4.2 核心参数:别被选项吓到

wrk 的参数不多,但每个都很关键。我列个表,你对照着看就行。

参数 含义 示例 我的建议
-t 线程数 -t 4 通常设为 CPU 核心数的 2-4 倍
-c 并发连接数 -c 100 从 10 开始逐步往上加,别一上来就 1000
-d 压测时长 -d 30s 至少 30 秒,太短的数据没参考价值
-s Lua 脚本路径 -s script.lua 模拟复杂行为时必用
--latency 输出延迟分布 --latency 强烈建议加上,能看到 P50/P99
注意: 线程数 -t 不是越大越好。我曾经在一台 4 核机器上设了 16 个线程,结果 wrk 自己先抢 CPU 抢到崩溃,压测数据全废了。一般 -t 不超过 CPU 核心数的 4 倍。

4.3 编写 Lua 脚本:模拟真实用户行为

wrk 自带的压测只能发 GET 请求,太死板了。真实用户会登录、搜索、下单,这些都需要 Lua 脚本来模拟。我个人习惯把脚本分成三块:初始化、请求构造、响应处理。

4.3.1 基础结构

-- 初始化阶段,只执行一次
function setup(thread)
    thread:set("id", 1)
end

-- 每个请求前调用,构造请求参数
function init(args)
    -- 这里可以解析命令行传入的参数
end

-- 核心函数:每次请求都会调用
function request()
    local headers = {}
    headers["Content-Type"] = "application/json"
    headers["Authorization"] = "Bearer " .. token
    
    local body = '{"user_id": ' .. math.random(1, 10000) .. '}'
    
    return wrk.format("POST", "/api/order", headers, body)
end

-- 每次响应后调用,用于统计或校验
function response(status, headers, body)
    if status ~= 200 then
        -- 记录异常,但不中断压测
        print("非 200 响应: " .. status)
    end
end

关键点: request() 函数必须返回一个完整的 HTTP 请求。你可以在这里动态生成 header、body、URL,甚至从文件里读取参数列表。我做过一个压测,需要模拟 1000 个不同用户的登录态,就是在 init() 里预加载了一个 token 池,然后 request() 里轮询使用。

4.3.2 模拟多接口流程

真实场景下,用户不会只点一个接口。比如电商下单流程:登录 → 搜索商品 → 加入购物车 → 下单。我写过一个脚本,用全局变量控制状态流转。

local state = 0
local token = ""
local product_id = ""

function request()
    if state == 0 then
        -- 登录
        local body = '{"username":"test","password":"123456"}'
        state = 1
        return wrk.format("POST", "/api/login", {}, body)
    elseif state == 1 then
        -- 搜索商品
        state = 2
        return wrk.format("GET", "/api/search?q=手机", {["Authorization"] = "Bearer " .. token}, nil)
    elseif state == 2 then
        -- 加入购物车
        state = 3
        local body = '{"product_id":"' .. product_id .. '","quantity":1}'
        return wrk.format("POST", "/api/cart", {["Authorization"] = "Bearer " .. token}, body)
    else
        -- 下单
        state = 0  -- 重置,模拟下一个用户
        return wrk.format("POST", "/api/order", {["Authorization"] = "Bearer " .. token}, '{"cart_id":"xxx"}')
    end
end

function response(status, headers, body)
    if status == 200 then
        if state == 1 then
            -- 从登录响应中提取 token
            local ok, json = pcall(cjson.decode, body)
            if ok then token = json.token end
        elseif state == 2 then
            local ok, json = pcall(cjson.decode, body)
            if ok then product_id = json.products[1].id end
        end
    end
end
避坑指南: 我曾经在 response() 里写了一个耗时的 JSON 解析,结果压测 QPS 直接掉了 30%。记住,response() 里不要做重操作,解析逻辑能精简就精简。如果非要解析,用 pcall 包起来,防止异常导致线程崩溃。

4.4 压测单接口:快速验证

单接口压测是最基础的场景。比如我想看看登录接口能扛多少并发。

wrk -t 4 -c 100 -d 30s --latency -s login.lua http://api.example.com

这个命令的意思是:4 个线程,100 个并发连接,持续 30 秒,用 login.lua 脚本,输出延迟分布。跑完之后你会看到类似这样的结果:

Running 30s test @ http://api.example.com
  4 threads and 100 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    45.23ms   12.34ms 345.67ms   75.89%
    Req/Sec   552.34     89.12   789.00     68.45%
  Latency Distribution
     50%   42.10ms
     75%   51.20ms
     90%   62.30ms
     99%   98.50ms
  66280 requests in 30.00s, 8.52GB read
  Requests/sec:   2209.33
  Transfer/sec:  290.67MB

4.5 压测多接口:模拟真实链路

多接口压测才是重头戏。我习惯把多个接口的脚本写在一个文件里,用状态机控制流程。上面那个电商流程的脚本,跑起来是这样的:

wrk -t 8 -c 200 -d 60s --latency -s e2e_flow.lua http://api.example.com

这里线程数提到了 8,并发 200,时长 60 秒。为什么调高?因为多接口流程里每个请求的耗时不一样,需要更大的并发池才能压满后端。我见过有人用单接口的参数去压多接口,结果后端 CPU 才用到 30%,压测就结束了——浪费。

经验之谈: 多接口压测时,重点关注 P99 延迟 和 错误率。单接口的 P99 可能只有 50ms,但多接口链路里,某个慢接口会把整体 P99 拖到 200ms 以上。我曾经在一个支付链路里发现,下单接口本身很快,但库存扣减接口偶尔会慢 500ms,导致整个下单流程的 P99 飙升。定位到问题后,把库存扣减改成了异步,P99 直接降了 60%。

4.6 结果解读:别只看 QPS

很多人拿到结果只看 Requests/sec,觉得数字大就是好。其实不然。我列几个关键指标,你对照着看。

指标 含义 健康范围 异常信号
Avg Latency 平均延迟 < 100ms > 500ms 说明系统有瓶颈
P99 Latency 99% 请求的延迟上限 < 200ms > 1s 说明存在严重抖动
Req/Sec 每秒请求数(QPS) 视业务而定 突然下降说明系统过载
+/- Stdev 延迟标准差 < 30% > 50% 说明延迟波动大
Transfer/sec 吞吐量 与 QPS 匹配 QPS 高但 Transfer 低,说明响应体小

举个例子。上面那个单接口结果里,Avg Latency 45ms,P99 98.5ms,说明系统很稳。但如果 P99 是 345ms,Avg 只有 45ms,那就说明有少量请求被「卡住」了——可能是 GC 停顿,也可能是某个后端节点超时。我遇到过一次,P99 飙到 2s,查了半天发现是数据库连接池满了,请求在排队。

注意: 压测结果里的 Socket errors 字段很容易被忽略。如果出现 connect 或 read 错误,说明客户端或网络层已经扛不住了,这时候的 QPS 数据不可信。先排查网络和 wrk 所在机器的资源,再怀疑后端。

4.7 本章知识体系

下面这张图总结了 wrk 实战的核心脉络,从安装到结果分析,一条线串下来。

wrk 实战知识体系 安装 wrk 核心参数 Lua 脚本 单接口压测 多接口压测 结果解读 源码编译 / 包管理器 -t / -c / -d / -s / --latency init / request / response 快速验证 / 基准测试 状态机 / 流程模拟 QPS / P99 / 错误率 核心原则:先单接口找基准,再多接口模拟真实链路 结果解读不能只看 QPS,P99 和错误率才是关键

这张图把 wrk 实战拆成了五个模块:安装、参数、脚本、压测、解读。你顺着箭头走一遍,基本就能上手了。我个人建议先从单接口压测开始,跑通了再上多接口流程。别一上来就搞复杂的 Lua 脚本,容易把自己绕进去。