☰
接口 QPS 与吞吐量自测:压测工具、系统拐点与容量评估
2026/10/1 9:31:48 网站建设 项目流程

接口上线前的容量评估,我见过太多团队是靠拍脑袋蒙过去的。开发说"我这接口很快,单机扛个几千 QPS 没问题",测试说"我拿 Postman 点了两下,响应挺快的",然后上线当天流量一冲,服务直接雪崩。问题的根子在于,很多人从头到尾就没弄清楚自己负责的接口到底能扛多少 QPS、吞吐量天花板在哪个位置、从第几个并发开始性能开始塌。自测接口的 QPS 和最大吞吐量,本质上是一次小型容量实验:你得知道测什么、怎么测、测出来的数字怎么读、以及为什么这个数字明天可能就不作数了。这篇文章适合后端开发、测试、运维以及任何需要给接口"报个数"的人,从概念到脚本到数据解读,我会把我自己踩过的坑和常用的手法都摊开讲。

1. 概念没理清,压测全白做

在动手之前,先把 QPS、吞吐量这几个词的真实边界划清楚。很多争议其实不是技术问题,而是双方说的根本不是同一个东西。

1.1 QPS、吞吐量、并发、响应时间各自指什么

QPS 是 Queries Per Second 的缩写,直译就是每秒查询数,落到接口这个场景,通常指这个接口每秒能处理完多少个请求,注意是"处理完",不是"收到"。有些监控把进入服务端的请求数除以时间就叫 QPS,那叫到达速率,和真正处理完成的速率中间可能隔着一个队列。

吞吐量是个更宽的概念,它可以按请求数算(那就和 QPS 数值上重合),也可以按字节算,比如每秒吞吐多少 MB。当你的接口返回的是大 JSON、图片或者文件流时,光看 QPS 会失真,因为同样 1000 QPS,返回 1KB 和 1MB,网络和内存的压力完全不是一个量级。

并发数是指同一时刻正在处理中的请求数量,响应时间是单个请求从发出到收到完整响应所花的时间。这四个指标不是孤立的,它们之间有一个非常实用的换算关系。

1.2 把四个指标串起来的利特尔法则

