P99 很高但平均 RT 正常,这类接口性能问题该怎么查?
平均 RT 还没明显变差,但 P99、超时和偶发卡顿已经先起来时,通常不是系统整体都慢了,而是少量请求、少量实例或某个等待阶段先出了问题。先确认这是不是尾延迟,再把接口、实例、参数、时间窗和阶段耗时切开,后面才知道该往实例差异、隐藏等待、数据库等待链还是发布差异收。
当你遇到“平均 RT 还正常,但 P99 已经很高”的现场时,最容易犯的错,就是继续盯着整体均值看。
因为这类问题最麻烦的地方,不是系统已经整体变慢,而是只有少量请求、少量实例,或者某一个等待阶段先坏了。均值会把这部分异常冲淡,但用户体验、超时和业务投诉不会等你。
所以这篇不打算把性能优化讲成一张大全图,只想回答一个更具体的问题:
当平均 RT 还没明显变差,但 P99 已经很高时,前面几步该怎么切,才不至于把尾延迟看成“整体慢了”?
如果你正在处理 Java 接口偶发卡顿、长尾升高、超时开始增加,但 CPU、GC、数据库总体图又没有明显爆掉的现场,可以直接从下面几个切面收窄。
如果 P99 先坏
不是所有“接口慢”都该从 P99 这条线查。先把现场分一下类:
| 你现在看到的现象 | 更像哪类问题 | 下一步优先看哪里 |
|---|---|---|
| 平均 RT、P50 还正常,但 P99、timeout、偶发投诉先起来 | 典型尾延迟问题 | 继续看本文 |
| 多个接口的平均 RT 一起变差,服务整体都慢 | 更像影响面或共享资源问题 | 单接口变慢和整个服务变慢,排查入口为什么完全不同? |
| 同一个接口只有少数实例拖尾 | 更像实例差异或环境漂移 | 同一个 API 只有部分实例变慢,应该先查什么? |
| CPU、GC、数据库都正常,但请求整体已经明显慢 | 更像隐藏等待链问题 | 接口慢但 CPU、GC、数据库都不高,常见隐藏等待点有哪些? |
| 数据库没打满,但连接池和 API 一起发紧 | 更像数据库等待链向上游传导 | 数据库没打满,为什么 API 和连接池已经开始变慢? |
| 最近一次发布后才开始出现尾延迟抖动 | 更像发布引入的新差异 | 最近一次发布后接口变慢,应该先查什么? |
如果你的现场最像第一行,就别再被“平均 RT 还没坏”这层表象拖住。
为什么平均 RT 还正常,P99 却会先坏
因为平均 RT 看的是整体均摊结果,P99 看的是最慢那一小撮请求。
只要下面这些情况里有一类成立,P99 就可能先抬起来:
- 只有一小类参数会走到坏路径
- 只有少量实例在拖尾
- 只有某个下游偶发长尾
- 只有高峰期某段等待被放大
- 只有某批请求命中了锁、热点 key、深分页或冷缓存
这些问题有一个共同点:
- 它们影响真实用户体验
- 它们会优先打坏尾部请求
- 但它们不一定多到足以立刻把平均 RT 一起拖坏
所以在这类现场里,真正有价值的问题不是“均值不是还好吗”,而是:
到底是哪一小撮请求、哪一批实例、哪一个阶段先分叉了?
第一轮先切 5 个维度,不要只看总量大盘
如果你现在要做一轮 P99 快速判断,我建议优先切 5 个维度:接口、实例、参数 / 租户、时间窗、阶段耗时。原因不复杂,尾延迟最容易被均值和总量面板盖住,只有把“哪一小撮请求、哪一批实例、哪一段耗时先坏”切出来,后面才知道该往实例差异、隐藏等待还是数据库等待链收。
P99 这类问题最怕一上来只盯全局监控。因为它往往不是“总量异常”最显眼,而是“切开以后异常”最明显。
1. 按接口和 URI 切
先回答两个问题:
- 是所有接口的 P99 都在抬,还是只有少数接口在抬
- 是单接口长尾变差,还是整组服务的尾延迟都在飘
如果只有某个接口坏,优先看它独有的 SQL、参数、业务分支和下游,不要一上来就怀疑整套基础设施。
2. 再按实例切
很多 P99 问题,本质上不是接口整体变慢,而是少量实例先拖尾。
常见表现是:
- 平均 RT 看起来还行
- 但某几台 Pod 或某几个节点的 P99 特别差
- 错误率不一定高,尾延迟先坏
- 流量一打散,整体均值还会继续掩盖问题
这时优先怀疑的是实例差异,而不是全局接口逻辑。比如预热不足、配置漂移、节点差异、局部网络路径更差、流量倾斜,都可能先把尾部打坏。
3. 再按参数、租户、热点对象切
P99 最常见的根因之一,不是接口整体慢,而是某一类请求特别慢。
例如:
- 大页码分页请求
- 大租户或大商家请求
- 某几个热点商品、订单或用户
- 某些带特殊筛选条件的查询
- 某类写请求命中了热点行或热点 key
如果不做这层切分,平均 RT、总体 P95 甚至局部面板都可能把真正的问题冲掉。
4. 再按时间窗切
还要继续确认:
- 是只在高峰期抖,还是全天都抖
- 是只在发布后抖,还是和发布无关
- 是只在批任务窗口抖,还是持续抖
- 是只在超时前后抖,还是与重试上升同步出现
很多尾延迟问题都是典型时间窗问题,而不是 24 小时恒定问题。
5. 最后按阶段耗时切
这是很多团队漏掉的一步,但它往往最能回答“时间到底花在哪”。
优先看:
- 应用内排队时间
- 获取线程 / 获取连接时间
- 数据库阶段耗时
- RPC / HTTP 下游阶段耗时
- DNS、建连、TLS 或网络前半段耗时
P99 问题如果不落到阶段耗时,最后很容易只停留在“看见抖动,但不知道谁先坏”。
推荐排查顺序:先确认尾部,再收窄坏路径
如果线上已经出现“P99 很高,但平均 RT 还正常”的场景,更建议按下面顺序走。
第一步:先对齐指标,不只看平均 RT
至少一起看这几个指标:
- P50、P90、P99
- timeout 数
- 错误率
- retry 数
- 请求量和并发变化
这一轮的目标不是找根因,而是先判断:
- 这是不是单纯的尾延迟问题
- 还是已经开始往超时、重试、排队放大链发展
如果 timeout、retry、queue 也在同步恶化,那就说明问题已经不只是“少量请求慢了一点”。
第二步:先切实例和请求维度
这一步最关键,通常能直接把范围缩小很多。
优先看:
- 是哪几个接口在抖
- 是哪几台实例在拖尾
- 是哪几类参数、租户、URI 更慢
- 是不是固定时间窗出现
这一步做完之后,你通常就能把问题初步归到“参数问题”“实例问题”或“时间窗问题”中的某一类。
第三步:再用 Trace 看阶段耗时
Trace 在这类问题里的价值非常高,因为它可以回答:
- 时间到底花在应用内排队、数据库、下游调用还是网络建连
- 长尾是不是集中在某个固定 span
- 是不是某个固定依赖或固定阶段特别长
很多团队在 P99 问题里一上来就翻日志,其实效率通常不如先切指标维度,再用 Trace 看阶段耗时。
第四步:最后再用日志补参数和异常证据
日志更适合回答下面这些问题:
- 慢请求命中了什么参数
- 是否伴随重试、异常码、限流、回退逻辑
- 是否和某次发布、某个租户、某个实例绑定
- 是否存在少量特征请求把整体尾部拉坏
也就是说,日志更适合做补证据,而不是做第一轮范围收敛。
这类问题背后的常见根因
尾延迟问题的根因不需要写成百科全书。实战里,最值得优先怀疑的通常就是下面几类。
1. 少量请求命中了坏路径
这是最常见的一类。
典型场景包括:
- 深分页
- 大结果集
- 冷缓存
- 热点 key
- 特殊筛选条件
- 事务链路更长的请求
这类问题的典型信号是:
- 平均 RT 正常
- P50、P75 可能都还行
- 但 P99、timeout、个别租户投诉先起来
如果你已经怀疑这里,最值钱的动作通常不是继续盯大盘,而是按参数、租户、业务类型和热点对象去分桶。
2. 少量实例在拖尾
这类问题也非常高发。
很多系统并不是接口整体变慢,而是只有一小批实例:
- 刚发布
- 刚重启
- 刚切流量
- 落在某个节点池
- 拿到了一组不同配置
于是整体会出现一种很迷惑的现象:总体均值没坏,但 P99 开始抬,某些 trace 特别长,某些实例的线程池或连接池表现明显更差。
如果你发现是这种长相,就该优先切到实例差异链路,而不是继续按全局接口逻辑查。
3. 等待型瓶颈只在尾部请求里放大
P99 问题很常见的特征,不是执行慢,而是等待慢。
例如:
- 线程池偶发排队
- 连接池偶发借连接变慢
- 锁等待只在热点请求里出现
- 获取令牌或配额时偶发阻塞
- 本地队列、批处理、异步回调在高峰期积压
这些等待型问题有个共同点:
- 不是每个请求都会遇到
- 但一旦遇到,单次就会被拖得很长
- 特别容易先反映在 P99,而不是平均 RT
如果你已经确认均值正常、P99 抖动严重,更应该优先看请求是不是在排队、取连接、等锁、等令牌,而不是继续怀疑 CPU 一定不够。
4. 下游没有整体变慢,但长尾开始恶化
很多下游依赖并不是“整体 RT 大幅上升”,而是平均值还行,但少量调用开始出现 1 秒级甚至更长的尾部耗时。
常见来源包括:
- RPC / HTTP 下游偶发长尾
- 缓存、消息队列、第三方服务响应不稳定
- DNS、TLS 建连、连接复用失效导致偶发建连慢
- 某组下游节点或某个可用区路径更差
一旦你的服务把这些长尾调用同步串在主要链路上,P99 就会先被拉坏。不要只看下游总体平均 RT,总体均值在这类问题上同样会骗人。
5. 重试和排队把少量慢请求继续放大
P99 一开始变差时,最危险的不是“慢了一点”,而是它很容易被继续放大。
典型演化顺序通常是:
- 少量请求先慢
- 少量请求开始接近超时
- 上游、网关或客户端开始重试
- 线程和连接被占得更久
- 排队继续上升,P99 更差
- 最后平均 RT 也跟着被拖下来
所以当你看到 P99 抖动时,最好顺手一起看 timeout、retry、线程池 queue、连接池 pending 有没有同步恶化。如果这些一起变坏,说明问题已经从“尾部异常”往“系统性放大”演化了。
读到这里,什么时候该换方向
这篇只处理一类很具体的现场:平均 RT 还没全面变差,但 P99、超时和偶发卡顿已经先起来。
如果你往下看时发现现场更像下面这些情况,我会直接换个方向继续查:
- 你先要分清这是单接口问题、整组服务变慢,还是共享资源一起出事
- 你已经确认是少数实例持续拖尾
- CPU、GC、数据库总体图都不高,但请求整体已经慢到不只是尾部问题
- 数据库连接、事务型接口、连接池等待信号更强
- 发布时间线最强,尤其是发布、灰度、扩容后才开始抖
换句话说,本文只解决“平均 RT 正常但 P99 已坏”这类尾延迟判断,不打算把实例差异、隐藏等待、数据库和发布问题一次讲透。
最容易误判的地方
误判 1:平均 RT 正常,就说明系统整体还健康
不对。对用户体验和超时更敏感的,往往恰恰是尾部请求,而不是均值对应的“平均用户”。
误判 2:P99 抖动就是监控噪声
采样、分位算法、流量波动确实可能造成部分抖动,但如果业务投诉、timeout、重试或少量实例异常也一起出现,就不要再把它当成纯监控问题。
误判 3:只看全局 P99,不切实例和参数
不切维度,P99 问题通常很难真正落到根因。越是局部异常,越要强制按实例、参数、租户和时间窗去拆。
误判 4:一看到 P99 变差,就先调大 timeout
这通常只会让慢请求更长时间占住线程、连接和下游资源,不一定更稳,反而可能把尾部问题放大成更大的排队问题。
FAQ
1. 平均 RT 正常,为什么业务还是会投诉慢?
因为用户遇到的不是“平均请求”,而是真实请求。只要一部分关键请求很慢,用户体验就已经受影响了,尤其是高价值租户、重参数请求或关键路径请求。
2. 这种问题先看日志还是先看 Trace?
通常先切指标维度,再看 Trace。Trace 更适合回答时间花在哪个阶段,日志更适合补参数、异常、重试和上下文细节。
3. 只看 P95 行不行,还是一定要看 P99?
P95 能帮助你看趋势,但如果投诉、超时和偶发卡顿主要落在最慢那一小撮请求上,P99 往往更接近真实用户体验。更稳妥的做法是把 P50 / P95 / P99 一起看,而不是只盯一个分位数。
4. 偶发超时该先查实例还是下游?
先别二选一,先看尾延迟是集中在少数实例,还是集中在某个固定 span。若少数实例特别差,先去 同一个 API 只有部分实例变慢,应该先查什么?。若 Trace 已经显示某个下游阶段明显拖长,再顺着下游或隐藏等待方向收窄。
5. P99 变差时,一定要先怀疑数据库吗?
不一定。它也可能是实例差异、线程池排队、下游长尾、网络建连,或者少量参数命中了坏路径。只有当数据库相关等待、事务型接口和连接池压力一起抬升时,才更像数据库等待链问题。
把尾部请求切开以后,下一步一般看这里
当你已经看清“是哪一小撮请求、哪一批实例、哪一段耗时先坏”,后面基本就是顺着最像的那条线继续补证据。
- 还没判断清这是单接口慢、整站慢,还是共享资源问题:先回到 单接口变慢和整个服务变慢,排查入口为什么完全不同?
- 同一个接口只有部分实例在拖尾:直接接 同一个 API 只有部分实例变慢,应该先查什么?
- CPU、GC、数据库都不高,但请求整体已经慢了:转去 接口慢但 CPU、GC、数据库都不高,常见隐藏等待点有哪些?
- 数据库没打满,但连接池和 API 一起开始变慢:先看 数据库没打满,为什么 API 和连接池已经开始变慢?。如果数据库链路已经坐实,再接 explain 看起来没问题,SQL 还是很慢,下一步该查执行、等待还是连接池?
- 这一波抖动就是最近一次发布之后才出现:再看 最近一次发布后接口变慢,应该先查什么?
最后留一句
遇到“P99 很高但平均 RT 正常”这类现场时,别急着调参数,也别急着给某个单点定锅。先确认它是不是尾延迟问题,再把接口、实例、参数、时间窗和阶段耗时切开;等 Trace 和日志把坏路径补齐以后,后面无论你往实例差异、隐藏等待、数据库等待链还是发布差异收,都会顺得多。