☰
wrk压测实战:事件驱动模型与Lua脚本性能调优指南
2026/10/10 3:54:04 网站建设 项目流程

简介:wrk 4.1.0 Linux版是一款轻量、高性能的HTTP基准测试工具,采用C语言编写,专门面向高并发Web服务器压力测试场景,适合开发、运维和测试人员在Linux环境中快速评估服务吞吐量、延迟与成功率等关键指标。资源包为7z压缩格式,整体约168.72MB,内含wrk 4.1.0源码及配套使用讲解,覆盖源码编译、权限设置、依赖安装等部署准备,并详细说明连接数、持续时间、线程数、自定义请求头等核心参数,以及如何借助Lua脚本定制请求头、请求体和测试节奏,模拟贴近业务实际的复杂压力场景。安装后通过命令行即可发起测试,输出中包含每秒请求数、平均延迟、最大延迟等指标,能直观判断服务器承载能力。同时,资料还对比了wrk与JMeter、ab等主流工具的适用边界,帮助读者理解高并发短连接测试与功能负载测试的差异,为工具选型提供参考。已有586人学习下载,适合需要快速完成Web服务基准测试、撰写压测报告或开展容量规划的工程师参考。

1. 先搞清楚:wrk 到底是什么,为什么值得花时间学

在 Linux 服务器上做接口压测,很多人第一个接触的工具是 curl 加死循环,或者压测平台上一键发压。真到了要量化“这个服务到底能扛多少 QPS、瓶颈在连接还是业务逻辑”的时候,这些手段就不够用了。wrk 就是为这个场景设计的:一个基于事件驱动模型的高性能 HTTP 压测工具,单机就能压出几十万并发连接,前提是你会正确用它的线程模型和 Lua 脚本接口。它不追求图形界面,不搞分布式管控,就一件事——把压力准确地打到你指定的接口上,然后给你一组可信的延迟和吞吐数据。这篇笔记适合两种读者:一是刚接触 Linux 压测、想找个趁手工具的人,二是已经在用 ab 或 curl 压接口、怀疑结果不准确想换个更接近真实场景的工具的人。先把最核心的一句话记住:wrk 不擅长模拟复杂业务流,它擅长的是把“固定压力下的服务表现”这个问题回答清楚。

2. 事件驱动模型:为什么 wrk 能压出比 ab 高一个数量级的并发

2.1 线程、连接和 epoll:wrk 的资源模型决定了它的天花板

wrk 压测时只做两件事:建立连接、发送请求。它之所以能轻松维持上万个并发连接,靠的是 Linux 的 epoll 事件通知机制,而不是传统的“每连接一线程”模型。传统压测工具,比如 ab,每开一个并发连接往往要占一个线程或进程,连接数一上去,线程切换和内存占用先把压测机自己拖垮。wrk 则是用少量线程配合 epoll 监听海量 socket 事件,事件来了才处理,没事件就挂起,CPU 都花在真正干活上。

wrk 默认参数里有一条硬约束:线程数等于 CPU 核数,这是作者设计上深思熟虑的。每个 wrk 线程会独占一部分连接,所有连接的事件都在同一个线程的 epoll 实例上处理,不存在跨线程锁竞争。你用-t 4 -c 10000跑 4 个线程 1 万个连接时,每个线程负责 2500 个连接的事件轮询,它们之间互不干扰。

这里有个常见误解:wrk 里的连接数是总连接数,不是并发连接数。-c参数指定的是总连接数,wrk 会把这些连接均匀分配到各个线程。连接建立完成后,每个连接上持续发请求,所以这 1 万个连接理论上都在并发工作,协议层没有 HTTP keep-alive 断开的问题。你可以把连接数理解成“管道数”,线程数理解成“管理管道的人”,管道越多、管理的人分配越合理,压力就越大。

wrk 对压测机的资源占用也要心里有数。每个连接上 wrk 都会分配读写缓冲区,连接数上到 5 万以上时,压测机的内存和安全连接数上限会先成为瓶颈。压测前必须检查两个内核参数:文件描述符上限和端口范围。很多人遇到“wrk 连接全部失败”的报错,根因往往不是 wrk 本身,而是ulimit -n不够。

