4、wrk进阶实战:自定义lua脚本、模拟不同请求体、保持用户登录态、多接口压测。
好,咱们继续往下走。上一章我们把 wrk 的基础用法捋了一遍,说白了就是能发个 GET 请求,看看 QPS 和延迟。但真实的生产环境哪有这么简单?你想想看,一个电商系统,你得先登录拿到 token,然后带着 token 去下单、去支付,中间还得传不同的 JSON 体。如果 wrk 只能发固定请求,那这压测基本就是自欺欺人。
所以这一章,咱们来点真格的。我会带你手写几个 Lua 脚本,把 wrk 从一个「玩具」变成真正的「生产级压测工具」。我个人习惯是把 Lua 脚本分成三个层次:请求构造层、状态管理层、多接口编排层。咱们一层一层剥开。
4.1 为什么需要 Lua 脚本?
wrk 本身是 C 写的,性能极好。但它只提供了一个简单的 HTTP 请求模板。如果你想动态改变请求体、想从上一个响应里提取数据、想模拟用户登录态……这些事,wrk 自己干不了。
Lua 脚本就是 wrk 的「外挂大脑」。它允许你在三个关键阶段插入自定义逻辑:
- setup:线程初始化时执行一次,适合做全局配置。
- request:每次发送请求前执行,你可以在这里修改请求头、请求体。
- response:每次收到响应后执行,你可以在这里解析数据、提取 token。
核心思路:用 Lua 脚本把「无状态」的 wrk 变成「有状态」的压测引擎。说白了,就是让 wrk 学会「记住」东西。
4.2 自定义请求体:模拟不同 JSON 数据
先来个简单的。假设我们要压测一个下单接口,每次请求的订单号、商品 ID、数量都得不一样。你不能所有请求都用同一个订单号,那后端一查缓存全是命中,压出来的数据就是假的。
我写一个 Lua 脚本,每次请求生成随机的订单数据:
-- random_order.lua
math.randomseed(os.time())
request = function()
local order_id = "ORD" .. math.random(100000, 999999)
local product_id = math.random(1, 100)
local quantity = math.random(1, 5)
local body = string.format(
'{"order_id":"%s","product_id":%d,"quantity":%d}',
order_id, product_id, quantity
)
wrk.headers["Content-Type"] = "application/json"
return wrk.format("POST", "/api/order/create", nil, body)
end
这里有个坑,我踩过。你看第一行 math.randomseed(os.time()),如果不加这一句,所有线程生成的随机数序列是一样的。因为 wrk 是多线程并发,每个线程的 Lua 环境是独立的,但如果不重新播种,每个线程的随机数种子都一样。结果就是:线程1生成订单号 ORD123456,线程2也生成 ORD123456。压测时后端收到大量重复订单号,直接报唯一索引冲突。
我曾经在一次双十一压测中忘了加随机种子,结果压了半小时,数据库里全是重复主键冲突。排查了半天才发现是随机数没播种。嗯,这个细节你一定要注意。
4.3 保持用户登录态:从响应中提取 Token
这才是 wrk 进阶的核心。很多系统需要先登录,拿到 token,然后后续请求都带着这个 token。wrk 本身没有 cookie 管理机制,但我们可以用 Lua 脚本手动实现。
思路是这样的:
- 在 setup 阶段,先发一次登录请求,拿到 token。
- 把 token 存到全局变量里。
- 在 request 阶段,把 token 塞进请求头。
看代码:
-- login_and_press.lua
token = nil
setup = function(thread)
-- 每个线程只做一次登录
local login_body = '{"username":"test_user","password":"123456"}'
local login_resp = wrk.format("POST", "/api/user/login", nil, login_body)
-- 注意:这里需要自己实现一个简单的 HTTP 客户端来发登录请求
-- wrk 的 setup 阶段不能直接用 wrk.format 发送,需要借助 socket
-- 实际项目中我一般用下面的方式:
local sock = ngx.socket.tcp()
sock:connect("your_host", 80)
sock:send("POST /api/user/login HTTP/1.1\r\nHost: your_host\r\nContent-Type: application/json\r\nContent-Length: " .. #login_body .. "\r\n\r\n" .. login_body)
local response = sock:receive("*a")
sock:close()
-- 解析 JSON 提取 token(这里用简单的字符串匹配,生产环境建议用 cjson)
local _, _, t = string.find(response, '"token":"([^"]+)"')
token = t
thread:set("token", token)
end
request = function()
local token = wrk.thread:get("token")
wrk.headers["Authorization"] = "Bearer " .. token
return wrk.format("GET", "/api/user/profile")
end
我个人习惯把登录逻辑单独抽成一个函数,然后在 setup 里调用。这样脚本结构更清晰。另外,如果登录接口有验证码或者动态密码,那你就得在压测前先准备好一批有效的 token,写到文件里,让每个线程读一个。别想着在压测过程中动态获取验证码,那太慢了。
4.4 多接口压测:模拟用户真实行为
真实用户不会只点一个按钮。他先登录,然后浏览商品,加购物车,下单,支付。如果你只压测下单接口,那后端看到的流量模式是失真的。因为缓存、连接池、数据库连接等资源的使用情况完全不同。
我建议用 Lua 脚本实现一个「用户行为序列」。说白了,就是定义一个数组,里面按顺序放好要调用的接口,然后循环执行。
-- multi_api_press.lua
local api_sequence = {
{method = "GET", path = "/api/product/list", body = nil},
{method = "POST", path = "/api/cart/add", body = '{"product_id":1,"quantity":1}'},
{method = "POST", path = "/api/order/create", body = '{"address_id":1}'},
{method = "POST", path = "/api/payment/pay", body = '{"payment_method":"wechat"}'},
}
local index = 1
request = function()
local api = api_sequence[index]
index = index + 1
if index > #api_sequence then
index = 1 -- 循环执行
end
return wrk.format(api.method, api.path, nil, api.body)
end
这里有个关键点:接口之间的依赖关系。比如下单接口需要用到购物车接口返回的 cart_id。那你就得在 response 阶段把 cart_id 提取出来,动态拼接到下一个请求里。这其实就是一个「轻量级的业务流程编排」。
我画了一张图,帮你理解整个流程:
4.5 实战中的避坑指南
脚本写完了,跑起来也简单,一行命令:
wrk -t4 -c100 -d30s -s multi_api_press.lua http://your-api.com
但跑起来之后,你可能会遇到几个问题。我一个个说:
| 常见问题 | 原因 | 解决方案 |
|---|---|---|
| QPS 上不去 | Lua 脚本里做了太多字符串拼接或 JSON 解析 | 把能预计算的内容放到 setup 阶段,不要在 request 里重复计算 |
| Token 失效 | 压测时间太长,token 过期了 | 在 response 里检测 401,触发重新登录;或者压测前生成一批长期有效的 token |
| 随机数重复 | 多线程未正确播种 | 每个线程的 setup 里调用 math.randomseed(os.time() + thread_id) |
| 内存泄漏 | response 阶段不断拼接字符串,未释放 | 使用局部变量,避免全局 table 无限增长 |
我曾经遇到过一个特别隐蔽的问题。压测跑了 10 分钟,QPS 从 5000 慢慢掉到了 2000。排查了半天,发现是 Lua 脚本里有个全局 table,每次 response 都往里 push 数据,但从来没清理过。结果这个 table 越来越大,GC 频繁触发,把 CPU 都吃掉了。嗯,全局变量一定要慎用,用完记得置 nil。
4.6 小结
这一章的内容,说白了就是让 wrk 学会「思考」。通过 Lua 脚本,我们可以:
- 动态生成不同的请求体,模拟真实用户数据
- 从响应中提取 token,保持登录态
- 编排多接口调用,模拟完整的业务流程
这些技巧,我在多个生产项目中都用过。比如某次电商大促压测,就是用这套方案模拟了 10 万用户同时浏览、加购、下单、支付的完整链路。最后发现支付接口的数据库连接池配置有问题,提前暴露了隐患。
你想想看,如果当时只用单接口压测,这个问题根本发现不了。这就是进阶实战的价值所在。
下一章,我们会聊到如何分析 wrk 的输出结果,以及如何根据延迟分布图来定位性能瓶颈。但那是后话了,先把这一章的脚本练熟,跑通几个场景,你就算真正入门了 wrk 进阶。
想要更多资料,联系可可