监控大屏上的TPS曲线像心电图一样平稳,稳定在3500上下,领导看了很满意;可另一边客服工单里堆满了“页面转圈、订单迟迟不更新”的投诉。这种“数字好看、体验翻车”的场景,在用性能监控工具做实时TPS与响应时间分析时太常见了,行业里给它起了个名字叫TPS虚高。这篇文章我想把一件事讲透:实时TPS和响应时间这两组监控指标,到底该怎么定义、怎么采集、怎么分析,以及为什么它们经常联手骗人。适合正在搭监控体系的运维和开发同学读,看完至少能知道自己监控面板上的数字有多少水分。
1. 先较个真:TPS到底在数什么,数错了全都白搭
1.1 TPS和QPS、HPS不是一回事
很多人把TPS挂在嘴边,但真要问“你监控面板上那个TPS数的到底是什么”,十有八九答不上来。TPS是Transactions Per Second,每秒事务数;QPS是Queries Per Second,每秒查询数;HPS是Hits Per Second,每秒点击数。三者的关系可以粗暴理解成:一个业务事务,往往由多个查询或多次客户端调用组成。用户点一次“提交订单”,浏览器可能发了3个请求,后端可能执行了8条SQL,那这一次下单算不算1个TPS?算3个QPS?还是算8个数据库查询?
不同的监控工具、不同的埋点方式,答案完全不同。很多APM工具默认把每一次HTTP请求都记为一个事务,这本身没错,但如果你拿“请求数”去冒充“业务TPS”,那数字天然就会被放大几倍。一个订单接口背后挂3个资源请求,请求级TPS就是业务TPS的3倍起步。这不是工具的问题,是口径问题。
1.2 一个业务事务的计数边界怎么划
我建议你在设计监控指标之前,先做一件事:把系统里的“事务”画个边界。所谓业务事务,应该以“用户一次完整的业务意图”为准。登录算一个事务,下单算一个事务,支付回调处理算一个事务。一个事务的内部请求、内部SQL、内部缓存操作都不应单独计为TPS。
划边界时最容易踩的坑有三个。第一,把健康检查算进去了。负载均衡器每5秒探活一次,50个实例每秒就是10个“事务”,面板上那点底数是这么来的。第二,把静态资源和网关转发都算进去了。图片、CSS、JS的请求不是事务,但在很多日志解析方案里它们会被无差别统计。第三,把重试请求算了两遍。上游超时重试3次,业务只成功1次,监控上记了4个事务。
口径不统一,后面所有分析都是空中楼阁。我的习惯是:在采集端就把事务类型打标签,transaction_type=login、transaction_type=order_create、transaction_type=health_check,统计时先过滤掉非业务标签,再做聚合。宁可少统计一点“边缘事务”,也要保证核心业务数字是可信的。
1.3 吞吐量高不等于性能好:两个指标分着看
TPS反映的是吞吐量,它只回答一个问题:系统单位时间内处理了多少事务。它完全不回答这些事务处理得有多快、用户等得有多苦。一个线程池被占满、请求全部在排队等待的系统,每秒依然能“受理”大量请求,TPS曲线可以非常平稳,但用户感受到的是卡顿和超时。所以性能监控必须把TPS和响应时间分开看,并且要意识到:TPS高不是性能好的充分条件,甚至不是必要条件。
打个比方,TPS像是餐厅门口排号机发出去多少号,响应时间像是每桌客人从落座到上齐菜品的时间。发号快不代表上菜快,如果后厨已经堵死了,发号再多只会让客人越积越多。这个类比在后面排查TPS虚高时会反复用到。
2. 实时监控链路怎么搭:埋点、聚合与存储的口径选择
2.1 三种主流数据来源:Agent埋点、中间件指标、日志解析
实时TPS监控的数据来源,业内无非三条路:Agent埋点、中间件指标、日志解析。Agent埋点是在应用内嵌入SDK,拦截HTTP入口和RPC调用,统计每个接口的成功数、失败数、耗时分布。这条路数据最精准,能精确到接口维度,但侵入性强,而且对语言栈有要求,Java系用起来最顺手。
中间件指标是另一条路,比如Nginx的$request_time变量、Tomcat的线程池指标、消息队列的消费速率,这些数据由中间件自己暴露,应用不用改代码。好处是无侵入、覆盖广,代价是拿不到业务维度信息——你能看到Nginx转发量大,但不知道其中多少是真实下单。
日志解析则是把访问日志、业务日志捞出来做流式统计,用Flink这类引擎实时聚合TPS。日志里能带业务字段,灵活度高,但延迟天然比前两者高,而且对日志规范要求苛刻:字段不统一,统计出来的数就是糊涂账。
成熟团队的做法通常是三者混用:Agent埋点做业务TPS主口径,中间件指标做容量和饱和度监控,日志解析做离线复核和维度分析。
2.2 时间窗口和聚合方式,直接决定看到的TPS真不真
实时TPS的“实时”也是有歧义的。统计窗口用多长,看到的曲线完全不同。用1秒窗口,曲线剧烈抖动,尖刺特别多,图很难看也更真实;用1分钟窗口,曲线平滑了,但突发流量被平均掉了,你根本不知道高峰期真实压力是多少。
这里我给一个经验值:实时监控至少保留10秒粒度的TPS曲线,告警则基于1分钟聚合后的数据,避免瞬时抖动误报。窗口太短,健康检查的周期性探活会变成规律尖刺;窗口太长,毛刺和异常被抹平,失去实时意义。
聚合方式同样有讲究。很多人直接sum(rate(...)),把所有实例加总成一个总TPS,这没问题,但一定要同时保留单实例的TPS视图。分布式系统里,流量倾斜非常常见,8个实例里5个空闲、3个打满,总TPS看着还很健康,实际上那3个实例已经濒临崩溃。不看实例分布的总TPS,就是个安慰剂数字。
2.3 小团队够用的选型组合
搭一套够用的实时TPS+响应时间监控,不一定要上全家桶。我的建议是分两档:
基础档:Prometheus + Grafana + Spring Boot Actuator。应用暴露http_server_requests_seconds_count这类指标,Prometheus每15秒拉一次,Grafana出图。这套组合胜在轻量、全开源,指标语义清晰,满足大多数中小团队“看看趋势、报警救人”的需求。缺点是拿不到调用链,出了问题还得靠日志慢慢翻。
进阶档:SkyWalking一类的开源APM工具。它自带Agent,能自动采集每个接口的TPS、响应时间、P99、错误率,还能生成服务拓扑和分布式调用链。排查“哪个下游把RT拖高”这种问题时,APM的调用链视图是纯指标监控给不了的。代价是Agent本身有少量性能损耗,内存占用需要评估。
选型上我个人的原则是:先确认你要回答的问题。只想回答“系统扛不扛得住”,Prometheus够用;想回答“慢在哪里、哪个调用环节拖后腿”,必须上APM或链路追踪。
3. 响应时间分析的深水区:平均值会骗人,长尾才是用户骂点
3.1 为什么看P99比看平均耗时靠谱
响应时间监控最经典的坑,就是盯着平均耗时看。平均值是会被极少数慢请求拉高的,但更可怕的是它也会被大量快请求拉低。举个真实例子:一个接口每秒1000个请求,990个响应在50毫秒以内,10个响应要5秒。平均值是(990×0.05+10×5)/1000≈0.1秒,看平均值你会觉得这接口性能非常好。但实际体验是,每100个用户里有1个人被卡了5秒,如果这个用户正在下单,他已经流失了。
所以在响应时间分析里,百分位数才是主角。P50代表一半请求的耗时水平,P95代表最慢的5%请求,P99代表最慢的1%请求。P99的恶化通常比平均值的恶化早几个小时出现,它是用户体验恶化的早期预警信号。监控上我会固定看四个数:P50、P90、P99、P999。P50和P90管日常基线,P99管预警,P999管强告警。
| 指标 | 含义 | 监控用途 |
|---|---|---|
| P50 | 一半请求在它以内 | 日常基线,反映整体手感 |
| P90 | 90%请求在它以内 | 反映大多数用户体感 |
| P99 | 99%请求在它以内 | 长尾预警,用户体验保险丝 |
| P999 | 99.9%请求在它以内 | 强告警,通常伴随故障 |
3.2 在客户端测还是在服务端测,结果能差出两倍
同一个接口,服务端监控显示P99是200毫秒,客户端真实体验可能接近500毫秒。差出来的部分包括:网络传输时间、DNS解析、网关转发、浏览器渲染之前的排队时间。服务端测的是“请求到达应用、应用处理完返回”的耗时,而用户感知的是“从点击到页面变化”的全链路时间。
所以一套完整的性能监控工具,响应时间必须分两层看。服务端测的是应用处理时间,客户端或拨测测的是端到端体验时间。我见过不少团队把服务端RT当成用户体验来做SLA承诺,结果用户投诉不断,监控却一片绿灯,问题就出在测的地方不对。
建议在小规模范围内做真实用户监控(RUM),或者用定时拨测模拟用户请求,把端到端响应时间单独拉一条曲线。服务端RT和服务端到端RT两条线一对比,网络损耗和前端排队问题立刻现形。
3.3 从RT分布反推瓶颈:匀速恶化还是偶发尖刺
响应时间不能只看趋势,还要看分布形态。同样的P99数值,背后的分布可能完全不同。一种是整体匀速恶化:P50、P90、P99同时往上走,说明容量不够,可能是CPU饱和、数据库连接池耗尽,所有请求都在抢资源,处理速度均匀下降。另一种是偶发尖刺:P50平稳,P99经常突然飙升后回落,说明系统多数时候健康,但存在间歇性阻塞——典型的触发点是Full GC、慢SQL偶发、下游服务超时重试。
处理思路也不同。匀速恶化优先扩容、优化资源瓶颈;偶发尖刺优先排查GC日志、慢SQL、下游依赖的熔断降级。只看平均值曲线,这两种情况几乎长一样,但排查方向天差地别。所以我强烈建议:响应时间监控至少同时展示P50和P99两条曲线,并且单独配一张耗时分布直方图。
4. TPS虚高的七种来历:看着漂亮,实则全是水分
4.1 把健康检查和静态资源当成了业务事务
这是最浅层的TPS虚高,也是最普遍的。只要做一次“事务类型分布”的审计,就能澄清很多误会。健康检查类的探活请求特征非常明显:周期性、固定间隔、短耗时,5秒一次、每次几十毫秒。静态资源请求特征也很明显:URI集中在.js、.css、.png,且没有业务字段。
排查方法不复杂,把监控面板的TPS按URI维度拆分,看排前几位的都是什么路径。如果/actuator/health排进了Top 10,那你面板上的总TPS至少有5%-10%是假业务。处理方式是采集端过滤,或用PromQL排除:sum(rate(http_server_requests_seconds_count{uri!~"/actuator/.*|/health|/static/.*"}[1m]))。这一个改动做下去,很多团队会发现自己的“高TPS”直接缩水两到三成。
4.2 异步丢消息:请求收了,活儿还没干
这是TPS虚高里最坑的一种,因为它骗的不只是监控,还让整个团队误判系统容量。典型场景在支付回调、消息通知、异步下单这类业务里:应用接到上游回调后,校验一下签名、把消息丢进MQ,立刻返回200。从HTTP协议层看,这个“事务”确实处理完了,耗时还很短;但从业务层面看,真正的订单状态更新、数据库写入、下游通知,全都在MQ消费者那边排队。
监控面板上的TPS记录的是“接收消息的速率”,而不是“处理业务的速率”。接收速率是每秒3000,消费者处理能力只有每秒500,于是MQ堆积越来越严重,业务延迟从几秒恶化到几分钟。你盯着TPS面板会陷入困惑:明明曲线平稳,为什么业务就是慢?答案很简单,你测的是受理量,不是完成量。
解法是双轨指标:一轨记录请求接收TPS,一轨记录业务完成TPS,再配一条MQ消费堆积曲线。三条线放在同一个看板上,接受量高、完成量低、堆积上升,三者同时出现,虚高立刻现形。
4.3 线程池排队:TPS稳如老狗,RT早已爆表
这种虚高可以用Little‘s Law解释:系统内并发数等于吞吐量乘以响应时间。假设一个服务的线程池有200个线程,每个请求处理需要2秒,那这个系统的真实吞吐上限就是200/2=100 TPS。但如果监控上显示1000 TPS,余下的900个请求在哪?答案是:在队列里排队。
排队期间,请求也算作“被受理”了,TPS统计依然在涨,但响应时间已经在成倍恶化。这时候去看RT曲线,P99已经飙到几秒甚至几十秒,可TPS曲线还是一片平地。很多运维第一次遇到这种情况都懵了,以为是监控数据错了,其实恰好相反:TPS没错,错的是把它当成“系统处理能力”。要验证很简单,看线程池的活动线程数和队列大小。活动线程数打满、队列持续堆积、TPS和RT同时高位,这就是排队虚高的铁证。
4.4 批量聚合和长轮询的时间错觉
批量聚合是另一类隐蔽的水分。有些系统为了吞吐优化,把写入操作攒批提交,比如消费端攒够100条或者500毫秒才 insert 一次。从业务事务的角度看,这100条业务确实在被处理,但如果监控口径是“把一次批量写入计为1个事务”,TPS就会被低估而不是高估。反过来,如果某个框架把批量内的每条记录都计为一次独立事务,那TPS就被均匀放大了,放大倍率取决于批量大小,这种放大平时看不出来,压测时数字能大得离谱。
长轮询和WebSocket这类长连接请求更特殊。一个长轮询请求可能会挂30秒等消息推送,如果监控把连接建立当成事务完成,TPS会偏高且虚高比例不稳定;如果把连接关闭当成事务结束,响应时间会被拉长到一个毫无意义的数值。对这种场景,我建议单独分类,不要把长连接和普通HTTP请求混在同一个TPS口径里。
4.5 网关缓存层造成的“区域性虚高”
加了缓存和CDN之后,TPS虚高会呈现出明显的分层现象。入口网关的TPS可能高达好几万,因为大量请求在网关层或缓存层直接命中返回了,根本没打到应用服务器。应用层的TPS可能只有几千,数据库层的TPS更低。
问题在于:你的监控面板默认显示的“系统TPS”到底是哪一层?如果只给领导看网关TPS,那数字永远漂亮,但上层命中缓存后,应用层压力根本反映不出来,容量规划会严重失算。更危险的是,缓存一旦大面积失效,所有请求穿透到应用层,网关TPS没怎么变,应用层TPS瞬间放大几十倍,如果应用层的容量预算一直按“系统TPS很低”来设计,这一下就能把服务打垮。
所以TPS监控必须分层次打标签:网关层TPS、应用层TPS、数据层TPS各一条曲线,配合缓存命中率一起看。虚高的数字不可怕,可怕的是不知道虚在哪里。
5. 一次真实排查:回调服务TPS虚高的完整定位过程
5.1 现场:网关收到的TPS曲线一切正常,业务却慢了三分钟
有一回线上出了这么个事:支付回调服务接到上游渠道商的回调请求,面板显示吞吐量稳定在3500 TPS,P99响应时间只有800毫秒,看起来一切健康。结果业务方找过来,说对账数据延迟严重,订单状态最长要3分钟才能更新完。
我当时第一反应就是TPS口径出了问题。3500 TPS但业务处理延迟到分钟级,这中间一定有个巨大的“缓冲地带”,把请求收进来却拖着不干完。
5.2 排查链路:从指标定义到MQ堆积
排查的第一步是看请求到底在哪条链路上。从网关日志追下去,回调请求的确是打到了应用实例上,应用返回200也很快。但打开应用日志仔细看,发现代码里收到回调后的核心操作是“解析报文、简单校验、发送MQ消息”,发送完就返回了。真正的订单更新逻辑在MQ消费者里。
于是第二步看MQ的消费曲线。这一看就明白了:生产者(回调接收端)的发送速率大约每秒3500条,消费者的处理能力只有每秒400到500条,消费位点持续落后,堆积条数以肉眼可见的速度往上爬。TPS虚高的根因清楚了——监控面板上的3500 TPS是“接收事务”的速率,而业务的完成速率只有500。
第三步做验证,把消费者的实际完成量和面板TPS拉到同一张图对比。两条曲线一上一下,差距越拉越大,无论怎么解释口径,业务延迟都必然持续恶化。至此根因锁定。
5.3 修正后的指标设计
修正方案分两步。第一步止血,给MQ消费者扩容、调大消费线程数,并针对堆积量做了告警。第二步才是治本,重新设计这个服务的监控指标:
- 接收TPS:记录HTTP接口实际接收并返回200的回调请求数,作为“入口压力”指标;
- 完成TPS:记录MQ消费成功后业务落库的完成数,作为“真实吞吐”指标;
- 堆积量:MQ消费位点落后的消息数,作为“健康度”核心指标;
- 消费耗时P99:消费者单条消息的处理耗时,作为“处理效率”指标。
这组指标上线后,再看这个服务就一目了然:接收TPS再高也不会误导人,只要完成TPS跟不上接收TPS,堆积量必然抬头,告警立刻触发。后来类似的异步场景,我都按这套“入口、出口、积压、耗时”四件套来设计,再没被“虚高涨”骗过。
6. 报警和看板:别让监控变成只有告警没结论的摆设
6.1 阈值别用绝对值硬卡,基线对比更靠谱
很多团队给TPS设告警时喜欢拍脑袋填一个绝对值,比如“TPS低于500就报警”。问题是,不同业务、不同时段、不同日期的TPS天然不一样,深夜低谷500可能正常,高峰期500可能是灾难。绝对值阈值只对容量严重不足的系统有参考意义,对于大多数稳定运行的系统,更好的做法是基线对比。
具体操作:取过去7天同一时段(比如昨天下午3点到4点,和上周三下午3点到4点)的TPS作为基线,当实时TPS偏离基线超过一定比例并持续若干分钟,才触发告警。比如实时TPS比基线下降70%以上,持续5分钟,大概率是挂了或者流量被切走了;实时TPS比基线暴涨200%以上,持续5分钟,可能是活动流量、可能是缓存失效穿透,也可能是爬虫攻击。这种相对告警比绝对值告警可靠得多,而且不需要你精通每条业务线的容量规划。
6.2 告警看组合拳:吞吐、延迟、错误率、饱和度缺一不可
单看TPS告警有个致命缺陷:它只能告诉你“量变了”,完全不能告诉你“系统是否还健康”。一个服务TPS从1000暴涨到5000,可能是好事(活动拉量),也可能是坏事(缓存穿透),甚至可能是灾难(告警风暴本身把系统拖垮)。所以告警规则一定要用组合条件,我自己的组合是:
- 吞吐异常:TPS偏离基线比例超限;
- 延迟异常:P99超过业务SLA阈值,比如核心接口P99大于1000毫秒持续3分钟;
- 错误率异常:5分钟内错误率超过1%,或绝对错误数超过固定下限;
- 饱和度异常:线程池活跃率超过80%,或者MQ堆积量超过可容忍上限。
这四个条件里,至少同时满足两个才真正触发告警。比如TPS暴涨且P99同步恶化,那是容量问题;TPS暴涨但P99平稳、错误率极低,多半是缓存命中或者健康的流量增长,只需要观察不需要惊动大家。组合条件能砍掉大量无效告警,让值班同学把精力留给真正的事故。
6.3 看板设计的三层结构
看板不是指标越多越好,而是要让看图的人在5秒内回答三个问题:系统现在忙不忙、系统有没有出错、用户体验有没有恶化。我习惯把看板设计成三层:
第一层是总览层,放全局TPS曲线和全局P99响应时间曲线,两张图上下对齐,x轴时间一致,方便一眼看出吞吐和延迟的联动关系。第二层是维度层,放按入口、按服务、按实例拆分的TPS热力图,用来定位流量到底集中在哪里、哪个实例出现了倾斜。第三层是根因层,放线程池活跃率、MQ堆积、GC耗时、下游依赖的P99,这一层平时不怎么需要看,但前两层出现异常时,第三层负责回答“为什么”。
三层看板配合组合告警,组成了一个完整的闭环:总览发现问题、维度层定位范围、根因层找到答案。很多团队把几十张图表铺满大屏,看起来专业,真出事时反而不知道该看哪张图。监控的核心不是信息多,而是信息能快速变成结论。
最后再分享一点个人的体会:做性能监控这几年,我最大的收获不是学会用多少工具,而是学会怀疑数字。TPS虚高这事,本质上是监控工具把“中间状态”当成了“最终结果”。每次搭建新的监控指标,我都会问自己一个问题:这个数字如果突然变得很好看,它意味着系统真的变好了,还是只是某个环节被绕过去了?带着这个疑问去设计指标口径,比掌握任何高端工具都重要。