在实际使用时,我是这么配置的:

# 查看当前文件描述符上限 ulimit -n # 临时调高单进程可用文件描述符数量(只对当前 shell 生效) ulimit -n 1048576 # 查看本地端口范围,确认有足够端口供连接建立 sysctl net.ipv4.ip_local_port_range

逻辑不复杂:wrk 作为客户端会占用本地端口,端口范围默认是 32768 到 60999,差不多 2 万多个端口。如果你压测的连接数大于端口范围,又没有开启net.ipv4.tcp_tw_reuse等回收机制,新连接就无法建立。这个坑在长连接压测场景不突出,但如果你用-d 30s压 30 秒、每次请求都新建连接,连接数一高马上就会撞上端口上限。

2.2 延迟分布和吞吐量:wrk 输出的指标到底应该怎么读

wrk 跑完后的输出里有一组数字,包括 Latency 平均值、标准差、最大值,以及 Request per second。新手最容易犯的错误是只看 QPS,忽略延迟分布。wrk 会把延迟的分布区间直接打出来,从 50% 分位到 99.99% 分位都有,这一组数据才是判断服务真实体验的关键。

我一般先看 p99,再看平均值。平均延迟低但 p99 高,说明大部分请求很快、但有一小部分请求卡了很久。这类情况在 wrk 输出里表现为 Std. Dev 特别大。比如同一个接口,Latency 平均值 20ms,标准差却到了 300ms,那基本可以判断服务存在间歇性阻塞,很可能是 GC 停顿、连接池获取超时或者某些线程执行了慢查询。

吞吐量部分还有一个输出项容易被忽略:每个线程的 Request/sec。wrk 会打印 “Requests/sec” 和每个线程的吞吐。如果 4 个线程跑出来的吞吐差异特别大,说明压测机本身的 CPU 调度不均衡,或者服务端把连接都打到了同一个 worker 进程上。这时候优先检查服务端的 accept 队列和 worker 绑定策略,而不是急着改 wrk 参数。

wrk 的输出还包含 Socket errors 统计,分为 connect、read、write、timeout 四类。读超时和写超时是服务端处理不过来的信号,connect 失败则多半是压测机或服务端的连接数天花板到了。我在一次压测里看到 read 超时上千个,平均延迟却不高,原因是服务端对长耗时请求直接做了超时断开,客户端看到的都是快速失败,平均数据自然好看。

有一个你自己得想清楚的点:wrk 输出的 QPS 是“压测机成功发送并收到响应的请求数”,不是服务端实际处理的请求数。如果服务端有负载均衡在前面,wrk 压的是负载均衡的入口,这个 QPS 代表的是整条链路的能力。你的压测目标具体是测单体服务还是测全链路,决定了 wrk 该压在哪一层。常见做法是在被测服务前面直接压,绕过负载均衡;但如果目标是评估扩容后集群的承载上限,就需要把负载均衡纳入压测范围。

2.3 别把 ab、wrk、jmeter 混为一谈:选型要看压测场景

工具选错,压测结果会误导扩容决策。ab 适合做最简单的连通性验证和单机小并发压测,它不维护连接池,每个请求都新建连接,压出来的数据天然偏低,而且高并发下压测机自己先撑不住。jmeter 功能最全,能模拟复杂的业务脚本、断言和分布式压测,但它的线程模型和资源开销注定单机并发能力有限,压 1 万并发时需要好几台压测机配合。

wrk 处于两者之间:比 ab 真实,比 jmeter 轻量。它维护连接池,可以做长时间稳定压测,能用 Lua 脚本构造不同的请求路径,但做不到分布式和复杂业务流编排。选型时就这么判断:如果只是想知道“接口通不通、能跑多少 QPS”,ab 够了;如果要压高并发长连接场景、看延迟分布、模拟真实请求头,直接用 wrk;如果要做全链路多步骤交易流压测,wrk 的 Lua 脚本也能勉强做,但维护成本高,不如直接上 jmeter。

