wrk HTTP/1.x 连接压测与生成器校准手册
wrk 测的是给定连接下能完成多少
wrk 用少量线程和 epoll、kqueue 一类事件通知机制维护大量 HTTP 连接。每条连接收到响应后继续推进请求,因此它天然形成闭环:服务变慢,连接发出下一次请求也会变慢,实际到达率随之下降。这个模型适合建立 HTTP/1.x 连接吞吐基线,不适合证明外部仍以固定速率到达时系统不会排队。
高 Requests/sec 也不等于业务成功。状态码为 401 的拒绝路径往往比真实业务短,wrk 可以非常高效地测量它。压测结论必须同时拥有客户端完成量、状态码与 socket 错误、服务端业务成功计数、发压机资源和服务端饱和证据;只保留终端摘要,无法区分容量、错误捷径和生成器上限。
源码身份比一个版本字符串更可靠
wrk 官方构建路径面向类 UNIX 系统。容量基线应同时固定 tag 和 tag 指向的完整提交,避免默认分支或异常移动的标签悄然改变二进制。本文沿用官方 wrk 4.2.0 tag 与提交 a211dd5a7050b1f9e8a9870b95513060e72ac4a0 作为复现实验基线,不把它写成永久最新版本。
set -eu
WRK_TAG='4.2.0'
WRK_COMMIT='a211dd5a7050b1f9e8a9870b95513060e72ac4a0'
git clone --depth 1 --branch "$WRK_TAG" --single-branch https://github.com/wg/wrk.git
cd wrk
test "$(git rev-parse HEAD)" = "$WRK_COMMIT"
make
./wrk --version
printf 'wrk source revision: tag=%s commit=%s\n' "$WRK_TAG" "$(git rev-parse HEAD)"源码提交、编译器、LuaJIT、OpenSSL、编译选项和二进制哈希一起进入工具清单。使用系统库时显式传入路径:
make WITH_LUAJIT=/usr WITH_OPENSSL=/usr
ldd ./wrk 2>/dev/null || otool -L ./wrk
sha256sum ./wrkLinux 或 macOS 包管理器可能提供带发行方补丁的 wrk,安装方便不代表与官方源码构建等价。报告要记录包版本、来源和链接库。Windows 开发机更适合在 WSL 或批准的 Linux 发压机中运行,不把来源不明的移植版混入基线。
-t 与 -c 管的是两层对象
wrk -t4 -c100 -d30s --timeout 2s --latency \
-H "Accept: application/json" \
-H "X-Load-Test: wrk-baseline" \
https://perf.example.com/health-t4 是四个工作线程,-c100 是总共保持的一百条 HTTP 连接,不是每线程一百条。每个线程分摊一部分连接并在事件循环中推进。-d30s 规定运行时长,不保证这段时间均匀到达;--timeout 2s 把超过等待上限的连接计入 timeout;--latency 打开详细延迟分布。
线程太少时,单个事件循环可能先吃满 CPU;线程过多又增加调度、缓存和统计合并成本。线程数应在固定发压机 CPU 亲和性与规格下校准,不与业务线程或用户并发做一一映射。连接数则直接影响客户端文件描述符、临时端口、目标 listen backlog、网关连接表和服务端 socket。
| 参数 | 实际控制对象 | 常见误读 |
|---|---|---|
-t | 发压机工作线程 | 业务并发数 |
-c | 全部线程共同维护的总连接 | 每个线程的连接数 |
-d | 运行持续时间 | 固定请求到达率 |
--timeout | 单次连接等待上限 | 整个实验硬停止时间 |
--latency | 输出更详细的延迟分布 | 自动证明业务成功 |
原版 wrk 没有 wrk2 的 -R 恒定吞吐参数。脚本或文档出现 wrk -R 时,先确认二进制身份;把两个项目的参数混写,会让实验根本无法复现。
闭环模型会在目标变慢时自动收油
设有二十条连接,每个请求稳定耗时约两百毫秒。在没有额外流水线的简化条件下,闭环完成速率约为 20 / 0.2 = 100 QPS。若响应耗时翻倍,连接下一轮请求也要更晚发出,完成速率自然降到约一半。系统看似“没有继续进流量”,其实是生成器在等待。
wrk -t1 -c20 -d15s --latency http://127.0.0.1:PORT/slow这个结果能回答指定连接并发下的完成能力,却不能回答固定外部 100 QPS 持续到达后的队列增长。后一个问题应使用支持开放模型或恒定到达率的工具,并同时画出计划到达率、入口到达率、完成率和在途请求。
闭环不是缺陷,而是模型选择。连接池、浏览器或同步客户端本身也可能闭环;只要报告准确写成“在指定连接数与响应等待模型下”,wrk 的结果就很有用。错误来自把连接并发偷偷改写成固定 RPS。
LuaJIT 扩展先固定请求,再增加逻辑
wrk 脚本可以设置方法、路径、请求头和 body,也可以在每次请求前生成内容或在 response() 中处理响应。官方说明指出,固定方法、路径、头和 body 的脚本通常开销有限;逐请求构建新请求、复杂 Lua 逻辑和响应回调会降低可生成负载。
最小静态脚本只表达不可由 CLI 直接固定的请求:
wrk.method = "POST"
wrk.body = '{"sku":"LOAD-001","quantity":1}'
wrk.headers["Content-Type"] = "application/json"
wrk.headers["X-Load-Test"] = "wrk-write-baseline"wrk -t2 -c20 -d20s --latency -s request.lua https://perf.example.com/api/items动态令牌、随机体和关联请求会改变客户端 CPU、内存与分配。需要它们时,先跑等价静态请求测生成器上限,再启用 Lua 并比较发压端差异。若脚本化后服务端资源不变而客户端 CPU 满载、QPS 下跌,测到的是脚本成本。
Lua 文件和请求体进入代码评审,但真实凭证不进入 Git。短期 token 由受控环境变量或临时文件注入,进程列表、终端录屏、异常输出和原始报告都要检查泄露。
一个本地实验拆开错误路径和闭环
实验服务只监听 127.0.0.1,提供正常、固定延迟和认证拒绝三条路径。目录和 PID 均由当前 shell 持有:
LAB="$(mktemp -d "${TMPDIR:-/tmp}/wrk-lab-XXXXXXXX")"
chmod 700 "$LAB"
cat > "$LAB/server.mjs" <<'JS'
import http from 'node:http';
const count = { ok: 0, rejected: 0, connections: 0 };
const server = http.createServer(async (req, res) => {
if (req.url === '/stats') return res.end(JSON.stringify(count));
if (req.url === '/auth') { count.rejected++; res.writeHead(401); return res.end(); }
if (req.url === '/slow') await new Promise(r => setTimeout(r, 200));
count.ok++;
res.writeHead(200, { 'content-type': 'text/plain', 'content-length': '3' });
res.end('ok\n');
});
server.on('connection', () => count.connections++);
server.listen(0, '127.0.0.1', () => console.log(`PORT=${server.address().port}`));
JS
node "$LAB/server.mjs" > "$LAB/server.log" 2>&1 &
PID=$!
for _ in $(seq 1 50); do grep -q '^PORT=' "$LAB/server.log" && break; sleep 0.1; done
PORT="$(sed -n 's/^PORT=//p' "$LAB/server.log")"
test -n "$PORT"先压拒绝路径:
wrk -t1 -c10 -d3s --latency "http://127.0.0.1:$PORT/auth"
curl -fsS "http://127.0.0.1:$PORT/stats"wrk 应报告大量 Non-2xx or 3xx responses,服务端 rejected 增长而 ok 不增长。再压慢路径,观察完成 QPS 随二百毫秒响应时间受连接数约束:
wrk -t1 -c20 -d5s --latency "http://127.0.0.1:$PORT/slow"
curl -fsS "http://127.0.0.1:$PORT/stats"
kill "$PID" 2>/dev/null || true
wait "$PID" 2>/dev/null || true
rm -rf -- "$LAB"固定的请求数量不是验收条件,机器调度会造成差异。稳定证据是错误路径只增加拒绝计数,慢路径业务成功增长且实际速率明显受连接等待约束。
输出要同时读完成、分布和错误
Requests/sec 是完成请求数除以实际运行时间。Latency 与 Req/Sec 的 Avg、Stdev、Max 是采样统计,详细分位数只有在 --latency 下出现。Non-2xx or 3xx responses 表示 HTTP 状态异常,Socket errors 则按 connect、read、write、timeout 分类;两类错误不能合并成一个“失败率”。
服务端还要有与 X-Load-Test 对齐的入口总量和业务成功计数。客户端完成数与入口请求量差异过大时,检查代理、重试、连接失败与统计窗口。入口量接近而业务成功偏低时,查状态码、认证、请求体与业务校验。只有两端语义对齐,延迟和吞吐才值得解释。
wrk 不读取完整响应体做业务断言。需要复杂成功条件时,应在压测前用单请求客户端验证,再让服务端计数或受控日志承担业务正确性;若每个响应都必须动态解析,k6 或 JMeter 更适合。
发压端先满时停止给服务端下结论
每档并发同步观察发压机各核心 CPU、网卡、文件描述符、临时端口、连接状态和 wrk socket 错误。服务端 CPU、池、队列和下游都未饱和,而发压机单核或网卡已满,容量上限属于生成器。增加线程、关闭 Lua 回调或换第二台隔离发压机后吞吐继续增长,进一步证明旧数字不是服务端极限。
建立本机静态响应的生成器基线很有价值:固定协议、响应大小、线程和连接,逐档记录 wrk 上限。正式实验的发压端应保留明确余量,不能用一个跨团队通用的 CPU 百分比代替本机基线。
连接突发还受 ephemeral ports、关闭连接回收和服务端 backlog 影响。官方 README 明确提醒发压机需要足够临时端口,服务端 listen backlog 要能承受初始连接峰值。connect errors 集中在启动阶段时,应先查这条链路,而不是直接增加应用线程池。
HTTP、TLS 和代理边界必须写进结果
wrk 面向 HTTP/1.x,没有官方 HTTP/2 或 HTTP/3 基线。HTTPS 能力依赖构建链接的 OpenSSL,证书、SNI、密码套件和 TLS 版本都会影响握手。直连、经企业代理、经网关缓存和经 CDN 是不同实验,不能混在一张趋势图中。
wrk 基础 CLI 没有与 ApacheBench 或 hey 完全相同的通用代理参数。为了复用现有脚本而偷偷改 DNS、hosts 或透明代理,会改变目标身份和网络路径。报告至少记录最终 IP、HTTP 版本、TLS 协商、Keep-Alive、连接数、代理和重定向行为。
压测只针对显式 allowlist。runner 的主机枚举、网络出口 ACL、入口来源规则与短时测试标记共同限制目标;客户端并发和时长不是安全控制。写请求使用隔离租户与有界数据,服务端保留独立停止开关。错误率、尾延迟、池、队列、数据库或业务告警越线时,停止动作不能依赖唯一的 wrk 终端仍在线。
基线完成时必须能复现也能退出
证据目录保存 wrk 源码身份、二进制哈希、链接库、脱敏命令、Lua 提交、发压机规格、目标构建、协议与连接模型、原始输出、业务计数和两端资源。报告写明这是闭环连接模型,并给出停止条件是否触发、限制和有效范围。
实验结束后确认 wrk 进程退出、临时入口规则关闭、短期凭证撤销、测试数据按 experiment_id 清理、目标指标恢复。无法证明生成器有余量或业务真正成功时,结果只能作为调试线索,不能进入容量承诺。
长期趋势还要保留测试的有效范围。应用构建、JDK、实例规格、网关、证书、数据量、缓存状态、发压机或网络位置任一变化,都可能让旧基线失去可比性。复测结果偏离时先比较这些输入,再定位代码;不要为了维持一条平滑曲线而忽略环境已经换代。对已退役的 Lua 脚本、工具镜像和历史入口保留只读证据,运行权限则随基线退出一起撤销。
官方能力和构建继续以 wrk 仓库、安装说明与仓库中的 SCRIPTING 文档为准。需要请求总量和 Keep-Alive 冒烟时进入 ApacheBench 手册,需要每 worker 限速或 HTTP/2 对照时进入 hey 手册。
