1. 项目概述最近在折腾一个Web服务的性能优化想看看接口在高并发下的表现到底怎么样。网上搜了一圈发现很多老手都在用wrk这个工具做HTTP压力测试。它不像JMeter那样需要复杂的图形界面也不像abApacheBench那样功能单一而是用C语言写的充分利用多核CPU一个命令就能打出很高的并发量特别适合我们这种在Linux服务器上做快速验证的场景。我的主力开发环境是Ubuntu 22.04 LTS所以这篇文章就详细记录下在Ubuntu Linux系统下从零开始安装wrk到上手使用再到用Lua脚本玩出花样的全过程。如果你也在找一款轻量级但威力巨大的HTTP压测工具那wrk绝对值得你花时间了解一下。简单来说wrk是一个现代HTTP基准测试工具。它最大的特点是“短平快”通过多线程配合epoll这样的高性能事件通知机制能在单台多核机器上模拟出巨大的HTTP连接压力。你可能会问为什么不用更出名的ab我实际对比过在相同的测试环境下wrk能更充分地榨干CPU资源尤其是线程模型和连接池的设计让它能轻松维持成千上万的并发连接而ab在高并发时很容易遇到端口耗尽或性能瓶颈。对于后端开发、运维或者任何需要评估Web服务性能边界的人来说掌握wrk就像手里多了一把精准的测压枪。2. 环境准备与wrk安装在Ubuntu上安装wrk通常有两种主流方法一种是直接使用系统包管理器安装预编译的版本这种方法最省事另一种是从源码编译安装这种方法能确保你获得最新的特性并且可以针对你的系统进行优化。我会把两种方法都走一遍并告诉你各自可能会遇到的坑。2.1 方法一使用APT包管理器安装最快捷对于大多数只想快速用起来的用户Ubuntu的官方仓库或一些PPA里其实已经包含了wrk。不过需要注意的是Ubuntu主仓库里的版本可能不是最新的。我们可以先尝试搜索一下sudo apt update apt search wrk如果搜索结果显示有wrk或wrk2一个社区维护的分支包那么可以直接安装sudo apt install wrk安装完成后通过wrk --version来验证是否成功。用APT安装的优点是依赖项如OpenSSL、LuaJIT会被自动处理好完全不需要你操心。但缺点也很明显版本可能比较旧。比如在Ubuntu 22.04上通过默认仓库安装的版本可能是一两年前的发布版会错过一些新的修复和功能。注意有些Ubuntu版本默认仓库里可能没有wrk。如果apt search找不到或者你希望使用最新版那么请直接跳到下面的源码编译方法。2.2 方法二从源码编译安装推荐获取最新版从源码编译是获取最新、最稳定wrk版本的最佳途径。这个过程需要一些基础的开发工具和库。第一步安装编译依赖我们需要安装C编译器、make工具以及关键的开发库。打开终端执行以下命令sudo apt update sudo apt install build-essential libssl-dev git -y这里解释一下这几个包的作用build-essential包含了gcc,g,make等编译套件是编译任何C/C项目的基础。libssl-devOpenSSL的开发库。wrk在进行HTTPS测试时需要用到SSL/TLS功能这个库提供了必要的头文件和链接库。git用于从GitHub克隆wrk的源代码仓库。第二步克隆源码并编译依赖装好后我们就可以获取wrk的源代码了。它的项目托管在GitHub上git clone https://github.com/wg/wrk.git cd wrk进入目录后直接使用make命令进行编译make如果一切顺利编译过程通常很快结束后会在当前目录下生成一个名为wrk的可执行文件。你可以把它移动到系统路径下方便在任何地方调用sudo cp wrk /usr/local/bin/第三步验证安装再次运行wrk --version如果能看到版本信息就说明从源码安装成功了。我比较推荐这种方式因为GitHub上的master分支通常包含了最新的提交修复了一些历史版本中可能存在的连接管理或统计显示的小问题。实操心得编译时可能遇到的坑关于LuaJITwrk的脚本功能依赖于LuaJIT。好消息是wrk的源码包里已经自带了一个LuaJIT在deps/目录下make时会自动编译它。所以你不需要单独安装luajit或libluajit-dev。如果编译失败并提示LuaJIT相关错误可以尝试先make clean再重新make。CPU架构优化make命令默认会使用通用的编译参数。如果你的服务器是特定的CPU架构比如AWS Graviton的ARM编译过程是兼容的但性能可能不是最优。对于生产环境的重度使用可以考虑研究一下针对特定CPU的CFLAGS优化选项不过对于绝大多数测试场景默认编译已经足够强大。权限问题将wrk复制到/usr/local/bin时需要sudo权限。你也可以选择不复制每次使用时通过./wrk来运行需要先cd到wrk目录或者把它添加到你的个人~/bin目录如果该目录已在PATH环境变量中。3. wrk核心参数与基础使用详解安装好之后我们直接来看怎么用。wrk的命令行界面非常简洁核心参数就几个但组合起来能应对各种测试场景。我们先从一个最简单的命令开始然后逐步拆解每个参数的含义和背后的原理。3.1 第一个测试命令假设我们有一个本地正在运行的Web服务地址是http://127.0.0.1:8080。我们想用12个线程模拟400个并发连接持续压测30秒。命令如下wrk -t12 -c400 -d30s http://127.0.0.1:8080/index.html运行后你会看到类似下面的输出Running 30s test http://127.0.0.1:8080/index.html 12 threads and 400 connections Thread Stats Avg Stdev Max /- Stdev Latency 10.52ms 15.12ms 350.60ms 92.34% Req/Sec 3.48k 0.94k 5.50k 69.17% 1250347 requests in 30.10s, 1.21GB read Requests/sec: 41539.52 Transfer/sec: 41.23MB这个输出信息量很大我们一行行看。3.2 命令行参数深度解析现在我们来详细拆解上面用到的以及其它重要的参数-t, --threads N线程数作用指定要使用的工作线程数量。原理与选择wrk使用多线程模型每个线程独立运行一个事件循环使用epoll管理一批HTTP连接。理论上线程数设置为你机器CPU的物理核心数通常能获得最佳性能。你可以用nproc命令查看核心数。设置过多远超CPU核心数会导致大量的线程上下文切换反而降低性能。我个人的经验是从CPU核心数开始测试比如4核机器就用-t48核就用-t8然后可以微调观察变化。-c, --connections N总连接数作用指定测试期间要建立和保持的HTTP连接总数。原理与选择这些连接会被平均分配到各个线程。例如-t12 -c400意味着每个线程大约管理33个连接。这个参数直接决定了你模拟的并发用户数。注意这里指的是“并发连接数”不是“每秒请求数RPS”。一个连接上可以持续发送多个请求如果服务器支持HTTP Keep-Alive。这个数字不能超过你系统允许的临时端口范围通常通过sysctl net.ipv4.ip_local_port_range查看。对于高并发测试如-c5000可能需要调整系统参数如net.ipv4.ip_local_port_range,net.core.somaxconn等。-d, --duration T测试持续时间作用指定压测运行的时间长度。格式后面跟时间单位例如10s10秒2m2分钟1h1小时。对于性能基准测试我建议至少运行30s以上以便系统包括你的服务、操作系统网络栈度过最初的“热身”阶段获得一个相对稳定的性能视图。短时间如1-2秒的测试结果波动会非常大参考价值有限。-s, --script SLuaJIT脚本路径作用指定一个Lua脚本用于自定义请求生成、响应处理和报告。这是wrk最强大的功能之一我们会在后面专门用一节来讲。例如-s ./scripts/post.lua。-H, --header H添加HTTP头作用为每个请求添加一个HTTP头。可以多次使用此参数来添加多个头。示例-H “User-Agent: wrk/1.0” -H “Authorization: Bearer xyz123”。这对于测试需要认证或特定头的API接口至关重要。--latency打印详细的延迟分布作用如果启用wrk会在标准输出结束后额外打印一张详细的延迟百分比分布表例如50% 75% 90% 99%的请求延迟是多少。这对于分析服务响应时间的“长尾效应”非常有用。很多时候平均延迟看起来不错但99分位延迟可能很高这会影响用户体验。--timeout T超时时间作用如果在一个请求发出后超过此时间仍未收到响应则将该请求记录为超时。默认超时时间很长似乎是1分钟对于测试内部服务建议根据实际情况设置一个合理的值比如--timeout 2s。超时的请求会计入总请求数但会显著影响平均延迟等统计。-v或--version打印版本信息。3.3 结果报告解读指南理解wrk的输出报告是分析性能瓶颈的关键。我们再看一下之前的输出样例Running 30s test http://127.0.0.1:8080/index.html 12 threads and 400 connections第一行和第二行是测试配置的回顾确认你的参数设置。Thread Stats Avg Stdev Max /- Stdev Latency 10.52ms 15.12ms 350.60ms 92.34% Req/Sec 3.48k 0.94k 5.50k 69.17%这是线程统计部分展示了所有线程在测试期间性能指标的聚合情况。Latency延迟单位通常是毫秒(ms)或微秒(us)。Avg: 平均延迟。10.52ms。Stdev: 标准差。15.12ms。这个值越大说明延迟的波动越大响应时间越不稳定。Max: 最大延迟。350.60ms。偶尔出现的慢请求。/- Stdev: 有92.34%的请求延迟落在 (平均值 - 标准差) 到 (平均值 标准差) 的区间内。这个比例越高说明延迟分布越集中性能越稳定。Req/Sec每秒请求数每个线程每秒完成的请求数。Avg: 平均每秒请求数。3.48k。Stdev: 标准差。0.94k。Max: 峰值。5.50k。/- Stdev: 69.17%的采样点落在一个标准差范围内。这个值如果太低说明线程的请求处理速率波动剧烈可能是遇到了资源竞争或外部依赖不稳定。1250347 requests in 30.10s, 1.21GB read这是汇总信息1250347 requests in 30.10s在30.10秒内总共完成了约125万次请求。1.21GB read总共从服务器读取了1.21GB的数据。这个“读”指的是wrk作为客户端接收到的响应体数据总量。Requests/sec: 41539.52 Transfer/sec: 41.23MB这是最终的两个核心性能指标Requests/sec(RPS/QPS)每秒请求数这是衡量服务吞吐量的黄金指标。本例中为41539.52意味着服务平均每秒能处理4.1万个请求。Transfer/sec每秒数据传输量。本例中为41.23MB/s。这个指标可以帮助你判断网络带宽是否成为瓶颈。如果RPS上不去但Transfer/sec已经接近你的网络带宽上限那么瓶颈很可能在网络上。重要提示wrk默认使用HTTP Keep-Alive这意味着连接在单个请求后不会关闭而是被复用于后续请求。这会极大地提升RPS因为避免了频繁的TCP三次握手和慢启动。如果你想模拟每次请求都新建连接的行为例如测试负载均衡器或连接池需要在Lua脚本中操作或者使用不支持Keep-Alive的HTTP/1.0协议通过-H “Connection: close”实现。4. 进阶使用Lua脚本定制压测场景基础用法只能测试固定的URL和请求方法默认GET。但真实的业务场景复杂得多需要测试POST接口、传递JSON body、处理Cookie/Session、从文件中读取测试数据、或者根据响应内容动态决定下一个请求。这时-s参数配合Lua脚本就派上用场了。wrk内置了LuaJIT可以在压测运行时调用你写的Lua脚本来完全控制请求和响应的生命周期。4.1 Lua脚本的基本结构与生命周期一个有效的wrkLua脚本通常需要定义几个特定的函数wrk会在不同阶段调用它们。下面是一个脚本骨架-- 示例custom_benchmark.lua -- 初始化阶段每个线程只调用一次 function setup(thread) -- thread.addr 是线程的“用户数据”可以在这里为每个线程初始化变量 thread.addr thread_data_placeholder end -- 请求生成阶段每次要发送请求时调用 function request() -- 必须返回一个字符串代表完整的HTTP请求 -- 例如return “GET /api/v1/users HTTP/1.1\r\nHost: localhost\r\n\r\n” -- 更常见的做法是使用wrk提供的辅助函数构造请求 end -- 响应处理阶段每次收到响应时调用 function response(status, headers, body) -- status: HTTP状态码 (数字) -- headers: 响应头的Lua table -- body: 响应体 (字符串) -- 可以在这里检查状态码、解析JSON、统计错误等 end -- 测试结束阶段整个压测结束后调用一次 function done(summary, latency, requests) -- summary: 包含总请求数、持续时间、错误数等的table -- latency: 包含延迟统计信息最小值、最大值、平均值、百分位数等的table -- requests: 包含每秒请求数统计信息的table -- 可以在这里打印自定义的汇总报告或写入文件 end4.2 实战脚本示例测试POST JSON API假设我们要测试一个用户登录接口POST /api/login 要求JSON格式的body{“username”: “test”, “password”: “123456”} 并且需要设置Content-Type: application/json。我们可以编写如下脚本post_login.lua-- post_login.lua wrk.method “POST” wrk.headers[“Content-Type”] “application/json” wrk.headers[“User-Agent”] “wrk/benchmark” -- 请求体内容 local body_json ‘{“username”: “test”, “password”: “123456”}’ -- 每个请求都使用相同的body function request() return wrk.format(“POST”, “/api/login”, wrk.headers, body_json) end -- 简单的响应检查如果状态码不是200打印警告 function response(status, headers, body) if status ~ 200 then print(“[WARN] Non-200 response: “ .. status .. “, body: “ .. body) end end然后使用命令运行wrk -t4 -c100 -d20s -s ./post_login.lua http://your-api-server.com关键点解析wrk.format(method, path, headers, body)这是一个极其重要的辅助函数。它帮你正确地构造HTTP请求字符串包括自动计算Content-Length头如果你提供了body。务必使用这个函数来生成请求而不是自己手动拼接字符串很容易出错。全局变量wrk.method和wrk.headers你可以在脚本最外层设置默认的请求方法和头信息。在request()函数内部你可以覆盖或修改这些值来生成不同的请求。request()函数它必须返回一个完整的HTTP请求字符串。wrk.format的返回值正是这个字符串。4.3 更复杂的场景参数化与动态请求单一请求的测试价值有限。我们常常需要模拟不同用户登录、查询不同商品ID等。这需要从外部文件读取数据或者在运行时动态生成参数。示例从CSV文件读取用户名密码进行登录压测首先创建一个users.csv文件user1,pass1 user2,pass2 user3,pass3 ... (更多行)然后编写Lua脚本post_login_dynamic.lua-- post_login_dynamic.lua wrk.method “POST” wrk.headers[“Content-Type”] “application/json” -- 读取CSV文件到table中 local csv_file “users.csv” local users {} local index 1 -- 注意setup函数在每个线程初始化时调用 function setup(thread) -- 为每个线程打开并读取文件简单示例假设文件不大 local f io.open(csv_file, “r”) if f then for line in f:lines() do local username, password line:match(“([^,]),([^,])”) if username and password then table.insert(users, {usernameusername, passwordpassword}) end end io.close(f) print(“Thread loaded “ .. #users .. ” user credentials.”) else print(“[ERROR] Failed to open file: “ .. csv_file) -- 如果文件读取失败使用一个默认值 table.insert(users, {username“default”, password“default”}) end -- 为每个线程设置一个独立的计数器避免多线程竞争 thread.addr {counter 1} end function request() -- 获取当前线程的计数器 local t wrk.thread:get(“addr”) local idx t.counter -- 从users表中循环获取数据 local user users[(idx % #users) 1] t.counter idx 1 -- 构造JSON body local body string.format(‘{“username”: “%s”, “password”: “%s”}’, user.username, user.password) -- 生成请求 return wrk.format(nil, “/api/login”, wrk.headers, body) end function response(status, headers, body) if status ~ 200 then -- 可以将错误信息记录到线程局部变量在done中汇总 local t wrk.thread:get(“addr”) t.errors (t.errors or 0) 1 end end function done(summary, latency, requests) -- 这里可以汇总所有线程的错误计数需要更复杂的线程间通信本例简化 print(“\n Test Finished ”) print(string.format(“Total Requests: %d”, summary.requests)) print(string.format(“Duration: %.2f sec”, summary.duration)) print(string.format(“Errors (non-200): (需额外逻辑统计)”)) end这个脚本展示了几个进阶技巧线程局部数据 (thread.addr)在setup中初始化在request和response中通过wrk.thread:get(“addr”)获取。用于存储每个线程独立的状态如计数器、错误数避免多线程并发访问共享数据带来的问题。循环读取数据通过取模运算(idx % #users) 1实现数据的循环使用确保在长时间压测中不会很快用完测试数据。文件读取在setup阶段读取文件。注意如果文件非常大这种方式会占用较多内存。对于海量数据可以考虑流式读取或使用更复杂的分片逻辑。注意事项Lua脚本的执行是在性能关键路径上的。过于复杂的request()或response()函数比如在request()里做复杂的字符串拼接或加密计算会显著降低wrk能产生的最大压力因为CPU时间被Lua脚本占用了。脚本的目标是“生成请求”和“检查响应”而不是做繁重的计算。如果你需要极致的压力请求内容应尽可能简单。5. 性能调优与结果分析实战掌握了基本和进阶用法后我们最终的目标是得到有意义的性能数据并据此进行分析。这部分我会结合一个实际的测试案例分享从测试设计、执行到结果分析的完整流程和避坑经验。5.1 测试环境与目标确认假设我们有一个用户查询服务提供GET /api/user/{id}接口。我们想找出它在当前服务器配置下的性能极限最大RPS并观察在不同并发压力下的延迟表现。测试客户端环境一台独立的Ubuntu 22.04服务器4核8G内存千兆网卡。确保这台机器本身资源充足不会成为瓶颈。可以用top或htop命令监控wrk进程的CPU使用率如果接近100%说明wrk自身是瓶颈可能需要减少线程数或优化脚本。测试服务端环境待测服务部署在另一台服务器上。测试前需要明确服务的配置如Web服务器worker数、应用服务器线程池、数据库连接池等。明确目标寻找最大稳定RPS。观察在最大RPS下延迟特别是P99延迟是否在可接受范围内例如100ms。找出系统瓶颈CPU、内存、网络、磁盘I/O、数据库等。5.2 阶梯式压力测试直接用一个很高的并发数去压可能会直接把服务打挂或者因为触发了限流/熔断而得不到有效数据。更科学的方法是进行阶梯式压测Step Load Test。我们可以编写一个Shell脚本逐步增加并发连接数-c观察每个阶梯下的RPS和延迟变化。#!/bin/bash # 文件名step_load_test.sh SERVER_URL“http://10.0.1.100:8080/api/user/123” DURATION“60s” # 每个阶梯持续60秒让数据更稳定 THREADS4 # 根据客户端CPU核心数设置 echo “Starting step load test for $SERVER_URL” echo “” for connections in 50 100 200 400 800 1600 3200 do echo “” echo “ Testing with -t$THREADS -c$connections -d$DURATION” wrk -t$THREADS -c$connections -d$DURATION $SERVER_URL --latency sleep 10 # 每个阶梯之间休息10秒让服务端恢复平静 done echo “” echo “Step load test finished.”运行这个脚本bash ./step_load_test.sh。你会看到一系列输出分别对应不同并发数下的性能表现。5.3 结果分析与瓶颈定位收集到数据后我们需要制作一个表格来直观对比并发数 (c)平均延迟 (ms)P99延迟 (ms)RPS (Requests/sec)客户端CPU使用率服务端CPU使用率网络带宽 (MB/s)备注5012.545398030%60%3.2延迟低RPS线性增长10013.152762055%95%6.1服务端CPU接近饱和20025.8180775060%100%6.2RPS增长停滞延迟翻倍40089.4650450040%100%3.6延迟急剧上升RPS下降如何解读这个表格理想阶段50-100并发随着并发数增加RPS几乎线性增长延迟增加不明显。这说明系统资源充足能够有效处理更多请求。拐点出现200并发RPS不再增长稳定在~7750。平均延迟开始明显上升从13ms到26ms。P99延迟飙升得更厉害从52ms到180ms。同时服务端CPU达到100%。这是一个强烈的信号服务端CPU已经成为瓶颈。系统已经达到其最大处理能力更多的并发连接只会导致请求排队从而增加延迟。过载阶段400并发RPS不升反降延迟尤其是P99延迟变得非常高。这是因为系统严重过载大量的时间花在了上下文切换、内存争用和请求排队上有效处理能力反而下降。结论与行动该服务的最大稳定处理能力约为7700 RPS在200并发时。超过这个值用户体验会因高延迟而恶化。当前的瓶颈是服务端CPU。优化方向可以是垂直扩展升级服务器CPU。水平扩展增加服务实例前面加负载均衡器。应用优化分析应用代码性能热点使用Profiling工具优化算法减少不必要的计算。缓存引入缓存如Redis减少对数据库或复杂计算的依赖。其他常见瓶颈的排查线索内存不足观察服务端内存使用率。如果内存耗尽会导致Swap使用磁盘I/O暴增系统响应极其缓慢。wrk输出中的延迟会变得极高且不稳定。网络带宽瓶颈监控wrk输出的Transfer/sec以及服务端的网络流量iftop,nload。如果传输速率接近网络带宽上限如千兆网约125MB/s那么RPS将受限于此。数据库瓶颈如果服务严重依赖数据库在服务端CPU未打满时RPS就上不去且数据库服务器监控显示高CPU或高I/O等待那么瓶颈可能在数据库。需要优化查询、增加索引或读写分离。连接数限制如果并发数加到一定程度后出现大量连接错误或超时可能是服务端或中间件如Nginx的backlog、max connections等参数设置过低。需要调整系统内核参数net.core.somaxconn和应用服务器配置。5.4 使用--latency进行延迟深度分析在压测命令后加上--latency参数你会得到一份详细的延迟百分位数报告Latency Distribution (HdrHistogram - Recorded Latency) 50.000% 9.65ms 75.000% 11.23ms 90.000% 13.57ms 99.000% 25.89ms 99.900% 89.45ms 99.990% 120.32ms 99.999% 150.11ms 100.000% 150.11ms这份报告比简单的“平均延迟”有价值得多P50中位数一半的请求快于此值。它比平均值更能代表“典型”用户体验。P90/P95可以定义为你服务的“达标延迟”。例如要求95%的请求在50ms内完成。P99/P99.9反映“长尾”延迟。即使只有1%或0.1%的请求很慢也可能影响到关键用户或导致连锁故障如超时重试雪崩。优化P99延迟往往是性能调优的深水区可能涉及垃圾回收GC调优、慢查询治理、缓存穿透等问题。通过对比不同并发压力下的延迟分布表你可以清晰地看到随着压力增大延迟的恶化是从哪个百分位开始的这对于设定服务的SLA服务等级协议和容量规划至关重要。6. 常见问题与排查技巧实录在实际使用wrk的过程中你肯定会遇到各种各样的问题。下面我整理了一些最常见的情况和解决办法这些都是我踩过坑后总结出来的经验。6.1 连接错误与“Socket errors”运行wrk时你可能会在输出结果的最后看到类似这样的错误统计Non-2xx or 3xx responses: 0 Socket errors: connect 542, read 0, write 0, timeout 0connect错误这通常意味着wrk无法建立TCP连接。原因1端口耗尽。这是高并发测试-c值很大时最常见的原因。客户端操作系统可用的临时端口号是有限的通常约28000个。当wrk试图建立大量连接时端口很快被占满。排查运行netstat -an | grep TIME_WAIT | wc -l查看处于TIME_WAIT状态的连接数。如果非常多说明端口被快速回收的连接占用着。解决增加本地端口范围sudo sysctl -w net.ipv4.ip_local_port_range“1024 65535”。这大幅增加了可用端口数。启用端口快速回收和重用sudo sysctl -w net.ipv4.tcp_tw_reuse1 sudo sysctl -w net.ipv4.tcp_tw_recycle1 # 注意在较新内核中可能已废弃或行为有变建议先查文档 sudo sysctl -w net.ipv4.tcp_fin_timeout30 # 减少FIN_WAIT_2状态的超时时间减少测试并发数如果只是为了测试服务端极限不一定需要客户端模拟极端高的并发连接数。可以尝试用较少的连接数如-c1000但更长的测试时间。原因2服务端backlog队列满。服务端listensocket的未完成连接队列backlog太小无法应对瞬间的连接建立请求。解决调整服务端的backlog参数例如在Nginx中是listen 80 backlog65535;同时调整客户端和服务端的系统参数sudo sysctl -w net.core.somaxconn65535。read/write/timeout错误read错误连接建立后读取响应时出错如连接被对端重置。write错误发送请求时出错。timeout错误在--timeout指定的时间内未收到响应。通用排查步骤检查服务端日志看是否有大量错误如5xx状态码、超时或崩溃。检查服务端资源CPU、内存、数据库连接是否耗尽。检查网络是否有丢包、防火墙规则拦截。调整--timeout对于慢接口适当增加超时时间如--timeout 10s避免因超时误判为错误。6.2 压测结果不稳定RPS波动大现象多次运行相同的wrk命令得到的RPS相差很大。可能原因及解决服务端有缓存预热第一次请求慢后续请求快。确保在正式记录数据前先进行一轮“预热”压测例如运行wrk -t2 -c10 -d10s让服务端的JVM如果适用、数据库缓存等热起来。客户端资源竞争运行wrk的机器本身可能资源不足。确保wrk是唯一占用大量CPU/网络资源的进程。用top查看wrk进程的CPU占用如果达不到100%单核可能是有其他进程在干扰。测试时间太短-d参数设置得太小如1s或2s结果受随机因素影响大。务必延长测试时间我建议任何严肃的性能测试单次持续时间不应少于30秒最好在1分钟以上。外部依赖波动如果你的服务依赖数据库、缓存或其他微服务它们的性能波动会直接影响你的测试结果。尽量在隔离的环境中对单一服务进行测试或者确保外部依赖同样处于稳定状态。6.3 Lua脚本调试技巧Lua脚本写错了wrk可能只会报一个简单的语法错误或者直接静默失败请求不正常。调试起来比较麻烦。语法检查可以先使用luajit -b your_script.lua检查语法。但wrk内置的Lua环境可能和 standalone 的luajit有细微差别。打印调试在Lua脚本的关键位置使用print()函数输出信息。注意print会输出到标准错误并且在高并发下会产生巨量输出只适合在低并发、短时间的调试运行中使用。例如在setup函数里打印加载的数据量在response里打印异常状态码。wrk -t1 -c1 -d2s -s debug_script.lua http://... # 用最少的线程和连接调试简化请求如果怀疑是request()函数生成的请求有问题可以先把函数体简化直接返回一个最简单的固定请求看测试是否能正常进行。然后逐步添加复杂逻辑定位问题点。验证请求内容有时是请求格式不对。可以临时在request()函数里将wrk.format()生成的字符串打印出来复制到像curl或Postman这样的工具里手动发送看服务端是否正确响应。6.4 与其它工具的对比与选型你可能会问有了wrk还需要ab,jmeter,locust吗它们各有适用场景工具优点缺点适用场景wrk单机性能极高轻量支持Lua脚本定制结果清晰。不支持分布式压测脚本能力虽强但需编程无图形界面。单机HTTP/HTTPS服务极限性能摸底开发/运维快速验证CI/CD中的自动化性能测试。ab (ApacheBench)极其简单几乎所有系统都预装。性能一般单线程功能单一高并发测试能力弱。最快速的简单测试例如在本地验证一个页面是否能访问。JMeter功能极其强大支持各种协议图形化界面可分布式压测有完善的监听器和报告。重量级资源消耗大学习曲线陡峭图形界面在服务器环境不便。复杂场景的综合性能测试需要模拟多种协议、复杂业务流程、参数化、逻辑控制器等。Locust使用Python代码定义用户行为非常灵活支持分布式有Web UI实时监控。单机性能不如wrkPython解释器开销较大。需要模拟复杂用户行为模式思考时间、步骤跳转的场景且团队熟悉Python。个人建议对于后端开发者或运维wrk应该是你工具箱里的首选HTTP压测工具。它的学习成本低上手快能在几分钟内给你一个服务性能的基线数据。当需要更复杂的场景模拟或团队协作时再考虑JMeter或Locust。最后性能测试本身是一个需要严谨态度和反复迭代的过程。wrk给了你一把锋利的刀但如何设计测试用例、如何分析结果、如何定位瓶颈更需要你的经验和系统知识。每次压测前问自己几个问题测试目标是否明确测试环境是否干净、可控测试数据是否具有代表性观察的指标是否全面想清楚这些你从wrk那里得到的数据才会真正成为优化系统、保障稳定性的有力依据。
Ubuntu下wrk HTTP压测工具从安装到实战:性能调优与Lua脚本进阶
发布时间:2026/7/17 6:33:41
1. 项目概述最近在折腾一个Web服务的性能优化想看看接口在高并发下的表现到底怎么样。网上搜了一圈发现很多老手都在用wrk这个工具做HTTP压力测试。它不像JMeter那样需要复杂的图形界面也不像abApacheBench那样功能单一而是用C语言写的充分利用多核CPU一个命令就能打出很高的并发量特别适合我们这种在Linux服务器上做快速验证的场景。我的主力开发环境是Ubuntu 22.04 LTS所以这篇文章就详细记录下在Ubuntu Linux系统下从零开始安装wrk到上手使用再到用Lua脚本玩出花样的全过程。如果你也在找一款轻量级但威力巨大的HTTP压测工具那wrk绝对值得你花时间了解一下。简单来说wrk是一个现代HTTP基准测试工具。它最大的特点是“短平快”通过多线程配合epoll这样的高性能事件通知机制能在单台多核机器上模拟出巨大的HTTP连接压力。你可能会问为什么不用更出名的ab我实际对比过在相同的测试环境下wrk能更充分地榨干CPU资源尤其是线程模型和连接池的设计让它能轻松维持成千上万的并发连接而ab在高并发时很容易遇到端口耗尽或性能瓶颈。对于后端开发、运维或者任何需要评估Web服务性能边界的人来说掌握wrk就像手里多了一把精准的测压枪。2. 环境准备与wrk安装在Ubuntu上安装wrk通常有两种主流方法一种是直接使用系统包管理器安装预编译的版本这种方法最省事另一种是从源码编译安装这种方法能确保你获得最新的特性并且可以针对你的系统进行优化。我会把两种方法都走一遍并告诉你各自可能会遇到的坑。2.1 方法一使用APT包管理器安装最快捷对于大多数只想快速用起来的用户Ubuntu的官方仓库或一些PPA里其实已经包含了wrk。不过需要注意的是Ubuntu主仓库里的版本可能不是最新的。我们可以先尝试搜索一下sudo apt update apt search wrk如果搜索结果显示有wrk或wrk2一个社区维护的分支包那么可以直接安装sudo apt install wrk安装完成后通过wrk --version来验证是否成功。用APT安装的优点是依赖项如OpenSSL、LuaJIT会被自动处理好完全不需要你操心。但缺点也很明显版本可能比较旧。比如在Ubuntu 22.04上通过默认仓库安装的版本可能是一两年前的发布版会错过一些新的修复和功能。注意有些Ubuntu版本默认仓库里可能没有wrk。如果apt search找不到或者你希望使用最新版那么请直接跳到下面的源码编译方法。2.2 方法二从源码编译安装推荐获取最新版从源码编译是获取最新、最稳定wrk版本的最佳途径。这个过程需要一些基础的开发工具和库。第一步安装编译依赖我们需要安装C编译器、make工具以及关键的开发库。打开终端执行以下命令sudo apt update sudo apt install build-essential libssl-dev git -y这里解释一下这几个包的作用build-essential包含了gcc,g,make等编译套件是编译任何C/C项目的基础。libssl-devOpenSSL的开发库。wrk在进行HTTPS测试时需要用到SSL/TLS功能这个库提供了必要的头文件和链接库。git用于从GitHub克隆wrk的源代码仓库。第二步克隆源码并编译依赖装好后我们就可以获取wrk的源代码了。它的项目托管在GitHub上git clone https://github.com/wg/wrk.git cd wrk进入目录后直接使用make命令进行编译make如果一切顺利编译过程通常很快结束后会在当前目录下生成一个名为wrk的可执行文件。你可以把它移动到系统路径下方便在任何地方调用sudo cp wrk /usr/local/bin/第三步验证安装再次运行wrk --version如果能看到版本信息就说明从源码安装成功了。我比较推荐这种方式因为GitHub上的master分支通常包含了最新的提交修复了一些历史版本中可能存在的连接管理或统计显示的小问题。实操心得编译时可能遇到的坑关于LuaJITwrk的脚本功能依赖于LuaJIT。好消息是wrk的源码包里已经自带了一个LuaJIT在deps/目录下make时会自动编译它。所以你不需要单独安装luajit或libluajit-dev。如果编译失败并提示LuaJIT相关错误可以尝试先make clean再重新make。CPU架构优化make命令默认会使用通用的编译参数。如果你的服务器是特定的CPU架构比如AWS Graviton的ARM编译过程是兼容的但性能可能不是最优。对于生产环境的重度使用可以考虑研究一下针对特定CPU的CFLAGS优化选项不过对于绝大多数测试场景默认编译已经足够强大。权限问题将wrk复制到/usr/local/bin时需要sudo权限。你也可以选择不复制每次使用时通过./wrk来运行需要先cd到wrk目录或者把它添加到你的个人~/bin目录如果该目录已在PATH环境变量中。3. wrk核心参数与基础使用详解安装好之后我们直接来看怎么用。wrk的命令行界面非常简洁核心参数就几个但组合起来能应对各种测试场景。我们先从一个最简单的命令开始然后逐步拆解每个参数的含义和背后的原理。3.1 第一个测试命令假设我们有一个本地正在运行的Web服务地址是http://127.0.0.1:8080。我们想用12个线程模拟400个并发连接持续压测30秒。命令如下wrk -t12 -c400 -d30s http://127.0.0.1:8080/index.html运行后你会看到类似下面的输出Running 30s test http://127.0.0.1:8080/index.html 12 threads and 400 connections Thread Stats Avg Stdev Max /- Stdev Latency 10.52ms 15.12ms 350.60ms 92.34% Req/Sec 3.48k 0.94k 5.50k 69.17% 1250347 requests in 30.10s, 1.21GB read Requests/sec: 41539.52 Transfer/sec: 41.23MB这个输出信息量很大我们一行行看。3.2 命令行参数深度解析现在我们来详细拆解上面用到的以及其它重要的参数-t, --threads N线程数作用指定要使用的工作线程数量。原理与选择wrk使用多线程模型每个线程独立运行一个事件循环使用epoll管理一批HTTP连接。理论上线程数设置为你机器CPU的物理核心数通常能获得最佳性能。你可以用nproc命令查看核心数。设置过多远超CPU核心数会导致大量的线程上下文切换反而降低性能。我个人的经验是从CPU核心数开始测试比如4核机器就用-t48核就用-t8然后可以微调观察变化。-c, --connections N总连接数作用指定测试期间要建立和保持的HTTP连接总数。原理与选择这些连接会被平均分配到各个线程。例如-t12 -c400意味着每个线程大约管理33个连接。这个参数直接决定了你模拟的并发用户数。注意这里指的是“并发连接数”不是“每秒请求数RPS”。一个连接上可以持续发送多个请求如果服务器支持HTTP Keep-Alive。这个数字不能超过你系统允许的临时端口范围通常通过sysctl net.ipv4.ip_local_port_range查看。对于高并发测试如-c5000可能需要调整系统参数如net.ipv4.ip_local_port_range,net.core.somaxconn等。-d, --duration T测试持续时间作用指定压测运行的时间长度。格式后面跟时间单位例如10s10秒2m2分钟1h1小时。对于性能基准测试我建议至少运行30s以上以便系统包括你的服务、操作系统网络栈度过最初的“热身”阶段获得一个相对稳定的性能视图。短时间如1-2秒的测试结果波动会非常大参考价值有限。-s, --script SLuaJIT脚本路径作用指定一个Lua脚本用于自定义请求生成、响应处理和报告。这是wrk最强大的功能之一我们会在后面专门用一节来讲。例如-s ./scripts/post.lua。-H, --header H添加HTTP头作用为每个请求添加一个HTTP头。可以多次使用此参数来添加多个头。示例-H “User-Agent: wrk/1.0” -H “Authorization: Bearer xyz123”。这对于测试需要认证或特定头的API接口至关重要。--latency打印详细的延迟分布作用如果启用wrk会在标准输出结束后额外打印一张详细的延迟百分比分布表例如50% 75% 90% 99%的请求延迟是多少。这对于分析服务响应时间的“长尾效应”非常有用。很多时候平均延迟看起来不错但99分位延迟可能很高这会影响用户体验。--timeout T超时时间作用如果在一个请求发出后超过此时间仍未收到响应则将该请求记录为超时。默认超时时间很长似乎是1分钟对于测试内部服务建议根据实际情况设置一个合理的值比如--timeout 2s。超时的请求会计入总请求数但会显著影响平均延迟等统计。-v或--version打印版本信息。3.3 结果报告解读指南理解wrk的输出报告是分析性能瓶颈的关键。我们再看一下之前的输出样例Running 30s test http://127.0.0.1:8080/index.html 12 threads and 400 connections第一行和第二行是测试配置的回顾确认你的参数设置。Thread Stats Avg Stdev Max /- Stdev Latency 10.52ms 15.12ms 350.60ms 92.34% Req/Sec 3.48k 0.94k 5.50k 69.17%这是线程统计部分展示了所有线程在测试期间性能指标的聚合情况。Latency延迟单位通常是毫秒(ms)或微秒(us)。Avg: 平均延迟。10.52ms。Stdev: 标准差。15.12ms。这个值越大说明延迟的波动越大响应时间越不稳定。Max: 最大延迟。350.60ms。偶尔出现的慢请求。/- Stdev: 有92.34%的请求延迟落在 (平均值 - 标准差) 到 (平均值 标准差) 的区间内。这个比例越高说明延迟分布越集中性能越稳定。Req/Sec每秒请求数每个线程每秒完成的请求数。Avg: 平均每秒请求数。3.48k。Stdev: 标准差。0.94k。Max: 峰值。5.50k。/- Stdev: 69.17%的采样点落在一个标准差范围内。这个值如果太低说明线程的请求处理速率波动剧烈可能是遇到了资源竞争或外部依赖不稳定。1250347 requests in 30.10s, 1.21GB read这是汇总信息1250347 requests in 30.10s在30.10秒内总共完成了约125万次请求。1.21GB read总共从服务器读取了1.21GB的数据。这个“读”指的是wrk作为客户端接收到的响应体数据总量。Requests/sec: 41539.52 Transfer/sec: 41.23MB这是最终的两个核心性能指标Requests/sec(RPS/QPS)每秒请求数这是衡量服务吞吐量的黄金指标。本例中为41539.52意味着服务平均每秒能处理4.1万个请求。Transfer/sec每秒数据传输量。本例中为41.23MB/s。这个指标可以帮助你判断网络带宽是否成为瓶颈。如果RPS上不去但Transfer/sec已经接近你的网络带宽上限那么瓶颈很可能在网络上。重要提示wrk默认使用HTTP Keep-Alive这意味着连接在单个请求后不会关闭而是被复用于后续请求。这会极大地提升RPS因为避免了频繁的TCP三次握手和慢启动。如果你想模拟每次请求都新建连接的行为例如测试负载均衡器或连接池需要在Lua脚本中操作或者使用不支持Keep-Alive的HTTP/1.0协议通过-H “Connection: close”实现。4. 进阶使用Lua脚本定制压测场景基础用法只能测试固定的URL和请求方法默认GET。但真实的业务场景复杂得多需要测试POST接口、传递JSON body、处理Cookie/Session、从文件中读取测试数据、或者根据响应内容动态决定下一个请求。这时-s参数配合Lua脚本就派上用场了。wrk内置了LuaJIT可以在压测运行时调用你写的Lua脚本来完全控制请求和响应的生命周期。4.1 Lua脚本的基本结构与生命周期一个有效的wrkLua脚本通常需要定义几个特定的函数wrk会在不同阶段调用它们。下面是一个脚本骨架-- 示例custom_benchmark.lua -- 初始化阶段每个线程只调用一次 function setup(thread) -- thread.addr 是线程的“用户数据”可以在这里为每个线程初始化变量 thread.addr thread_data_placeholder end -- 请求生成阶段每次要发送请求时调用 function request() -- 必须返回一个字符串代表完整的HTTP请求 -- 例如return “GET /api/v1/users HTTP/1.1\r\nHost: localhost\r\n\r\n” -- 更常见的做法是使用wrk提供的辅助函数构造请求 end -- 响应处理阶段每次收到响应时调用 function response(status, headers, body) -- status: HTTP状态码 (数字) -- headers: 响应头的Lua table -- body: 响应体 (字符串) -- 可以在这里检查状态码、解析JSON、统计错误等 end -- 测试结束阶段整个压测结束后调用一次 function done(summary, latency, requests) -- summary: 包含总请求数、持续时间、错误数等的table -- latency: 包含延迟统计信息最小值、最大值、平均值、百分位数等的table -- requests: 包含每秒请求数统计信息的table -- 可以在这里打印自定义的汇总报告或写入文件 end4.2 实战脚本示例测试POST JSON API假设我们要测试一个用户登录接口POST /api/login 要求JSON格式的body{“username”: “test”, “password”: “123456”} 并且需要设置Content-Type: application/json。我们可以编写如下脚本post_login.lua-- post_login.lua wrk.method “POST” wrk.headers[“Content-Type”] “application/json” wrk.headers[“User-Agent”] “wrk/benchmark” -- 请求体内容 local body_json ‘{“username”: “test”, “password”: “123456”}’ -- 每个请求都使用相同的body function request() return wrk.format(“POST”, “/api/login”, wrk.headers, body_json) end -- 简单的响应检查如果状态码不是200打印警告 function response(status, headers, body) if status ~ 200 then print(“[WARN] Non-200 response: “ .. status .. “, body: “ .. body) end end然后使用命令运行wrk -t4 -c100 -d20s -s ./post_login.lua http://your-api-server.com关键点解析wrk.format(method, path, headers, body)这是一个极其重要的辅助函数。它帮你正确地构造HTTP请求字符串包括自动计算Content-Length头如果你提供了body。务必使用这个函数来生成请求而不是自己手动拼接字符串很容易出错。全局变量wrk.method和wrk.headers你可以在脚本最外层设置默认的请求方法和头信息。在request()函数内部你可以覆盖或修改这些值来生成不同的请求。request()函数它必须返回一个完整的HTTP请求字符串。wrk.format的返回值正是这个字符串。4.3 更复杂的场景参数化与动态请求单一请求的测试价值有限。我们常常需要模拟不同用户登录、查询不同商品ID等。这需要从外部文件读取数据或者在运行时动态生成参数。示例从CSV文件读取用户名密码进行登录压测首先创建一个users.csv文件user1,pass1 user2,pass2 user3,pass3 ... (更多行)然后编写Lua脚本post_login_dynamic.lua-- post_login_dynamic.lua wrk.method “POST” wrk.headers[“Content-Type”] “application/json” -- 读取CSV文件到table中 local csv_file “users.csv” local users {} local index 1 -- 注意setup函数在每个线程初始化时调用 function setup(thread) -- 为每个线程打开并读取文件简单示例假设文件不大 local f io.open(csv_file, “r”) if f then for line in f:lines() do local username, password line:match(“([^,]),([^,])”) if username and password then table.insert(users, {usernameusername, passwordpassword}) end end io.close(f) print(“Thread loaded “ .. #users .. ” user credentials.”) else print(“[ERROR] Failed to open file: “ .. csv_file) -- 如果文件读取失败使用一个默认值 table.insert(users, {username“default”, password“default”}) end -- 为每个线程设置一个独立的计数器避免多线程竞争 thread.addr {counter 1} end function request() -- 获取当前线程的计数器 local t wrk.thread:get(“addr”) local idx t.counter -- 从users表中循环获取数据 local user users[(idx % #users) 1] t.counter idx 1 -- 构造JSON body local body string.format(‘{“username”: “%s”, “password”: “%s”}’, user.username, user.password) -- 生成请求 return wrk.format(nil, “/api/login”, wrk.headers, body) end function response(status, headers, body) if status ~ 200 then -- 可以将错误信息记录到线程局部变量在done中汇总 local t wrk.thread:get(“addr”) t.errors (t.errors or 0) 1 end end function done(summary, latency, requests) -- 这里可以汇总所有线程的错误计数需要更复杂的线程间通信本例简化 print(“\n Test Finished ”) print(string.format(“Total Requests: %d”, summary.requests)) print(string.format(“Duration: %.2f sec”, summary.duration)) print(string.format(“Errors (non-200): (需额外逻辑统计)”)) end这个脚本展示了几个进阶技巧线程局部数据 (thread.addr)在setup中初始化在request和response中通过wrk.thread:get(“addr”)获取。用于存储每个线程独立的状态如计数器、错误数避免多线程并发访问共享数据带来的问题。循环读取数据通过取模运算(idx % #users) 1实现数据的循环使用确保在长时间压测中不会很快用完测试数据。文件读取在setup阶段读取文件。注意如果文件非常大这种方式会占用较多内存。对于海量数据可以考虑流式读取或使用更复杂的分片逻辑。注意事项Lua脚本的执行是在性能关键路径上的。过于复杂的request()或response()函数比如在request()里做复杂的字符串拼接或加密计算会显著降低wrk能产生的最大压力因为CPU时间被Lua脚本占用了。脚本的目标是“生成请求”和“检查响应”而不是做繁重的计算。如果你需要极致的压力请求内容应尽可能简单。5. 性能调优与结果分析实战掌握了基本和进阶用法后我们最终的目标是得到有意义的性能数据并据此进行分析。这部分我会结合一个实际的测试案例分享从测试设计、执行到结果分析的完整流程和避坑经验。5.1 测试环境与目标确认假设我们有一个用户查询服务提供GET /api/user/{id}接口。我们想找出它在当前服务器配置下的性能极限最大RPS并观察在不同并发压力下的延迟表现。测试客户端环境一台独立的Ubuntu 22.04服务器4核8G内存千兆网卡。确保这台机器本身资源充足不会成为瓶颈。可以用top或htop命令监控wrk进程的CPU使用率如果接近100%说明wrk自身是瓶颈可能需要减少线程数或优化脚本。测试服务端环境待测服务部署在另一台服务器上。测试前需要明确服务的配置如Web服务器worker数、应用服务器线程池、数据库连接池等。明确目标寻找最大稳定RPS。观察在最大RPS下延迟特别是P99延迟是否在可接受范围内例如100ms。找出系统瓶颈CPU、内存、网络、磁盘I/O、数据库等。5.2 阶梯式压力测试直接用一个很高的并发数去压可能会直接把服务打挂或者因为触发了限流/熔断而得不到有效数据。更科学的方法是进行阶梯式压测Step Load Test。我们可以编写一个Shell脚本逐步增加并发连接数-c观察每个阶梯下的RPS和延迟变化。#!/bin/bash # 文件名step_load_test.sh SERVER_URL“http://10.0.1.100:8080/api/user/123” DURATION“60s” # 每个阶梯持续60秒让数据更稳定 THREADS4 # 根据客户端CPU核心数设置 echo “Starting step load test for $SERVER_URL” echo “” for connections in 50 100 200 400 800 1600 3200 do echo “” echo “ Testing with -t$THREADS -c$connections -d$DURATION” wrk -t$THREADS -c$connections -d$DURATION $SERVER_URL --latency sleep 10 # 每个阶梯之间休息10秒让服务端恢复平静 done echo “” echo “Step load test finished.”运行这个脚本bash ./step_load_test.sh。你会看到一系列输出分别对应不同并发数下的性能表现。5.3 结果分析与瓶颈定位收集到数据后我们需要制作一个表格来直观对比并发数 (c)平均延迟 (ms)P99延迟 (ms)RPS (Requests/sec)客户端CPU使用率服务端CPU使用率网络带宽 (MB/s)备注5012.545398030%60%3.2延迟低RPS线性增长10013.152762055%95%6.1服务端CPU接近饱和20025.8180775060%100%6.2RPS增长停滞延迟翻倍40089.4650450040%100%3.6延迟急剧上升RPS下降如何解读这个表格理想阶段50-100并发随着并发数增加RPS几乎线性增长延迟增加不明显。这说明系统资源充足能够有效处理更多请求。拐点出现200并发RPS不再增长稳定在~7750。平均延迟开始明显上升从13ms到26ms。P99延迟飙升得更厉害从52ms到180ms。同时服务端CPU达到100%。这是一个强烈的信号服务端CPU已经成为瓶颈。系统已经达到其最大处理能力更多的并发连接只会导致请求排队从而增加延迟。过载阶段400并发RPS不升反降延迟尤其是P99延迟变得非常高。这是因为系统严重过载大量的时间花在了上下文切换、内存争用和请求排队上有效处理能力反而下降。结论与行动该服务的最大稳定处理能力约为7700 RPS在200并发时。超过这个值用户体验会因高延迟而恶化。当前的瓶颈是服务端CPU。优化方向可以是垂直扩展升级服务器CPU。水平扩展增加服务实例前面加负载均衡器。应用优化分析应用代码性能热点使用Profiling工具优化算法减少不必要的计算。缓存引入缓存如Redis减少对数据库或复杂计算的依赖。其他常见瓶颈的排查线索内存不足观察服务端内存使用率。如果内存耗尽会导致Swap使用磁盘I/O暴增系统响应极其缓慢。wrk输出中的延迟会变得极高且不稳定。网络带宽瓶颈监控wrk输出的Transfer/sec以及服务端的网络流量iftop,nload。如果传输速率接近网络带宽上限如千兆网约125MB/s那么RPS将受限于此。数据库瓶颈如果服务严重依赖数据库在服务端CPU未打满时RPS就上不去且数据库服务器监控显示高CPU或高I/O等待那么瓶颈可能在数据库。需要优化查询、增加索引或读写分离。连接数限制如果并发数加到一定程度后出现大量连接错误或超时可能是服务端或中间件如Nginx的backlog、max connections等参数设置过低。需要调整系统内核参数net.core.somaxconn和应用服务器配置。5.4 使用--latency进行延迟深度分析在压测命令后加上--latency参数你会得到一份详细的延迟百分位数报告Latency Distribution (HdrHistogram - Recorded Latency) 50.000% 9.65ms 75.000% 11.23ms 90.000% 13.57ms 99.000% 25.89ms 99.900% 89.45ms 99.990% 120.32ms 99.999% 150.11ms 100.000% 150.11ms这份报告比简单的“平均延迟”有价值得多P50中位数一半的请求快于此值。它比平均值更能代表“典型”用户体验。P90/P95可以定义为你服务的“达标延迟”。例如要求95%的请求在50ms内完成。P99/P99.9反映“长尾”延迟。即使只有1%或0.1%的请求很慢也可能影响到关键用户或导致连锁故障如超时重试雪崩。优化P99延迟往往是性能调优的深水区可能涉及垃圾回收GC调优、慢查询治理、缓存穿透等问题。通过对比不同并发压力下的延迟分布表你可以清晰地看到随着压力增大延迟的恶化是从哪个百分位开始的这对于设定服务的SLA服务等级协议和容量规划至关重要。6. 常见问题与排查技巧实录在实际使用wrk的过程中你肯定会遇到各种各样的问题。下面我整理了一些最常见的情况和解决办法这些都是我踩过坑后总结出来的经验。6.1 连接错误与“Socket errors”运行wrk时你可能会在输出结果的最后看到类似这样的错误统计Non-2xx or 3xx responses: 0 Socket errors: connect 542, read 0, write 0, timeout 0connect错误这通常意味着wrk无法建立TCP连接。原因1端口耗尽。这是高并发测试-c值很大时最常见的原因。客户端操作系统可用的临时端口号是有限的通常约28000个。当wrk试图建立大量连接时端口很快被占满。排查运行netstat -an | grep TIME_WAIT | wc -l查看处于TIME_WAIT状态的连接数。如果非常多说明端口被快速回收的连接占用着。解决增加本地端口范围sudo sysctl -w net.ipv4.ip_local_port_range“1024 65535”。这大幅增加了可用端口数。启用端口快速回收和重用sudo sysctl -w net.ipv4.tcp_tw_reuse1 sudo sysctl -w net.ipv4.tcp_tw_recycle1 # 注意在较新内核中可能已废弃或行为有变建议先查文档 sudo sysctl -w net.ipv4.tcp_fin_timeout30 # 减少FIN_WAIT_2状态的超时时间减少测试并发数如果只是为了测试服务端极限不一定需要客户端模拟极端高的并发连接数。可以尝试用较少的连接数如-c1000但更长的测试时间。原因2服务端backlog队列满。服务端listensocket的未完成连接队列backlog太小无法应对瞬间的连接建立请求。解决调整服务端的backlog参数例如在Nginx中是listen 80 backlog65535;同时调整客户端和服务端的系统参数sudo sysctl -w net.core.somaxconn65535。read/write/timeout错误read错误连接建立后读取响应时出错如连接被对端重置。write错误发送请求时出错。timeout错误在--timeout指定的时间内未收到响应。通用排查步骤检查服务端日志看是否有大量错误如5xx状态码、超时或崩溃。检查服务端资源CPU、内存、数据库连接是否耗尽。检查网络是否有丢包、防火墙规则拦截。调整--timeout对于慢接口适当增加超时时间如--timeout 10s避免因超时误判为错误。6.2 压测结果不稳定RPS波动大现象多次运行相同的wrk命令得到的RPS相差很大。可能原因及解决服务端有缓存预热第一次请求慢后续请求快。确保在正式记录数据前先进行一轮“预热”压测例如运行wrk -t2 -c10 -d10s让服务端的JVM如果适用、数据库缓存等热起来。客户端资源竞争运行wrk的机器本身可能资源不足。确保wrk是唯一占用大量CPU/网络资源的进程。用top查看wrk进程的CPU占用如果达不到100%单核可能是有其他进程在干扰。测试时间太短-d参数设置得太小如1s或2s结果受随机因素影响大。务必延长测试时间我建议任何严肃的性能测试单次持续时间不应少于30秒最好在1分钟以上。外部依赖波动如果你的服务依赖数据库、缓存或其他微服务它们的性能波动会直接影响你的测试结果。尽量在隔离的环境中对单一服务进行测试或者确保外部依赖同样处于稳定状态。6.3 Lua脚本调试技巧Lua脚本写错了wrk可能只会报一个简单的语法错误或者直接静默失败请求不正常。调试起来比较麻烦。语法检查可以先使用luajit -b your_script.lua检查语法。但wrk内置的Lua环境可能和 standalone 的luajit有细微差别。打印调试在Lua脚本的关键位置使用print()函数输出信息。注意print会输出到标准错误并且在高并发下会产生巨量输出只适合在低并发、短时间的调试运行中使用。例如在setup函数里打印加载的数据量在response里打印异常状态码。wrk -t1 -c1 -d2s -s debug_script.lua http://... # 用最少的线程和连接调试简化请求如果怀疑是request()函数生成的请求有问题可以先把函数体简化直接返回一个最简单的固定请求看测试是否能正常进行。然后逐步添加复杂逻辑定位问题点。验证请求内容有时是请求格式不对。可以临时在request()函数里将wrk.format()生成的字符串打印出来复制到像curl或Postman这样的工具里手动发送看服务端是否正确响应。6.4 与其它工具的对比与选型你可能会问有了wrk还需要ab,jmeter,locust吗它们各有适用场景工具优点缺点适用场景wrk单机性能极高轻量支持Lua脚本定制结果清晰。不支持分布式压测脚本能力虽强但需编程无图形界面。单机HTTP/HTTPS服务极限性能摸底开发/运维快速验证CI/CD中的自动化性能测试。ab (ApacheBench)极其简单几乎所有系统都预装。性能一般单线程功能单一高并发测试能力弱。最快速的简单测试例如在本地验证一个页面是否能访问。JMeter功能极其强大支持各种协议图形化界面可分布式压测有完善的监听器和报告。重量级资源消耗大学习曲线陡峭图形界面在服务器环境不便。复杂场景的综合性能测试需要模拟多种协议、复杂业务流程、参数化、逻辑控制器等。Locust使用Python代码定义用户行为非常灵活支持分布式有Web UI实时监控。单机性能不如wrkPython解释器开销较大。需要模拟复杂用户行为模式思考时间、步骤跳转的场景且团队熟悉Python。个人建议对于后端开发者或运维wrk应该是你工具箱里的首选HTTP压测工具。它的学习成本低上手快能在几分钟内给你一个服务性能的基线数据。当需要更复杂的场景模拟或团队协作时再考虑JMeter或Locust。最后性能测试本身是一个需要严谨态度和反复迭代的过程。wrk给了你一把锋利的刀但如何设计测试用例、如何分析结果、如何定位瓶颈更需要你的经验和系统知识。每次压测前问自己几个问题测试目标是否明确测试环境是否干净、可控测试数据是否具有代表性观察的指标是否全面想清楚这些你从wrk那里得到的数据才会真正成为优化系统、保障稳定性的有力依据。