wrk 还有一点优于 jmeter:结果稳定。jmeter 在高并发时会因为自身 JVM GC 导致压测数据抖动,wrk 是纯 C 实现,没有 GC 停顿,压测过程中 CPU 占用平稳。你拿去跟领导汇报压测结果时,wrk 的数据不会被质疑“是不是压测工具自己不稳定”。

3. 从源码编译到跑出第一份报告:wrk 的最小上手路径

3.1 编译安装:三分钟装好一个可用的 wrk 4.1.0

wrk 4.1.0 的安装方式很简单,源码编译。它依赖 OpenSSL 开发头文件,主要用来支持 HTTPS 压测,如果你只需要压 HTTP,理论上去掉 TLS 相关代码也能编译,但我不建议你这么做——生产环境接口基本都是 HTTPS,装一次带 TLS 支持的版本省得以后重新编译。

编译过程分三步,每一步都可能遇到环境问题,我直接给完整的命令序列:

# 安装基础依赖(Debian/Ubuntu 系) apt update apt install -y build-essential libssl-dev git # 克隆 wrk 源码并切换到 4.1.0 标签 git clone https://github.com/wg/wrk.git /opt/wrk cd /opt/wrk git checkout 4.1.0 # 编译并安装到 /usr/local/bin make ln -s /opt/wrk/wrk /usr/local/bin/wrk # 验证版本 wrk --version

依赖安装这一步在不同发行版上命令不一样,CentOS 系列用dnf install -y gcc make openssl-devel git,上面这段适用 Debian/Ubuntu。编译时如果报错找不到openssl/ssl.h,说明libssl-dev没装或版本不匹配,装好后记得ldconfig刷新链接库缓存。git 拉取源码时如果网速不稳定,可以换镜像源,但注意别随便用第三方的压缩包,版本混了编译期看不出来,跑起来对不上数据就麻烦了。

git checkout 4.1.0这一步一定要做,因为 master 分支上可能有不稳定的新特性,压测工具需要的是确定性,不是最新代码。装完之后在任意目录敲wrk -v能看到版本号,如果提示找不到命令,大概率是/usr/local/bin不在 PATH 里,可以直接用绝对路径/opt/wrk/wrk验证编译产物是否正确。

有一个环境细节经常坑人:wrk 是单文件二进制,编译机用的 glibc 版本如果低于运行机,运行时会报 “version GLIBC_xxx not found”。所以一般建议在目标服务器上直接编译,不要在一台高版本系统的机器上编完拷到老服务器上跑。

3.2 核心参数逐个拆解:一次压测需要确定哪些要素

wrk 的参数不多,但每个都直接影响测试结果。我用一张实际压测命令做例子,把每个参数的作用和边界条件都拆开讲:

wrk -t 8 -c 4000 -d 60s -T 5s --timeout 10s -H "Host: api.example.com" \ -H "User-Agent: Mozilla/5.0" --latency https://api.example.com/v1/health

-t 8指定线程数为 8,通常设置为压测机 CPU 核数。核数不足时多开的线程会互相抢 CPU,压测能力反而下降;核数太多时每个线程管理的连接数过少,事件批处理效率变低。-c 4000是总连接数,不止在于大小,还要注意它必须能被线程数整除。wrk 分配连接时做的是平均分配,除不尽时会有线程多拿连接,造成负载不均。

-d 60s是压测时长,最少不要低于 30 秒,否则 TCP 慢启动和连接建立过程还没走完,数据不具有代表性。-T 5s是 wrk 自身的超时探测间隔,不是请求超时时间。--timeout 10s才是 socket 超时时间,超过 10 秒没有响应的请求会被判定为超时错误。-H是请求头,一次可以传多个,注意 wrk 默认请求头只包含 HTTP/1.1 必要字段,真实业务需要带上 Host、Authorization 时都要通过-H手动附加。

最后那个--latency参数很重要,加了它输出里才会有延迟分布区间表格。不加只有平均值、标准差和总吞吐。压测接口做容量评估时,p99 指标必须有,我习惯性每次压测都带上这个参数,多敲几个字母,回报是一份能拿去讲清楚问题的数据。