利特尔法则(Little's Law)原本是排队论里的结论,搬到接口性能上同样成立:

并发数 = QPS × 平均响应时间(单位统一为秒)

举个具体的例子。假设你希望接口跑到 1000 QPS,实测平均响应时间是 50ms,也就是 0.05 秒,那么需要的并发大约是 1000 × 0.05 = 50。反过来说,你用 50 个并发去压,如果响应时间没变,理论上能压出 1000 QPS;如果响应时间涨到 200ms,QPS 就会掉到 250 左右,因为同样 50 个并发,每个请求占用连接的时间变长了四倍。

这个公式最大的价值在于帮你做交叉验证。压测报告里如果出现"200 并发,QPS 5000,平均响应时间 400ms",代进去算一下:200 ÷ 0.4 = 500,和 5000 差了一个数量级,那这个数据里必然有错,要么并发统计方式不对,要么响应时间统计口径有问题。

1.3 定指标之前必须先问清楚的三件事

第一件事是业务峰值模型。电商大促是短时间脉冲式,后台批处理是长时间平稳,两者对"最大吞吐量"的定义完全不同。脉冲式要关注瞬时峰值和恢复能力,平稳型要关注长时间运行的稳定性,比如有没有内存泄漏。

第二件事是请求成本是否一致。同一个接口,带缓存命中和回源查库的耗时可能差十倍。如果你压测用的是固定参数,很可能全部命中缓存,测出来的数字漂亮但没有意义。真实流量里参数是散的,得让压测数据也散起来。

第三件事是可接受的错误率和延迟红线。没有任何系统能在无限压力下保持零错误,"最大吞吐量"通常定义为错误率开始稳定超过某个阈值(比如 0.1%),或者 P99 延迟越过红线(比如 500ms)之前的那个点。这个红线得提前和业务方对齐,不然压出来的数字没人认。

2. 压测工具怎么选,别一上来就抬重炮

工具选错,要么压不上去,要么压出来的数不可信。工具本身没有绝对的好坏,只有场景匹配度。

2.1 主流工具的能力对照

我把自己常用和见过的工具整理成一张表,方便快速判断。

工具脚本语言协议支持单机压测能力上手成本典型场景
curl无HTTP 等约等于 1极低单次探针、验证接口通不通
ab无HTTP中(单线程模型,受限于 CPU 单核)低快速冒烟、粗略看吞吐
hey无HTTP中低ab 的现代替代品,支持并发和 QPS 限速
wrkLuaHTTP/HTTPS高(多线程 + 事件驱动)中追求极限 QPS、需要脚本定制
k6JavaScriptHTTP/1.1、HTTP/2、gRPC、WebSocket高中云原生、CI 集成、场景化脚本
LocustPythonHTTP 为主中高(可分布式)中复杂业务链路、需要分布式扩展
JMeterGUI + 少量代码几乎全协议中(GUI 模式笨重,建议命令行)高多协议混合、企业存量资产

选型上我的经验是:单接口极限 QPS 用 wrk 或 k6,多接口业务链路用 Locust 或 JMeter。ab 和 hey 适合五分钟出个粗略数字,但别拿它们的结果去写正式的容量报告,因为它们的客户端模型太简单,容易自己先成为瓶颈。

2.2 选型时容易被忽略的三个维度

第一个维度是连接复用能力。你的接口如果支持 keep-alive,压测工具就必须开启长连接,否则压测脚本会把大量时间花在 TCP 三次握手和四次挥手上。wrk 默认就是长连接,ab 需要手动加-k,JMeter 要在 HTTP 请求默认值里勾选 keep-alive。

第二个维度是结果输出的可解析性。做容量基线是要留档的,最好选能输出结构化结果(JSON、CSV)的工具,方便后续做曲线。k6 的--out json、Locust 的 CSV 都很好用,JMeter 的 jtl 也可以,但 wrk 的默认输出只有文本,需要自己解析。

第三个维度是是否支持压测过程中的动态行为。真实流量不会一直匀速,会有突发。k6 的 stages 和 Locust 的 LoadTestShape 都能模拟阶梯、尖峰、波浪等各种压力曲线,wrk 靠 Lua 脚本也能做简单的限速,但表达力弱一些。

2.3 压测环境准备的检查清单

环境没准备好,压出来的数字全是噪声。下面这份清单我每次都会过一遍:

  • 压测客户端和服务端不要在同一台机器上,除非你就是想测同机极限(那结论也只能用于同机场景)。
  • 压测机自己要先调优:ulimit -n调到 65535 以上,net.ipv4.ip_local_port_range改成1024 65535,net.ipv4.tcp_tw_reuse打开,否则单机并发过千就会因为端口耗尽报错。
  • 被测服务要提前预热。Java 服务的 JIT 编译、连接池初始化、缓存加载都需要时间,冷启动阶段测出的前几十秒数据不能算数,我一般会先跑个 10% 压力预热 2 到 3 分钟再正式开始。
  • 关掉不相关的后台任务,比如日志采集、定时任务、监控 Agent 的高频上报,这些会偷走 CPU。
  • 统一时间基准,客户端和服务端做一次 NTP 对时,不然跨机器统计的时间戳没法和延迟对不上。

注意:千万不要在生产环境做破坏性压测。如果确实要在生产验证,只做限量的、带熔断保护的探测,并且提前知会相关方。

3. 手把手做一次可信的 QPS 压测

理论说够了,下面是完整的实操过程。我会用 wrk 做主演示,因为这个工具能压出比较真实的极限数字,同时给出 k6 的等价写法。

3.1 压测脚本怎么写才不会被客户端拖累

先用最简单的命令行模式压一下:

wrk -t8 -c200 -d60s --latency \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ http://target-host:8080/api/v1/query?id=1001

参数含义:-t8是 8 个线程,-c200是 200 个并发连接,-d60s持续 60 秒,--latency输出延迟分布。线程数一般设成压测机 CPU 核数,连接数按目标并发来。

但这只是固定参数的压法,容易被缓存欺骗。更接近真实的是用 Lua 脚本做参数随机化:

-- query.lua local counter = 1 local threads = {} function setup(thread) thread:set("id", counter) table.insert(threads, thread) counter = counter + 1 end request = function() local uid = math.random(1, 1000000) local path = "/api/v1/query?id=" .. uid return wrk.format("GET", path) end done = function(summary, latency, requests) io.write("---- done ----\n") end

运行方式变成wrk -t8 -c200 -d60s -s query.lua http://target-host:8080。这里的math.random让每次请求的 id 都不同,能有效避开简单的本地缓存和 CDN 缓存,测出的才是后端真实计算成本。

如果用 k6,等价脚本长这样:

import http from 'k6/http'; import { check } from 'k6'; export const options = { stages: [ { duration: '2m', target: 50 }, { duration: '3m', target: 200 }, { duration: '3m', target: 500 }, { duration: '2m', target: 0 }, ], thresholds: { http_req_failed: ['rate<0.001'], http_req_duration: ['p(99)<500'], }, }; export default function () { const uid = Math.floor(Math.random() * 1000000); const res = http.get(`http://target-host:8080/api/v1/query?id=${uid}`); check(res, { 'status is 200': (r) => r.status === 200 }); }

k6 的 stages 天然支持压力梯度,而且 thresholds 能在跑的过程中就给出"是否达标"的判定,非常适合放进流水线做回归。

3.2 压力梯度怎么设计才靠谱

不要一上来就 200 并发。我的做法是阶梯式上探,具体档位根据接口预期来定,一般从预估峰值的 20% 起步。假设你预估这个接口峰值 1000 QPS,可以这样排:

阶段并发持续时长观察目的
预热202 分钟JIT 完成、连接池预热、缓存填充
冒烟503 分钟确认基本可用、拿到基线延迟
爬坡100 / 200各 3 分钟观察延迟是否线性增长
逼近400 / 600各 3 分钟寻找拐点前兆
探顶800 / 1000各 3 分钟定位拐点、观察错误率
拉满1500 起2 分钟看失败模式(快速失败还是拖死)

每一档之间留 30 秒的间隔,让系统有个短暂的"喘气"时间,观察恢复情况。如果某一档之后延迟没有回落、线程数没有下降、CPU 没有回落,那可能出现了队列积压,需要单独记录。

为什么每个档位要至少 3 分钟?因为很多性能问题在短时间内是看不出来的,比如连接池耗尽、GC 长暂停、磁盘 IO 打满,这些现象往往在持续压力下经过一两分钟才显现。3 分钟是个经验值,能覆盖大多数抖动周期。

3.3 压测过程中必须同步采集的两侧数据

只盯着 QPS 一个数,等于闭着眼睛开车。压测时至少同步采集以下指标。

客户端侧:

  • 实际并发数(不是配置值,是实际建立的连接数)
  • QPS、响应时间 P50 / P95 / P99
  • 错误分类:连接超时、读超时、5xx、业务错误码
  • 压测机自身 CPU、内存、网络带宽

服务端侧:

  • 应用 CPU、内存、GC 次数和停顿时长
  • 线程池活跃数、队列积压数
  • 数据库连接池使用率、慢查询数
  • 上下游依赖的响应时间(比如缓存、MQ、第三方服务)
  • 网络收发字节数

我一般会用dstat或sar记录压测机的资源,服务端靠 Prometheus 或 APM 抓取。两边的时间戳要对齐,方便后面把曲线叠加起来看。这一步做与不做,直接决定你能不能在出问题时快速定位。

3.4 一次完整的压测现场记录

下面是我最近一次给一个查询接口做容量测试的现场记录,脱敏后分享出来。

被测接口:GET /api/v1/item/detail?id=xxx,返回一个约 3KB 的 JSON,后端是一次 Redis 查询加一次 MySQL 点查。

压测机:8 核 16G,与被测服务不同宿主机,同机房内网。

服务端:4 核 8G 容器,JVM 堆 4G,连接池最大 100。

结果汇总如下:

并发QPS平均延迟P95P99错误率服务端 CPU
50118042ms78ms120ms025%
100226044ms85ms140ms048%
200305065ms160ms380ms072%
4003400117ms340ms890ms0.05%91%
6003180188ms620ms1800ms1.2%96%
8002600305ms1100ms2600ms6.8%97%

读这张表,拐点出现在 200 到 400 并发之间,QPS 峰值大约在 3400 附近。400 并发时 P99 已经接近 900ms,超过了我们定的 500ms 红线,所以安全水位应该在 200 到 300 并发、QPS 2200 到 3000 之间。再往上虽然 QPS 短暂冲高到 3400,但延迟和错误率已经不可接受了。

这个数字最后定在 2500 QPS 作为单实例的容量基线,配合 0.7 的冗余系数,上线时按"单实例 2500 QPS、水平扩展"来做容量规划。

4. 数据怎么读,把曲线翻译成结论

压测跑完拿到一堆数字,真正的功夫在于解读。会看的人能从一个 QPS 曲线的形状里读出系统瓶颈在哪。

4.1 识别系统拐点的四种信号

拐点不是一个精确的点,而是一个区间。当下面这些信号同时出现两个以上,基本可以判定进入拐点区。

第一,延迟开始非线性上升。并发翻倍,延迟涨得比一倍还多,说明请求开始排队。可以用一个简单的比值判断:并发从 A 到 2A,如果 P95 延迟涨幅超过 2 倍,系统已经开始争抢资源。

第二,QPS 不再随并发增长。这是最直观的信号,继续加并发,QPS 反而持平或下降,说明系统的处理能力已经饱和,多出来的并发只是在排队。

第三,错误率从零跳到非零。哪怕只有 0.01%,也要警觉,因为错误率通常不是线性增长,而是到达某个点后突然放大。

第四,某种资源打满。CPU 100%、连接池满、线程池队列堆积、磁盘 IO 饱和,任何一个资源到顶都会成为瓶颈。定位瓶颈最快的办法就是看哪个资源最先到顶。

4.2 平均延迟会骗人,分位数才有话说

假设 1000 个请求里 990 个耗时 20ms,10 个耗时 3000ms,平均延迟是 (990×20 + 10×3000) ÷ 1000 = 49.8ms。看起来还行,但那 10 个用户实际等待了 3 秒,体验是灾难性的。平均值把长尾抹平了,分位数不会。

P95 表示 95% 的请求快于这个值,P99 表示 99% 的请求快于这个值。容量评估时我一般遵循这样的原则:平均延迟用于看整体趋势,P95 用于看大部分用户体验,P99 用于看极端情况和容量红线。业务对延迟敏感的话,有时候还要看 P99.9。

有一个经验公式可以帮你快速估算:如果 P99 是 P50 的 10 倍以上,说明系统里存在明显的排队或者间歇性停顿,去查 GC、慢查询、锁竞争。

4.3 写出一份团队看得懂的容量结论

压测报告不要只丢一堆数字,要让读的人能直接拿去做决策。我通常按这个结构写:

接口:GET /api/v1/item/detail 测试时间:2025-xx-xx 测试环境:4C8G 容器,JVM 4G,MySQL 主从 结论: 1. 单实例峰值吞吐 3400 QPS(400 并发时) 2. 推荐安全水位 2500 QPS(P99 < 500ms,错误率 0) 3. 拐点区间 200~400 并发,瓶颈为服务端 CPU(400 并发时达 91%) 4. 水平扩展建议:目标集群 QPS / 2500 = 实例数,向上取整后再加 1 个冗余实例

注意"安全水位"这个词很重要,它不是峰值。很多事故就是上线时按峰值容量来配,一旦流量有波动或者依赖抖动,立刻越界。

5. 常见问题与排查实录

压测过程中遇到的怪事比顺利的时候多。下面这些是我踩过或者帮别人排查过的典型问题。

5.1 压不上去,到底是谁的锅

现象:无论怎么加并发,QPS 卡在一个数字上不去,延迟也没明显上升。

排查顺序我一般这样走。

先看压测机。top看 CPU 是否打满,ss -s看连接数是否接近端口上限,netstat -an | grep TIME_WAIT | wc -l看有多少连接卡在 TIME_WAIT。如果是压测机的问题,先按 2.3 节调优,再不行就上多台压测机分布式压。

再看网络。同机房内网带宽一般不是问题,跨机房或者走公网就要看带宽是否被吃满。用iftop或nload看一眼收发速率。一个 3KB 的响应在 3000 QPS 下就是 9MB/s,大约 72Mbps,如果压测机是百兆网卡,直接就到顶了。

最后才看服务端。服务端如果有前置的 Nginx 或网关,检查它们的连接数限制、worker_connections、限流规则。我遇到过一次 QPS 死活上不去,最后发现是 Nginx 的limit_req默认配置在生效,每秒只放行 500 个请求。

5.2 结果忽高忽低,波动大得离谱

现象:同样参数跑两次,QPS 差 30% 以上。

可能原因我整理成一张速查表。

现象可能原因验证方式处理建议
两次结果差异大缓存命中率不同看 Redis 命中率、DB 查询量压测数据随机化,先预热
周期性抖动GC 长暂停开 GC 日志,看停顿分布调堆大小、换垃圾回收器
前快后慢连接池/线程池耗尽看池的活跃数和等待队列扩大池或降低并发
忽快忽慢压测机被其他任务干扰top看负载独占压测机
全都很慢上下游依赖变慢单独压依赖服务先解决依赖瓶颈
短时间跑很快,长时间跑崩内存泄漏或连接泄漏长跑 30 分钟看内存曲线修代码,别用压测掩盖问题

5.3 关于"最大吞吐量"的几个误区

第一个误区:把 QPS 峰值当成可承载值。峰值只代表瞬时能力,系统在这个点上没有任何余量,任何抖动都会导致雪崩。

第二个误区:忽略依赖。被测接口自己很快,但它依赖的 MySQL、Redis、第三方接口可能早就到极限了。压测时要拉通看,别只盯着被测服务。

第三个误区:压测环境与生产环境差异太大。核数、内存、JVM 参数、数据库规格、网络拓扑,任一不同都会让数字失真。我的做法是至少保证核数、内存和数据库规格一致,配置差异要显式记录在报告里。

第四个误区:用单接口的 QPS 简单乘以接口数来估算全局容量。多个接口共享 CPU、内存、连接池,叠加时会有资源争抢,实际总吞吐往往低于简单相加。

6. 几个我踩过坑才明白的细节

聊到这,再分享几个特别实用的经验。

第一,压测一定要设上限。脚本里要写清楚最大并发和最长时长,避免跑飞。我见过一次压测脚本没设时长,第二天早上才发现还在跑,白跑了一晚上不说,还把测试库的磁盘写满了。给脚本加--timeout或者在代码里加停止条件。

第二,观察"恢复能力"比观察"峰值"更重要。压到拐点之后,把压力降回来,看系统能否在 1 分钟内恢复到基线延迟。如果降回来之后依然慢,说明有资源没释放,比如连接没归还、线程没回收,这是个隐藏 bug。

第三,记录下每次压测的环境快照。同一份脚本,换台机器、换个 JVM 版本、改个连接池参数,结果都可能变。我会把uname -a、java -version、关键配置文件都存档,半年后回头看还能复现。

第四,接口幂等性和压测的关系。如果接口不是幂等的(比如下单),压测参数一定要用可识别的测试标记,方便事后清理脏数据。如果是写接口,还要考虑数据库写入本身会不会拖慢,读写混合的压测和纯读压测完全是两个结果。

第五,别迷信一次性结论。代码在变、依赖在变、流量模型在变。我给自己定的规矩是核心接口每个大版本上线前重压一次,日常做轻量级的回归压测,确保容量基线不过期。

第六,学会用免费工具做长期监控。压测是一次性的,容量评估需要长期数据支撑。可以把压测的核心指标接入监控看板,日常流量的 P99、错误率、资源水位都能辅助判断容量趋势。至于压测本身,wrk、k6、Locust 这些开源工具已经足够覆盖绝大多数场景,没必要为了压测专门去采购重型商业方案,把预算花在监控和自动化上更划算。

接口的 QPS 和吞吐量自测,说到底是一场主动性很强的实验。你得先想清楚要回答什么问题,再设计实验,再读数据,最后把结论翻译成团队能用的容量规划。整个过程里,工具只是手段,对系统行为的理解才是真正的门槛。

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

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

立即咨询