Java

P99 很高但平均 RT 正常,这类接口性能问题该怎么查?

平均 RT 还没明显变差,但 P99、超时和偶发卡顿已经先起来时,通常不是系统整体都慢了,而是少量请求、少量实例或某个等待阶段先出了问题。先确认这是不是尾延迟,再把接口、实例、参数、时间窗和阶段耗时切开,后面才知道该往实例差异、隐藏等待、数据库等待链还是发布差异收。

  • P99
  • 接口慢
  • 性能排查
  • 尾延迟
  • Java
16 分钟阅读

当你遇到“平均 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 一开始变差时,最危险的不是“慢了一点”,而是它很容易被继续放大。

典型演化顺序通常是:

  1. 少量请求先慢
  2. 少量请求开始接近超时
  3. 上游、网关或客户端开始重试
  4. 线程和连接被占得更久
  5. 排队继续上升,P99 更差
  6. 最后平均 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 变差时,一定要先怀疑数据库吗?

不一定。它也可能是实例差异、线程池排队、下游长尾、网络建连,或者少量参数命中了坏路径。只有当数据库相关等待、事务型接口和连接池压力一起抬升时,才更像数据库等待链问题。

把尾部请求切开以后,下一步一般看这里

当你已经看清“是哪一小撮请求、哪一批实例、哪一段耗时先坏”,后面基本就是顺着最像的那条线继续补证据。

最后留一句

遇到“P99 很高但平均 RT 正常”这类现场时,别急着调参数,也别急着给某个单点定锅。先确认它是不是尾延迟问题,再把接口、实例、参数、时间窗和阶段耗时切开;等 Trace 和日志把坏路径补齐以后,后面无论你往实例差异、隐藏等待、数据库等待链还是发布差异收,都会顺得多。