参数确定后按这个顺序跑压测:先用小并发热身 10 秒,确认接口返回正常、没有 4xx 5xx 错误,再逐步上调连接数跑正式测试。一上来就用大并发压,一旦服务端有拦截策略,wrk 会被挡在网关外面,压到的全是网关的拒绝响应,数据完全没有参考价值。

# 热身:确认接口连通性,观察响应码分布 wrk -t 2 -c 50 -d 10s --latency http://127.0.0.1:8080/ping # 正式测试:先按当前机器 CPU 核数设置线程数,再逐步加连接数 wrk -t 8 -c 1000 -d 60s --latency http://127.0.0.1:8080/api/query wrk -t 8 -c 2000 -d 60s --latency http://127.0.0.1:8080/api/query

从 1000 到 2000 的连接数递增是有讲究的。单接口压测时,吞吐量会随连接数上升而上升,直到触碰某个瓶颈点,之后 QPS 不再增长、延迟开始线性上升。这个拐点就是服务的并发承载上限,找到它比跑一个超大数据看最高 QPS 更有价值。做容量规划时,我会在不同连接数下各跑一轮 60 秒压测,记录 QPS 转折点,然后用这个值乘以一定的安全系数作为线上限流阈值参考。

参数还有两个冷门但关键的可调项:-L是持久化连接的总时长上限,-s是指定 Lua 脚本路径。-s后面会专门展开,这里先记住一个约定:没有-s时,wrk 对每个连接持续发送固定请求;有-s时,脚本里的request()函数决定每次请求的内容,极大地扩展了压测的玩法。

3.3 最小压测脚本:从搭服务到出报告的一次完整演练

光讲参数不落地是学不会 wrk 的。我建议你先在自己机器上搭一个测试服务,用最简单的 Python 接口来跑通整个流程。这个练习的价值在于:你能在一个完全可控的环境里理解连接数、线程数、延迟之间的关系,比直接去压线上服务心里有底得多。

# server.py:本地测试服务,返回 JSON 和模拟处理延迟 from http.server import HTTPServer, BaseHTTPRequestHandler import json import time class Handler(BaseHTTPRequestHandler): def do_GET(self): time.sleep(0.01) # 模拟 10ms 业务处理耗时 resp = json.dumps({"status": "ok"}).encode() self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(resp))) self.end_headers() self.wfile.write(resp) def log_message(self, format, *args): pass # 关闭默认日志,避免压测时刷屏 HTTPServer(("0.0.0.0", 8080), Handler).serve_forever()

这个服务每个请求固定睡 10 毫秒,理论上单线程 QPS 上限约 100,线程池或协程并发下吞吐可以更高。先用python3 server.py启动,再按上一节的热身命令跑一轮。正常结果里 Requests/sec 应该在 300 到 800 之间,具体取决于你的 CPU 性能,因为sleep(0.01)让线程能在等待期间处理新请求。

跑完这轮后,试着把连接数从 50 提升到 500,你会看到请求延迟几乎不变,但 QPS 显著上升。这是因为连接数增加后,实际并发处理能力被充分使用。继续往上加到 5000,你会发现延迟开始明显变大,QPS 不再线性增长,这就是找到服务瓶颈了。通过这个简单实验,你就理解了 wrk 压测的核心逻辑:上调并发 → 观察延迟和吞吐的转折点 → 定位瓶颈。

4. 用 Lua 脚本把 wrk 从压测工具变成业务模拟器

4.1 Lua 脚本的三个阶段:setup、request、response 的配合逻辑

wrk 自带的 Lua 脚本接口是它区别于 ab 的最大亮点。脚本贯穿一次压测任务的全过程,分为三个阶段:setup函数在线程启动时执行一次,request函数在每个请求发送前被调用,response函数在收到响应后执行。三者的关系可以用一个实际场景说清楚:压一个登录接口,需要先拿到 token,再带着 token 压业务接口。

