不知道你注意没有,每次做压测或者上线前预估容量,大家嘴里翻来覆去离不开四个词:吞吐量、QPS、并发数、响应时间。每次复盘都说“吞吐量上不去”“QPS不够”“响应时间太慢”,但真被追问一句“这几个指标之间到底是啥关系?为什么并发上去了吞吐量反而往下掉?”很多人就开始含糊了。
我最早搞性能优化的时候也踩过这个坑。当时某项目上线前压测,负责的同事只盯着 QPS 看,发现 500 并发的时候 QPS 是 1500,加到 800 并发反而掉到 1300,他第一反应是“压测工具出 bug 了”,后来仔细一查,是线程池参数设置不合理,大量线程在等待锁和 IO,CPU 大部分时间花在线程切换上。这个教训让我意识到,不理解指标的底层含义,只盯着数字看,是排查不了性能问题的。
这篇内容就把这四个核心指标彻底掰开揉碎讲一遍:它们各自是什么、怎么算、之间存在什么约束关系,以及真正落地时提高吞吐量可以从哪些方向入手。不管你是后端开发、运维,还是刚入行的测试同学,把这些底层逻辑理顺了,排查问题时思路会清晰很多。
1. 性能指标全景认知:先弄清楚这四个词到底在说什么
1.1 吞吐量:单位时间内系统吞进去又吐出来的总量
吞吐量(Throughput),指的是系统在单位时间内能够成功处理的请求总数。单位可以是每秒请求数(req/s)、每分钟事务数(TPM)、每小时订单量等等,取决于你度量的是什么类型的业务对象。
关键点是“成功处理”。那些因为超时、熔断、拒绝连接而失败的任务,严格意义上不算在吞吐量里。如果系统限流了一堆请求,这些请求不会进入核心处理逻辑,自然不该算作“处理量”。
吞吐量是一个“总量视角”指标,适合用来回答“系统最大能撑多少”的问题。比如大促的时候预估会有 500 万次页面浏览,分摊到峰值两小时,平均每秒大概 700 个请求,那么系统吞吐能力至少要覆盖 700 req/s,还是要有一定冗余才对。
需要区分的是:吞吐量和带宽不是一回事。带宽更多指网络层面每秒能传的数据量,吞吐量是整个系统链路的处理能力,数据库、应用服务器、网络任何一个环节都可能先到瓶颈,所以说到底,这是一个综合指标。
1.2 QPS:每秒查询/请求数,一种精细化的吞吐量表达
QPS 全称 Queries Per Second,最初更多用在数据库领域,指每秒执行的查询次数。后来在接口层面,大家习惯把“每秒请求数”也叫 QPS,再细化一点还有 TPS(每秒事务数)、RPS(每秒请求数)的说法。对于绝大多数互联网业务,“接口 QPS”就是每秒打进来的请求数量。
为什么有了吞吐量还要单独强调 QPS?因为吞吐量有时候太“宽”了。你说系统吞吐量是 10000 req/s,但没说明是全部平均还是峰值,也没有说明是读多还是写多、是静态资源还是动态请求。而 QPS 通常配合着具体的场景来说,比如“首页接口 QPS 2000”“下单接口 QPS 300”,目标要明确得多。
一个容易被忽略的点:QPS 是“服务端接收请求”的速率,但它同时也是“用户侧发起请求”的速率。压测时这俩要能对应起来——压测工具配置了多少并发、每个线程每秒发多少请求,最终服务端收到的才是多少。很多压测工具显示的数字和线上实际值对不上,往往就是这层关系没理清。
1.3 并发数:不是越大越好,它描述的是“同时”的状态
并发数,也叫并发量、在线数,指的是同一时刻系统和客户端之间活跃的处理数量。你可以把它理解为“系统正在同时接待多少客人”。
举个例子:餐厅有 20 张桌子,每桌客人平均吃 40 分钟,那么餐厅系统里“并发数”最多也就是 20(不考虑拼桌、等位的时候进来的额外排队)。再多来客人,就只能排队等着,服务系统内部的队列里会累积等待。同理,服务器层面的并发数包括正在被线程处理的请求,也包括排队的请求、等待 IO 返回的请求。
并发数并不是越高越好。系统每个请求都会占用固定资源(线程、内存、连接对象),并发数超过某个阈值后,资源之间会互相竞争,甚至出现严重的线程上下文切换、锁等待、内存抖动,导致单个请求的处理时间变长,最终系统总吞吐反而下降。后面讲拐点的时候会详细说。
1.4 响应时间:用户感知的“快慢”,性能好坏的第一观感
响应时间(Response Time,RT)是指从客户端发出请求开始,到收到完整响应为止的总耗时。它通常包括网络传输时间、服务端排队等待时间、服务端实际处理时间、响应数据返回时间。
响应时间不是单一数值,而是一个分布。一个接口平均响应时间 200ms,不代表所有用户都是 200ms——可能有 10% 的用户要等 1 秒以上。所以在实践中我们更关注分位数,尤其是 P95/P99:P95 表示 95% 的请求在多少毫秒内完成,能够反映大多数用户遇到的真实体验,也会掩盖不住少数长尾请求的问题。
没有重点考核 P95/P99 之前,一个接口从性能表现上看还挺“稳”的,一加 P99 指标,立刻就要面对长尾问题了。加缓存、加连接池,表面平均响应时间可能变化不大,但尾部延迟明显变好,这些都要靠分位数来观察。
这四个指标的关系可以用一张简单的表来归纳:
| 指标 | 定义 | 单位 | 回答的问题 |
|---|---|---|---|
| 吞吐量 | 单位时间内系统成功处理的总请求量 | req/s | 系统能扛多少 |
| QPS | 每秒请求/查询次数 | 次/秒 | 请求频率有多高 |
| 并发数 | 同一时刻活跃请求数 | 个 | 同时有多少人在“被服务” |
| 响应时间 | 单次请求从发出到返回的耗时 | ms / s | 用户需要等多久 |
2. 四个指标的内在关系与核心公式:一张公式串起整个性能逻辑
2.1 利特尔定律的落地版本
软件性能领域有一个非常经典的关系式,来自排队论的利特尔定律(Little's Law)。它的核心表达是:系统中平均请求数 = 请求到达速率 × 请求平均停留时间。映射到工程上可以写成:
并发数 = QPS × 平均响应时间
或者反过来:
QPS = 并发数 ÷ 平均响应时间
这个公式的价值在于,它把三个最常用的性能指标统一到一个式子里。只要知道两个,就能算出第三个。比如某个接口在 500 并发下测出来平均响应时间是 100ms,理论吞吐就是 5000 QPS;如果平均响应时间涨到 200ms,同样的并发下吞吐就会掉到 2500 QPS。
但要注意,“并发数”不是“在线用户数”。在线用户 100 万,不代表系统要同时处理 100 万个请求,用户大部分时间在思考、浏览页面,真正活跃请求可能只有几千个。这个换算关系可以用“活跃率”来描述,通常远小于 1。
2.2 响应时间不是恒定的:并发与延迟的博弈
仔细观察你会发现,公式右边是两个会相互影响的变量。并发数提高,系统资源占用率提高,一般来说平均响应时间也会随之上升。于是出现了:
- 并发较低时:响应时间基本稳定,QPS 随并发数线性增长。
- 并发达到某个临界点:响应时间开始明显上升,但 QPS 仍然在增长,只是增长变缓。
- 并发继续增加:响应时间急剧恶化,QPS 反而开始下降。
这个模式决定了我们不能简单地把“并发数调大”当作“提高吞吐量”的手段。最优并发数就是刚好卡在拐点附近的那个值。这也是为什么压测要测不同并发级别下的表现,而不是只测单一并发。
2.3 系统拐点与瓶颈识别:先看资源再谈优化
想找到拐点,最直接的办法是梯度压测:从 100 并发开始,逐步加到 200、400、600、800,记录每个级别的 QPS 和 RT,再结合 CPU、内存、磁盘 IO、网络 IO 的监控数据来判断瓶颈到底在哪。
我总结过一套常用的判断思路:
- CPU 利用率接近 100%,说明瓶颈在计算,考虑优化算法或者加机器。
- CPU 不高但响应很慢,大概率是 IO 等待、锁竞争、GC 停顿或网络延迟,需要看线程状态和 JVM 日志。
- 磁盘 IO 满了,看看是不是索引缺失、全表扫描、日志刷盘太频繁。
- 数据库连接池被打满,看看 SQL 有没有慢查询。
这些都要落到数据上看,别靠“感觉系统有点慢”来猜。有一次我帮某团队排查,对方一口咬定是数据库慢,结果我上去看了一眼监控,数据库 P99 延迟只有 5ms,反而是应用层有个第三方 SDK 每次请求都要做一次 DNS 解析,白白多了 300ms。数据和监控不会骗人,感觉会。
3. 提高吞吐量的核心思路:六个方向手把手拆解
3.1 减少单请求耗时:缓存是第一优先级的通用手段
从公式看,响应时间越短,同样并发下吞吐越高。所以要提高吞吐量,最直接的思路就是压缩单个请求的处理链路。而在所有压缩手段里,缓存几乎是最有性价比的。
缓存的价值在于,把重复计算和重复 IO 变成内存查询。比如一个商品详情接口,每次都查 MySQL,假设查询要 30ms,加上序列化、网络这些,整个接口要 120ms;如果热点数据全量进 Redis,接口响应时间能压到 20ms 以内,吞吐量理论上是原来的 6 倍。
但要记住,缓存不是随便加的。加缓存要有明确的 key 设计、过期策略、一致性保证和缓存穿透、击穿、雪崩的预案。我以前见过一个系统,缓存没设置合理的过期时间,又没做预热,一上线把数据库打穿了。那种场景下“提高吞吐量”直接变成了“制造事故”。
3.2 串行转异步:把非关键路径拆出去
另一种思路是让“该等的等,不该等的别等”。很多请求里,核心逻辑其实只占一小部分,大量时间花在调用外部服务、发短信、写日志这些“非关键路径”上。把这些同步操作改成异步化,请求就能快速返回,整体吞吐自然上去了。
异步化的方式很多:消息队列、线程池 + Future、响应式编程等。核心原则是:事务一致性和时序性要求不高的操作,尽量后置。比如用户注册成功后发欢迎短信,完全没必要阻塞在主流程里。我建议先盘点每个接口里有哪些环节是可以延后的,再决定用什么异步方案。
需要注意,异步化会让系统的链路变长,排查问题难度也随之上升。引入消息队列之前,要评估是否真的需要;如果只是简单场景,一个内存版本的异步执行器就够了,杀鸡没必要用牛刀。
3.3 批量思维:把多次 IO 合并成一次
IO 是快速处理的天敌。同样传输 1KB 数据,网络开销的时间往往几倍甚至几十倍于数据处理本身。所以“批量处理”也是提高吞吐量的重要思路。
常见做法包括:批量查询(把 N 个单点查询合并成一个 IN 查询)、批量插入(攒一批数据再写库)、批量发送(把多条消息打包成一个报文)。我自己做过一个数据同步模块,原来是每次 1 条记录 SQL insert,后来改成攒够 100 条批量提交,耗时从几分钟降到几十秒。
批量处理要注意一个“尺寸问题”。批次太小,效果不明显;批次太大,单次执行时间过长,可能触发超时或者内存压力。这个值要靠测试来定,比如每次批量 50、100、200 条分别测一下,选一个吞吐和延迟都比较理想的档位。
3.4 资源复用与池化:连接池、线程池背后的逻辑
系统里有很多对象创建销毁的成本非常高昂,比如数据库连接、HTTP 连接、大对象的初始化。如果每次请求都临时创建,系统大部分时间都在做“无意义的重建”,吞吐上不去是必然的。
连接池和线程池本质上是把成本前置,在系统启动时或者第一次使用时提前准备好一批资源,请求过程只做借还。具体参数上,连接池大小、线程池核心线程和最大线程、队列容量,都要根据业务特性来调。IO 密集型的业务,线程数可以大于 CPU 核数;CPU 密集型的业务,线程数接近 CPU 核数通常更好。
资源池也不是越大越好。连接池设置过大,数据库那边可能扛不住;线程池过大,线程切换和内存浪费反而拖慢速度。调参是一个典型的“找到平衡点”的过程,建议从一个小保守值开始,配合压测逐步上调,观察拐点在哪。
3.5 架构层面的扩展:水平扩容与读写分离
单机能力有天花板,当优化已经做得差不多,吞吐量还是不够,下一步考虑架构层面的扩展。
水平扩展是最常见的:通过负载均衡把请求分散到多台应用服务器,配合数据库读写分离、分库分表,让系统的整体容量近似线性扩展。方向没有新意,难点在于状态的管理——用户会话、本地缓存、分布式锁、唯一 ID,这些东西在单机下很简单,一旦拆到多台,就要引入集中化的方案。
这里要提一点:加机器之前先把单机的瓶颈找清楚。如果某个接口大量时间都在等一个外部服务,那加十台机器也没用,因为瓶颈不在应用本身。当年某平台大会前扩容,运维同学把所有核心服务翻倍部署,结果压测还是一塌糊涂,最后发现瓶颈在一个公共的影子库同步任务上,和业务服务本身的实例数毫无关系。
3.6 兜底策略:限流、降级、熔断对吞吐量的保护意义
大家提到吞吐量优化,往往只想到“怎么更快”,其实“怎么别被压垮”同样重要。没有保护策略的系统,在流量突增时会进入雪崩状态:大量请求积压、超时后重试、重试又增加压力、数据库连接被耗尽,最终吞吐量直接归零。
限流的意义在于,把请求控制在系统能承受的范围内,保证处理中的请求都能正常完成。看似损失了一部分请求,实际上是保住了整体吞吐。降级则是主动砍掉非核心功能,比如推荐列表本来要算很久,流量高峰时直接返回兜底数据。
所以我在设计高吞吐系统时,一定会上限流和降级,而且会在压测阶段就把阈值标定出来。与其任由系统自然过载,不如在可控范围内“有损服务”。这个思路可能在业务同学那里不太容易通过,但真正出过线上事故的人都会理解它的价值。
4. 实操:一次商家营销系统的吞吐量优化全过程
4.1 问题背景与初步压测
某公司做商家营销活动的时候,开发了一个“活动详情页”接口,用来承载商家后台的活动配置信息。上线之前做压测,目标吞吐量 3000 QPS,P95 响应时间不超过 500ms。压测结果出来,250 并发下只有 780 QPS,P95 到了 2.6s,离目标差得很远。
第一反应查监控:CPU 平均利用率只有 30%,内存正常。那显然瓶颈不在计算层面,而是在等待或者说锁上。再看数据库慢查询日志,发现接口对活动表、商家表、图片配置表做了三次查询,每次都要回源数据库;同时在线程栈里看到大量线程卡在数据库连接的获取上。当时连接池配置很小,请求稍微一多,大部分线程就在等连接。
4.2 第一批优化:加缓存 + 连接池调参
先把热点数据查出来放进 Redis。活动配置、商家基础信息这些是典型的“读多写少”数据,缓存命中率能达到 95% 以上。用“缓存优先 + 失效回源”的模式,回源时上一个互斥锁,防止缓存穿透时把所有请求压到数据库。改造后单个请求平均响应时间从 1.2s 降到 180ms。
然后调整数据库连接池:最小空闲连接数调到 10,最大连接数从 20 调到 50。同时把应用线程池核心线程数从 40 调到 64,等待队列容量限制在 200,并配置线程池拒接策略。为什么线程数要 64?因为这台机器是 16 核,线程里大量是 IO 等待状态,适当超配能更好地利用 CPU。压测发现,这个配置下虽然响应时间明显变短,但吞吐仍然没到目标。
4.3 第二批优化:同步调用改异步
进一步分析接口逻辑发现,活动详情页里除了核心的活动信息,还同步调用了两个外部服务:一个是优惠券模板,一个是用户画像。这两个服务平均耗时都在 400ms 以上,而且对主流程来说并不是强依赖。
和业务方确认后,把这两个调用改成了异步模式:核心数据直接返回,非核心数据通过异步线程池补齐后写入 Redis。如果用户下次访问,直接读缓存中补齐后的数据;首次访问时先返回降级数据,后台任务几秒内把完整数据补上。这个改法带来的影响是,用户首次看到的数据可能不完整,但对整个营销场景是可接受的。
这一改动响应时间再次下降,单请求平均到了 80ms 左右,QPS 提升到 1900。看着快接近目标,实际压力测试加并发到 500 时,吞吐反而略有下降。
4.4 第三批优化:批量接口合并 + GC 调优
再往下深挖,发现接口内部为了拿到活动关联的商家信息,虽然配置信息一次性拿到了,但是对每个子活动又调了一次单独的数据查询,导致一个请求内部实际上产生了多次小查询。把多次查询合并为一次多表联合查询,返回后在内存里做分组组装。
同时看了一下 JVM GC 日志,发现 GC 停顿有点多,每次 Full GC 都在 200ms 以上,导致响应时间出现明显毛刺。把堆内存从 2G 调到 4G,切换到了 G1 回收器并调整了停顿时间目标。这一顿调整之后,再压测:500 并发,QPS 稳定在 3200,P95 响应 340ms,目标达成。
4.5 优化效果汇总与经验总结
| 优化阶段 | 主要动作 | 响应时间变化 | QPS 变化 |
|---|---|---|---|
| 初始状态 | 无 | 平均 1.2s,P95 2.6s | 780 |
| 第一批 | 加缓存、调连接池/线程池 | 平均 180ms | 1500 |
| 第二批 | 外部服务异步化 | 平均 80ms | 1900 |
| 第三批 | 批量合并查询、GC 调优 | P95 340ms | 3200 |
能看出来,这次优化不是单一手段的功劳,而是几类手段的组合:缓存减少重复计算、连接池和线程池调优降低资源竞争、异步化拿掉非关键路径、批量合并减少 IO、GC 调优消除长尾毛刺。每个手段贡献大约 30%-50% 的提升,叠加起来就非常可观。
经验上有一点想强调:性能优化要按照瓶颈驱动来做,而不是把网上所有优化手段都堆上去。每做完一步,压测一次,看监控数据有没有变化,再决定下一步做什么。我见过一个反面案例,是有人在还看到数据库 CPU 100% 的时候去优化方法名长度,属于典型的无效努力,方向一错再努力也白搭。
5. 常见问题与排查技巧实录:压测踩坑速查
5.1 CPU 利用率不高,但响应时间很长,问题出在哪?
这类情况最常见的原因有四个方向:
- 等待数据库、Redis、外部 HTTP 等 IO 返回。看线程栈可以看到大量线程处于 BLOCKED 或 WAITING 状态,或者线程 dump 里很多线程卡在 socketRead 上。
- 锁竞争严重。看线程状态有没有 BLOCKED,以及锁等待的统计信息。
- 数据库连接池打满,线程都在等待获取连接。
- JVM GC 停顿。Full GC 期间所有线程都会被阻塞住。
排查顺序建议是:先看应用线程状态,再看 GC 日志,再看慢查询与连接池监控。哪一个指标异常,就往哪一条线路深挖。有一回我排查了很久,最后发现是某个监控 SDK 在后台线程里做实时聚合,每 5 分钟一次全量扫描,恰好压测时间段和扫描时段重合,响应时间就出现了假性升高。
5.2 并发调大,吞吐量不升反降
这几乎是最常见的压测误区。原因主要在三方面:
- 线程池过度创建,导致大量上下文切换。每切换一次线程,CPU 就要忙活一阵,真正的业务计算没跑多少。
- 锁竞争激化。多线程同时抢同一把锁,大量时间浪费在等待锁上。
- 资源池打满导致排队。比如数据库连接池只有 20,500 个线程都在等连接,等于没人干活。
解决办法也很明确:锁定一个合适的并发区间,而不是盲目往上加。找到拐点后,把系统“最佳并发数”当作一个可配置参数管理起来。这个参数不是一次测完就固定不动的,服务发布、数据量变化、硬件替换都会影响它的位置。
5.3 QPS 与 TPS、RPS 到底有什么区别?
QPS 通常指每秒查询次数,适合查询、读场景;TPS 指每秒事务数,强调完整业务操作,比如一个“下单”要调用商品、库存、支付三个接口,整体算一个事务;RPS 泛指每秒请求次数,是一个更宽泛的说法。实际工作中不要太纠结名称,弄清楚你度量的是“请求数”还是“业务完成数”就行,关键是度量口径保持一致。
我在带新人时经常说:你可以用 QPS 看负载,用 TPS 看业务吞吐,但最终对用户有价值的,还是响应时间和成功率。指标是手段,不是目的。
5.4 压测数据不稳定的常见元凶
压测结果不稳定的原因,很多时候不在被测系统,而在压测工具本身:
- 压测客户端所在机器的网络、CPU 不够,它自己就成了瓶颈。
- 压测客户端配置了太高的 think time 或延迟,导致真实请求量达不到设定值。
- 被测系统冷启动,JIT 还没完成编译时,性能表现会明显偏低。
- 全局有定时任务在并发执行,把监控数据搅浑。
我做压测的一个习惯是:压测前先跑 5 分钟预热请求,等 JIT 和缓存都热了再开始记录数据。另外一个习惯是,把压测机放在和被压系统同一内网,排除网络抖动因素的影响。还有一个更细的坑:压测工具的线程数和服务端线程数容易混淆,很多人说“压测用了 1000 并发”,实际上是工具开了 1000 个线程,服务端看到的活跃连接数可能远小于这个值。
5.5 常见问题速查表
| 现象 | 可能原因 | 初步处理方向 |
|---|---|---|
| CPU 打满,QPS 上不去 | 算法复杂度过高、正则/序列化开销大 | 热点函数优化、加缓存、水平扩容 |
| CPU 不高,RT 很长 | IO 等待、锁等待、GC 停顿 | 看线程 dump、GC 日志、慢查询 |
| 数据库连接池耗尽 | 慢 SQL、连接泄漏、池过小 | 抓慢 SQL、检查连接释放、调池参数 |
| 并发增加吞吐下降 | 线程过多、锁竞争、池耗尽 | 梯度压测找拐点,限制并发 |
| 响应时间出现周期性毛刺 | 定时任务、GC、外部服务抖动 | 对齐监控时间线,查周期任务 |
| 压测结果忽高忽低 | 压测机瓶颈、JIT 未预热、网络波动 | 压测机升级,预热,同内网压测 |
最后说一点个人体会。很多人一聊到吞吐量优化就急着上“高级方案”,分布式、微服务、各种中间件全上。但我的经验是,绝大多数系统的问题,出在最基础的环节:SQL 没走索引、缓存没做好、线程池胡乱配置、GC 参数完全没有调过。先把基础打好,往往就能取得非常大的效果。性能优化这事,有点像看病,得先诊断再开药,数据在哪,病根就在哪,思路自然就清晰了。希望这篇里的思路和案例,能帮你少走一点弯路。