常见的做法是写一个脚本,用setup阶段完成登录获取 token,把 token 存入全局变量;request阶段每次构造带 token 的请求头;response阶段检查响应状态码、统计失败数。wrk 官方文档里对这三个阶段有明确说明,但实际使用时很多人会把setup理解成压测前的准备工作,这是不对的——setup在每个线程里都会执行一次,不是整个压测任务只执行一次。所以你在setup里做的任何初始化工作,都要保证多线程同时执行时不会互相踩踏。

-- auth.lua:带 token 的压测脚本 local token = nil function setup(thread) -- 每个线程启动时执行一次,用于初始化 local resp = wrk.request("POST", "http://api.example.com/login", {["Content-Type"] = "application/json"}, '{"username":"test","password":"123456"}') local body = resp:match('"token":"([^"]+)"') token = body -- 存到全局变量,request 阶段使用 end function request() return wrk.request("GET", "http://api.example.com/order/list", {["Authorization"] = "Bearer " .. token}) end function response(status, headers, body) if status ~= 200 then -- 统计非 200 响应,用计数器记录 wrk.stats.add("bad_response", 1) end end

wrk.request在 setup 阶段返回的是一个字符串响应体,不是连接句柄,所以直接拿resp做字符串匹配取 token。这段脚本里的wrk.stats.add是给自定义统计项累加数值的接口,压测结束时wrk.stats会打印所有自定义统计值。

脚本调试阶段有个血泪经验:先wrk -t 1 -c 1 -d 5s -s auth.lua跑一次,确认 token 能正常获取、请求返回 200,再上正式并发。直接用大并发调试脚本的话,一旦脚本本身写错,所有线程都会同时报错,你根本分不清是脚本问题还是服务问题。

4.2 模拟真实用户思考时间:wrk 也能压出“有节奏”的流量

wrk 默认是发完一个请求立刻发下一个,这是最粗暴的压测模式。真实用户不会这样操作,他们点击后有阅读时间、填写表单、犹豫操作等停顿,这种节奏差异会直接影响服务端的连接占用和资源释放规律。纯并发压测下,服务端连接池会被持续占满;真实节奏下,连接会周期性释放,服务端能复用连接和缓冲。

模拟思考时间有两种常见做法。一种是在 Lua 脚本的request()函数里直接wrk.sleep(0.1),每次请求前强制休眠 100 毫秒;另一种是用wrk.response阶段根据上一个请求的结果动态调整下次请求的时间。前者简单直接,后者更接近真实但控制复杂。

-- delay.lua:模拟用户思考时间的压测脚本 local rand = require "random" -- wrk 内置的随机数模块 function request() -- 模拟 50% 概率下用户阅读了 2 秒才进行下一步 if math.random() < 0.5 then wrk.sleep(2) end return wrk.request("GET", "http://api.example.com/feed/list", {["Accept"] = "application/json"}) end function response(status, headers, body) -- 记录各类状态码出现的次数 if status == 500 then wrk.stats.add("server_error", 1) elseif status == 502 or status == 503 then wrk.stats.add("gateway_error", 1) end end

wrk.sleep的单位是秒,支持小数。用math.random()控制概率时注意 Lua 的随机数种子,wrk 在每个线程启动时会自动设置随机种子,不同线程的随机序列不会完全一致,这一点你不用额外处理。加上思考时间后,同样的连接数下实际请求频率会降低,但服务端面临的连接生命周期变化更加复杂,更接近线上真实情况。

模拟思考时间有一个副作用需要关注:加长单个请求的间隔后,整体压测时长同步延长,指标采集周期内的样本量变少,延迟分布的置信度略降。要保证统计意义,整体压测时长最少保持在 60 秒以上,否则失真的风险变大。我一般会在测试报告中注明“加了思考时间模拟”,避免后续看数据的人误以为是高并发极限测试。

4.3 压测 POST 和 PUT:带 JSON 体、带文件上传的脚本写法

GET 接口压测相对简单,真实业务里 POST、PUT 接口的压测需求更常见,特别是带 JSON 请求体的接口,比如下单、支付、异步任务提交。wrk 的 Lua 脚本处理 POST 请求有几种写法,核心是把请求体编译到脚本里,减少运行时的拼装开销。

-- post.lua:POST JSON 请求压测脚本 local body = '{"user_id":1024,"product_id":555,"quantity":2,"amount":99.50}' local headers = {["Content-Type"] = "application/json"} function request() return wrk.request("POST", "http://api.example.com/api/order/create", headers, body) end function response(status, headers, body) if status ~= 200 then wrk.stats.add("non_200", 1) end end

这段脚本每发一个请求就重新构造一次 body 字符串,在 Lua 层面属于低效操作。如果每次请求的 body 都一样,可以在setup阶段把request用的函数体做成闭包,直接复用同一个字符串。压测数据不变、只关心接口处理能力时,不追求每个请求体都重新生成,复用是最优解。

真正需要动态请求体的场景比较少,比如传入不同的 user_id 来模拟多用户并发。wrk 的做法是给每个线程分配一段独立的 user_id 区间,然后根据线程 ID 取余构造请求。这个方式有点绕,但确实不用额外引入别的工具就能实现多用户模拟。

-- multi_user.lua:按线程分配用户区间 math.randomseed(os.time()) local thread_id = tonumber(wrk.thread and wrk.thread:get("id") or 0) local base_id = thread_id * 100000 function request() -- 每次请求取一个随机用户 ID,模拟独立用户访问 local uid = base_id + math.random(0, 99999) local body = string.format('{"user_id":%d,"page":1}', uid) return wrk.request("POST", "http://api.example.com/api/user/action", {["Content-Type"] = "application/json"}, body) end

wrk.thread:get("id")在官方文档里没有特别强调,但确实存在,用于读取当前线程的索引。如果你在脚本里调用它时不放心,可以先在setup阶段把索引存到线程对象上,再在request阶段读取。多用户模拟的关键是让服务端无法做缓存优化,所以每次请求的 user_id 必须变化。

文件上传接口的压测在 wrk 里也能做,绕一点:把文件内容读进 Lua 字符串,拼成 multipart/form-data 格式。注意文件内容比较大的时候,脚本内存占用上升,压测机本身会有压力。我一般不是特别建议用 wrk 压文件上传,更合适的方式是构造小文件甚至空文件体去压接口的逻辑处理链路,而不是真的压大文件传输。

4.4 多接口混合压测:一条脚本模拟真实调用链

真实业务很少只压单个接口,一个下单操作可能涉及商品查询、库存锁定、订单创建、支付跳转多个接口调用,每个接口的耗时特性不同。wrk 的多接口混压就是在一个request()函数里按概率轮流发不同类型的请求。

-- mix.lua:多接口混合压测 local r = math.random function request() local p = r() if p < 0.4 then -- 40% 概率压商品列表 return wrk.request("GET", "http://api.example.com/api/products", {["Accept"] = "application/json"}) elseif p < 0.8 then -- 40% 概率压商品详情 return wrk.request("GET", "http://api.example.com/api/product/1024", {["Accept"] = "application/json"}) else -- 20% 概率压下单 return wrk.request("POST", "http://api.example.com/api/order", {["Content-Type"] = "application/json"}, '{"product_id":1024,"quantity":1}') end end

混合压测的统计口径和内网接口压测完全不同,wrk 会把所有请求的响应合并统计,不会区分每个接口的单独延迟。你要看某个接口的表现,得在response()函数里根据请求方法加 URL 做标记,自己去维护延迟数组。这不是 wrk 的短板,本来它就是无状态压测器,追求的是整体视角。

混合压测有一点必须提前规划:各接口的需求比例要基于线上真实的调用统计,而不是拍脑袋。拿上面的脚本举例,如果线上商品列表被调用的概率实际是 70%,你却按 40% 去压,压测结果只能说明“当前比例下服务能扛住”,不能说明“真实流量下服务能扛住”。所以每次中型以上压测前,我会先从监控系统拉流量比例数据,量化到脚本里,而不是凭感觉写概率。

5. 压测排错避坑:wrk 用的越多越该记住的五个现场

5.1 压测机 CPU 跑满但 QPS 上不去,不是服务端的问题

现象:wrk 的 CPU 占用已经到了 100%,但服务端 QPS 显示远低于预期,延迟数据看着也正常。原因多半出在 wrk 自身的工作模型上:线程数超过 CPU 核数后,线程切换本身消耗大量 CPU,真正用来发请求的时间片变少。另一种可能是连接数太大,每个连接的事件轮询分片太小,导致 epoll 事件上下文切换开销压过了实际的收发动作。

解决:先确认压测机核数,把线程数调成和核数一致或核数加 1,并同步减少连接数试试。如果 QPS 随线程数同步上升,问题就是线程数设置过大;如果线程数降低了 QPS 反而上升,则说明之前确实在 CPU 内耗。反复调参时用top监控 wrk 进程的状态,看%CPU除以线程数的值是否接近单核满载。

5.2 长时间压测时 wrk 突然报 “Too many open files”,服务一切正常

现象:wrk 跑了 5 分钟后,连接建立速度急剧下降,日志里出现大量EADDRNOTAVAIL或者连接失败记录。原因比较明确:服务端没出问题,是压测机的文件描述符或本地端口耗尽了。长连接保持时 fd 占用是持续的,短连接模式下则因为端口被占满了而无法创建新连接。

解决:压测前先确认ulimit -n的值,建议至少是连接数的 4 倍。短连接模式下再查看端口范围,临时调整net.ipv4.ip_local_port_range的小值,例如改成 1024 到 65535。还要打开net.ipv4.tcp_tw_reuse=1和net.ipv4.tcp_tw_recycle=0(注意回收参数在新内核里已经废弃,改成依赖tcp_tw_reuse)。改完内核参数记得sysctl -p生效。

5.3 延迟标准差巨大,平均延迟很低,问题可能出在请求体上

现象:一次压测里平均延迟 15ms,标准差却到 220ms,看起来数据异常。原因往往是业务接口里存在明显的长尾路径:部分请求走了数据库慢查询、部分请求触发了 Redis 缓存穿透。wrk 的输出里 p99 分位数如果远高于平均值,就符合这种规律。

解决:这种时候不要急着优化 wrk 参数,而是把响应时间的分布数据拉到监控平台按时间线画出来,对比时间段内其他指标(GC、CPU、TCP 重传率等)。wrk 客户端存在一种自身导致的假性长尾:当压测机 CPU 被占满时,部分请求在发送阶段就被延后了,也会表现为延迟抖动。可以在压测时用另一台机器跑一个占用较小的监控命令,排除客户端抖动对延迟分布数据的污染。

5.4 服务端出现大量 TIME_WAIT 连接,压测结束后仍然堆积

现象:短连接压测方式下,服务端机器netstat显示 TIME_WAIT 达到数万,占用大量端口和内存。原因就是每次请求都新建连接,服务端主动断开连接后进入了 TIME_WAIT 状态。TIME_WAIT 本身不是错误,但数量过多会影响服务端接受新连接的能力。

解决:优先把压测模式从短连接改成 keep-alive 长连接。wrk 默认就是长连接模式,出现这种情况只有两个可能:脚本里每次请求都用了不同的 Host 或 Connection: close,或者服务端配置了空闲连接超时自动断开。检查脚本请求头里没有写Connection: close,再检查服务端的 keep-alive 超时设置。如果必须保留短连接压测,就需要调高服务端net.ipv4.tcp_max_tw_buckets并开启tcp_tw_reuse,但这属于事后补救,不如直接改长连接。

5.5 压测 HTTPS 接口一直报 SSL 握手失败,但浏览器能正常访问

现象:wrk 压 HTTP 接口没问题,改成 HTTPS 后大量请求报 SSL 错误,且错误码集中在证书验证阶段。原因大概率是 wrk 编译时链接的 OpenSSL 版本与运行环境不一致,或者目标服务的证书链不完整导致客户端校验失败。wrk 默认不对证书做严格校验,但如果证书链缺失中间证书,连接会直接断开。

解决:先确认编译环境安装的是完整版 OpenSSL 开发库,不要用 LibreSSL 替代。然后看目标服务证书链是否完整,用openssl s_client -connect host:443 -showcerts检查服务端下发链路。如果证书链没问题,可以把 wrk 的 TLS 校验方式调整为跳过证书验证来定位是否客户端问题,但生产环境压测强烈不建议绕证书校验,数据容易失真。

6. 上传带宽、连接池和 QPS 会互相掣肘:多机压测的参数再校准

wrk 单机性能已足够强,但压测大流量接口时你早晚会撞到一个天花板:单台压测机的带宽和 CPU 是有限的。常见做法是在多台机器上部署 wrk,同时向同一目标发起压测,然后把数据汇总到一个地方做聚合。这一步看起来简单,实际会遇到两个关键问题:多机的时间对齐和结果合并。

先解决时间对齐问题。两台压测机即使同时执行 wrk 的命令,启动时刻也会有毫秒级偏差,这对 60 秒压测的影响可以忽略,但如果压的是 10 秒短突发流量,就必须用一个同步机制。最常见做法是通过脚本同时 SSH 到多台机器,用一条命令把它们拉起来:

# multi_wrk.sh:批量在多台压测机发起压测 for host in 192.168.1.101 192.168.1.102 192.168.1.103; do ssh $host "cd /opt/wrk && ./wrk -t 8 -c 2000 -d 60s --latency \ http://api.example.com/health > /tmp/wrk_$(hostname).log 2>&1" & done wait echo "所有压测机已完成,开始汇总结果"

这段脚本用了 shell 的后台执行和wait,保证三台机器在同一时刻附近启动压测任务。输出日志分别保存到各自主机名命名的文件里,防止数据混淆。做完之后把每个文件里的 Requests/sec 和 Latency 相加,或者简单做加权平均,多机压测的数据汇总没有专门工具,通用的做法是直接拉平后相加得到总吞吐。

多机压测的参数分配有一个关键点:各机器的连接数和线程数不需要完全一致。带宽小的机器少分担连接数,CPU 强的机器多分。比如三台机器里两台是 8 核,一台是 4 核,可以分别用-t 8 -c 2000和-t 4 -c 1000。wrk 本身没有分布式协调机制,手动给每台机器分配负载是常用的替代方案。

多机压测还要注意目标服务器的连接数上限。单台 wrk 开了 2000 个连接,三台就是 6000 个,如果你的服务端配置了每 IP 最大连接数限制,多机压测直接把目标打挂,大量的连接被服务端拒绝重试,垃圾流量会把压测数据污染。所以压测之前先确认服务端和中间层(Nginx 层、网关层)的 worker 连接数配置,预留 30% 余量。

如果压测的接口是上传类或下载类,带宽的计算要先于连接数。公式很简单:并发连接数乘以单连接平均吞吐,不能超过压测机网卡带宽的 80%。比如单连接平均流量是 20KB/s,压 1000 个连接就是 20MB/s,再换算成带宽就是 160Mbps,千兆网卡下还算安全,但十千兆网卡连续跑满带宽会让 TCP 重传率飙升,压出来的延迟数据全是水分。

多机压测完成后,验证数据的可信度有个笨办法:把各台机器的响应时间分布单独拿出来对比。如果某一台机器的 p99 显著高于其他机器,大概率是这台压测机本身有问题(带宽不够、CPU 抢占、内核参数没调),而不是目标服务在对应链路上变慢。数据一致性检查这一步,我每次都会做,比我用平均数据去掩盖单机异常要靠谱得多。

最后说一个我自己的习惯:压测脚本和参数记录到 git 里,每次压测完把 wrk 的输出数据文件一起提交,备注写明压测目标、时间、服务版本、wrk 参数。等下次上线前要做回归对比时,直接翻历史记录就能知道上一轮基线是多少,不用重新跑一遍。这个习惯帮我避免过很多次“这轮数据到底比上轮好还是差”的争论。wrk 学到这一步,你缺的已经不是工具命令了,而是对每一份压测数据都保留可复现的上下文——这才是压测真正值钱的地方。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询