接口突然变慢,你怎么排查?

场景:

线上有一个订单查询接口,平时响应 100ms 左右,今天突然变成 3 秒以上。没有明显报错,CPU 也不是特别高。你会怎么排查?

线上接口变慢是后端工程师非常常见的问题。它不像接口直接报错那样明确,也不像服务宕机那样容易被发现。很多时候,监控只告诉你接口响应时间从 100ms 涨到了 3 秒以上,日志里没有明显异常,CPU 也没有打满,看起来系统还活着,但用户已经明显感觉到卡顿。

这种问题最怕的是一上来就凭经验猜,比如"是不是 SQL 慢了""是不是 Redis 有问题""是不是 GC 了"。这些猜测都有可能对,但如果没有排查顺序,很容易在错误方向上浪费时间。我的思路是:先确认问题边界,再沿着请求链路逐层拆解,最后把瓶颈定位到一个具体组件、具体资源或具体代码路径上。 线上排查不是展示你知道多少工具,而是看你能不能在压力下快速缩小范围。

一、先不要急着优化,先确认"慢"到底是什么慢

假设现在有一个订单查询接口,平时平均响应时间在 100ms 左右,今天突然变成 3 秒以上。第一步我不会马上去看 SQL,也不会直接重启服务,而是先确认几个基础问题:

  • 是所有请求都慢,还是部分请求慢?

  • 是平均耗时变高,还是 P99 被拉高?

  • 是从某个时间点突然变慢,还是逐渐变慢?

  • 是只有订单查询接口慢,还是整个应用都慢?

  • 是所有机器都慢,还是某几台实例慢?

这些问题决定了后面的排查方向。如果只有订单查询接口慢,其他接口正常,问题大概率在这个接口的业务链路上,比如 SQL、缓存、下游 RPC、序列化、返回数据量等。如果整个应用都慢,范围就要扩大到 JVM、线程池、连接池、机器资源、网络、依赖服务等。如果只是部分用户慢,就要关注数据分布,例如某些大客户订单量特别大、某些用户命中了特殊业务逻辑、某些请求参数导致查询无法走索引。

我通常会先看监控上的几个维度:

监控指标 主要目的
QPS 判断是不是流量突增导致
平均 RT、P95、P99 判断是整体慢还是长尾请求慢
错误率 判断慢是否已经演变成失败
CPU、内存、Load 判断机器资源是否异常
GC 次数和耗时 判断是否存在 JVM 停顿
线程池、连接池 判断是否存在排队或资源耗尽

如果 QPS 没变但 RT 暴涨 ,通常说明某个依赖变慢、资源被阻塞或代码路径变重。如果 QPS 暴涨后 RT 上升 ,可能是容量不足、热点流量、缓存失效或线程池排队。如果 错误率也上升,说明慢已经演变成失败,需要先考虑止血。

二、用链路耗时拆分问题

一个订单查询接口通常不会只做一件事。它可能先查用户权限,再查订单主表,然后查订单明细、物流信息、支付状态、售后状态,最后组装 DTO 返回给前端。如果只知道整个接口耗时 3 秒,但不知道这 3 秒花在哪里,排查会非常被动。

因此我会优先看链路追踪,或者在关键节点补充耗时日志,把接口拆成多个阶段:

  • 网关转发耗时

  • 应用接收请求耗时

  • 参数校验耗时

  • Redis 查询耗时

  • 数据库查询耗时

  • RPC 调用耗时

  • 对象转换耗时

  • JSON 序列化耗时

  • 响应写回耗时

只要拆开看,就能很快知道问题到底在应用内部,还是在外部依赖。如果数据库查询用了 2.5 秒,那重点就是 SQL 和数据库。如果数据库只有 20ms,但接口整体 3 秒,就要看线程阻塞、远程调用、连接池排队、序列化或网络传输。

我比较推荐在核心接口中保留分段耗时日志,尤其是订单、支付、库存这类高频核心链路。日志不需要写得很重,但要能回答一个问题:这次请求慢,慢在了哪一步。

示例日志可以像这样:

复制代码
traceId=abc123, api=queryOrder, userId=10001,
checkAuth=8ms, queryCache=12ms, queryOrderDb=1870ms,
queryLogisticsRpc=430ms, buildResult=35ms, total=2380ms

这类日志的价值在于它能直接告诉我们:数据库查询是主要瓶颈,物流 RPC 也有明显耗时,但对象组装不是问题。 没有这种拆解时,大家只能凭经验争论;有了这种拆解,排查就变成了证据驱动。

三、如果数据库慢,不能只说"加索引"

订单查询接口最常见的慢点之一是数据库,但**"数据库慢"只是一个现象,不是结论**。数据库慢可能是 SQL 本身没有走索引,也可能是返回数据量过大,可能是连接池耗尽,也可能是锁等待、主从延迟、磁盘 IO 抖动,甚至可能是数据库被其他业务拖慢。

如果链路追踪显示数据库查询耗时明显升高,我会按下面的顺序继续排查:

  1. 先看慢 SQL 日志

    确认是不是某条 SQL 在问题时间点变慢,而不是所有 SQL 都变慢。

  2. 再看执行计划

    重点关注是否走索引、扫描行数是否异常、是否出现 Using filesortUsing temporary

  3. 检查索引是否匹配查询条件

    例如接口按 user_id + order_status + create_time 查询订单列表,但数据库只有 user_id 单列索引,数据量变大后就可能扫描大量记录。

  4. 检查 SQL 写法是否破坏索引

    常见问题包括对索引列使用函数、隐式类型转换、前置模糊匹配、OR 条件不合理等。

  5. 检查连接池和锁等待

    如果 SQL 执行本身不慢,但应用里耗时很高,就要看是不是获取数据库连接时发生了排队,或者事务中存在锁等待。

订单查询接口还有一个容易被忽略的问题:数据分布不均。 有些 SQL 在测试环境很快,是因为测试数据量太小;在线上对普通用户也很快,但对大客户很慢,是因为某个用户有几十万订单。此时单纯加索引可能只能缓解一部分问题,更合理的做法是限制查询范围、优化分页方式、按时间分区、冷热数据分离,或者把后台复杂查询和前台核心查询拆开。

如果 SQL 本身执行很快,但应用里数据库操作耗时很高,就要看连接池。很多人只盯着数据库执行时间,却忽略了应用从连接池获取连接也可能排队。如果 HikariCP 或 Druid 的活跃连接数打满,线程会卡在获取连接上,最终表现为接口变慢。这个时候盲目调大连接池不一定正确,因为数据库承载能力有限,连接池扩大可能把数据库打得更慢。

四、如果缓存异常,要区分命中率下降和 Redis 本身变慢

订单查询接口通常会用 Redis 缓存一些数据,例如订单摘要、用户权限、配置、商品信息等。如果接口突然变慢,缓存也是重点排查对象,但这里同样不能只问"Redis 挂了吗"。Redis 没挂也可能出问题,比如命中率下降、大 key 读取过慢、热 key 被打爆、网络抖动、连接池耗尽、缓存批量失效等。

我会先区分两类问题:

1. 缓存命中率下降

如果命中率突然从 95% 降到 50%,数据库压力上升就很合理,接口变慢也可能是缓存击穿或缓存雪崩导致的。比如某些热门订单、热门商品或配置 key 同时过期,大量请求直接穿透到数据库,数据库变慢后又反过来拖慢接口,最终形成连锁反应。

这类问题的修复不能只靠恢复 Redis,而要重新设计缓存策略:

  • 过期时间增加随机值,避免同一时间批量失效

  • 热点 key 使用逻辑过期或后台刷新

  • 缓存预热,避免流量一进来就打数据库

  • 对不存在的数据做空值缓存,防止缓存穿透

  • 必要时使用本地缓存兜底

2. Redis 本身操作变慢

如果命中率正常,但 Redis 操作本身耗时变高,就要看是不是存在大 key 或慢命令。比如一次查询从 Redis 取出一个很大的 Hash 或 List,平时数据小没问题,数据增长后序列化和网络传输都会变慢。

还有些代码会在请求链路上使用 keys、大范围 hgetall、批量反序列化复杂对象。这些问题在低流量时不明显,但线上流量一上来就会放大。缓存的意义是减少数据库压力,但如果缓存对象过大、使用方式不当,它本身也会变成性能瓶颈。

五、下游 RPC 慢时,要警惕超时和重试放大问题

订单查询接口经常依赖其他服务,比如物流服务、支付服务、优惠券服务、售后服务。某个下游服务变慢时,订单接口本身可能没有任何报错,但整体耗时会明显增加。更麻烦的是,如果调用方配置了不合理的超时和重试,下游慢会被进一步放大,最终把调用方线程池也拖垮。

例如订单查询接口调用物流服务,单次超时时间设置为 1 秒,重试 2 次。正常情况下物流服务 50ms 返回,用户没有感觉;一旦物流服务抖动,每次请求最多可能阻塞 3 秒。此时订单查询接口的总耗时变成 3 秒,并不是因为订单服务 CPU 高,也不是因为数据库慢,而是线程都在等下游返回。

这类问题我会重点看:

  • 每个下游服务的调用耗时

  • 超时次数和错误率

  • 重试次数是否异常增加

  • RPC 线程池是否打满

  • 是否有熔断、限流、降级策略

  • 慢请求是否集中在依赖某个下游的请求上

短期止血可以降低超时时间、关闭非核心字段、对弱依赖降级。比如物流状态查询失败时返回"暂不可用",而不是阻塞整个订单查询。长期来看,订单查询接口要区分核心依赖非核心依赖,不能因为一个展示字段拖慢整个主接口。对于非核心信息,可以异步加载、缓存兜底,或者在前端分块请求。

六、CPU 不高,不代表应用没有瓶颈

题目里提到 CPU 不是特别高,这个信息很关键。很多人看到 CPU 不高,就会排除应用问题,但这是不严谨的。CPU 不高只能说明机器没有大量计算,不代表线程没有阻塞。

接口变慢时,如果 CPU 不高,反而更应该关注:

  • IO 等待

  • 锁等待

  • 线程池排队

  • 连接池耗尽

  • 下游服务超时

  • 数据库慢查询

  • GC 停顿

Java 应用里大量性能问题不是"算得慢",而是"等得久"。线程可能在等数据库连接、等 Redis 连接、等 HTTP 响应、等锁、等队列消费、等磁盘 IO。此时 CPU 看起来很健康,但请求已经排队很久。

判断这种问题最直接的方式是看线程状态,可以通过 jstack、Arthas、JFR 等工具查看大量线程到底卡在哪里:

  • 如果大量线程停在 java.net.SocketInputStream.socketRead0,通常说明在等网络 IO,可能是数据库、Redis 或下游 HTTP 调用慢。

  • 如果大量线程卡在连接池获取连接的方法上,就要检查连接池配置和使用情况。

  • 如果大量线程处于 BLOCKED,说明可能存在锁竞争。

  • 如果线程数持续上涨,还要关注是否有线程泄漏、异步任务堆积或线程池隔离不合理。

常用排查命令可以包括:

复制代码
# 查看 Java 进程基本资源
top -Hp <pid>

# 查看线程栈
jstack <pid> > jstack.log

# 查看 GC 情况
jstat -gcutil <pid> 1000 10

# 查看 JVM 参数和运行状态
jcmd <pid> VM.flags
jcmd <pid> GC.heap_info

如果线上允许使用 Arthas,也可以用 tracewatchtt 这类命令观察方法耗时。不过我对这类工具的态度是谨慎使用,尤其是在高峰期,不要为了排查问题给线上系统增加新的风险。工具是放大镜,不是方向盘,前面的监控和链路分析仍然是基础。


七、线程池和连接池是线上慢请求的高发区

订单查询接口本身可能没有使用显式线程池,但整个系统一定依赖各种池化资源,例如 Tomcat 线程池、数据库连接池、Redis 连接池、RPC 客户端连接池、异步任务线程池。池化资源一旦打满,请求就会排队,排队时间会直接反映到接口响应时间上。

我会重点看几个指标:

池化资源 重点指标 常见问题
Tomcat 线程池 当前线程数、繁忙线程数、队列长度 请求进入应用前已经排队
数据库连接池 活跃连接数、等待次数、获取连接耗时 慢 SQL 或事务过长导致连接耗尽
Redis 连接池 活跃连接数、等待次数、命令耗时 Redis 操作慢或连接数不足
RPC 线程池 活跃线程数、队列长度、拒绝次数 下游慢导致线程被占满
业务线程池 活跃线程、队列长度、任务耗时 异步任务堆积,影响主链路

线程池问题最忌讳简单粗暴地调大参数。比如下游服务已经很慢,调用方把线程池从 100 调到 500,短期看排队减少了,但下游承压更大,超时更多,最终系统可能雪崩。连接池也是一样,数据库连接不是越多越好,连接过多会增加数据库上下文切换和锁竞争。

更合理的方式是:先找到阻塞原因,再决定是扩容、限流、降级、隔离还是优化慢操作。

八、GC 和内存问题也可能表现为接口变慢

接口突然变慢不一定伴随 OOM,GC 抖动也能让接口响应时间明显升高。如果应用频繁发生 Young GC,或者偶尔 Full GC,每次停顿几百毫秒甚至几秒,用户看到的就是接口卡顿。订单查询接口如果返回对象过大、DTO 组装复杂、一次性加载大量订单明细,可能会产生大量临时对象,加重 GC 压力。

我会先看 JVM 相关监控:

  • 堆内存使用趋势

  • Young GC 次数和耗时

  • Full GC 次数和耗时

  • 对象分配速率

  • 老年代增长趋势

  • 是否存在大对象分配

如果接口慢的时间点和 GC 停顿高度重合,说明 GC 是重要因素。接下来要看对象来源,可能是某个接口返回数据量突然变大,也可能是缓存本地对象没有淘汰,还可能是异步队列堆积导致大量任务对象留在内存中。

不过我不会一看到 GC 就立刻调 JVM 参数。很多 GC 问题本质上是业务代码造成的,比如无分页查询、一次性构造大列表、重复对象转换、日志打印大对象、缓存没有上限。调参数可以缓解,但如果对象创建量不受控,问题迟早会回来。对于订单查询这种接口,限制返回字段、控制分页大小、避免重复查询和重复转换,往往比单纯调大堆内存更有效。


九、不要忽略发布变更和配置变更

线上问题如果是"今天突然变慢",我一定会检查最近是否有发布、配置变更、数据库变更、索引变更、流量切换或依赖服务升级。很多性能问题并不是自然发生的,而是某次改动触发的。

常见触发点包括:

  • 新增字段,导致接口多调用了一次下游服务

  • 修改排序规则,导致 SQL 无法走原来的索引

  • 调整缓存过期时间,导致大量 key 同时失效

  • 新增埋点或日志,导致请求链路增加同步 IO

  • 配置了不合理的超时时间或重试次数

  • 发布后某段代码出现锁竞争或对象膨胀

  • 数据库表结构或索引被调整

排查时,时间线非常重要。接口从几点开始变慢,那个时间点前后发生了什么,这是定位问题的关键线索。如果问题发生在发布后,优先考虑回滚或灰度对比,而不是长时间在线上猜测。如果只有新版本实例慢,旧版本实例正常,原因基本就在代码或配置变更里。如果所有版本都慢,就要看共享依赖,比如数据库、Redis、网关、下游服务或机房网络。

我个人比较重视变更记录,因为它能把排查从"无限可能"变成"有限集合"。成熟团队应该能快速回答最近有哪些发布、哪些配置改动、哪些数据库操作、哪些流量调整。线上故障很多时候不是因为技术能力不够,而是因为缺少可追溯性。


十、一个比较完整的排查路径示例

假设收到告警:订单查询接口 P99 从 150ms 涨到 3.2s,持续 10 分钟。我的排查路径会是这样的。

1. 先看整体监控

我会先确认平均 RT、P95、P99 是否都上涨,错误率是否上升,QPS 是否异常。如果发现 QPS 基本稳定,错误率没有明显变化,但 P99 大幅上涨,说明不是全量请求都慢,更像是部分请求命中了慢路径,或者某个依赖偶发抖动。

2. 再看链路追踪

链路追踪显示慢请求主要耗时集中在 queryOrderList 这个数据库查询,单次耗时从几十毫秒上涨到 2 秒左右。此时排查方向就可以收敛到数据库查询,而不是继续怀疑所有组件。

3. 继续看慢 SQL 和执行计划

慢 SQL 显示新增了一个按 update_time desc 排序的查询,执行计划没有使用预期索引,而是扫描大量记录后排序。这里基本可以判断,SQL 查询模式发生了变化,现有索引不再匹配。

4. 关联发布时间线

对比发布时间,发现今天上午刚上线了一个需求:订单列表支持按更新时间排序。这个时候基本可以确认,接口变慢和新排序条件导致索引失效有关。

5. 先止血,再根治

短期处理上,我会优先评估是否回滚排序需求,或者临时关闭这个排序能力。如果不能回滚,可以限制查询时间范围,避免扫描历史全量数据,同时给接口加上降级策略,保护数据库。

长期处理上,需要根据真实查询模式设计联合索引,例如结合 user_idstatusupdate_time,同时评估分页方式是否合理。如果用户订单量很大,还要考虑游标分页、冷热分离或单独的查询模型。

这个过程看起来并不复杂,但关键点在于没有跳步骤。先确认现象,再拆链路,再看 SQL,再关联发布变更,最后做止血和长期修复。 真正的线上排查不是"我觉得是索引问题",而是"从监控和链路看,慢点集中在某条 SQL;从执行计划看,它没有走索引;从变更记录看,今天上线了相关排序需求;所以根因是新增查询模式缺少匹配索引"。


十一、线上处理要分清"止血"和"根治"

接口已经从 100ms 变成 3 秒时,第一目标不是写出完美方案,而是先降低影响面。止血和根治要分开看。

1. 止血手段

常见止血方式包括:

  • 回滚版本

  • 关闭非核心功能

  • 降低下游调用超时时间

  • 对弱依赖降级

  • 限制异常流量

  • 临时扩容应用实例

  • 隔离慢接口

  • 恢复或预热缓存

  • 终止异常 SQL

止血动作必须考虑风险,不能为了让接口快一点,把数据库或下游服务打崩。比如下游服务已经慢了,再盲目增加重试次数,只会让情况更差。

2. 根治方向

根治需要复盘根因,补监控,补压测,补索引,调整架构,完善降级策略。比如这次是 SQL 慢,就不只是加一个索引,还要检查为什么上线前没有发现,测试数据是否失真,SQL 审核是否缺失,慢 SQL 告警是否滞后,核心接口是否缺少链路耗时。

我认为一个成熟的后端工程师,不能只会定位问题,还要能把问题沉淀成系统能力。线上慢接口背后往往暴露的是监控不足、容量评估不足、变更管理不足、代码边界不清晰、依赖隔离不充分。解决一个线上问题的最佳结果,不是"这次好了",而是"以后类似问题更早发现、更容易定位、影响更小"。

十二、总结回答

如果这是一个面试问题,我会这样组织答案:

首先确认慢的范围,看是单接口慢还是全站慢,是所有用户慢还是部分用户慢,是平均耗时变高还是 P99 变高。然后看监控和链路追踪,把 3 秒耗时拆成网关、应用、缓存、数据库、下游 RPC、序列化等阶段,先定位主要耗时在哪里,而不是直接猜 SQL。

接着针对具体瓶颈深入排查。如果是数据库,就看慢 SQL、执行计划、索引、锁等待和连接池。如果是下游服务,就看超时、重试、熔断和线程池。如果 CPU 不高但接口慢,就重点看线程阻塞、连接池等待、IO 等待和 GC。

在处理上,我会先做止血,例如回滚变更、关闭非核心能力、限流降级、降低超时、恢复缓存或临时扩容,避免影响继续扩大。问题稳定后再做根因分析,结合发布时间线、执行计划、线程栈、GC 日志和依赖监控确定最终原因。

最后要补上长期治理,包括:

  • 核心接口链路耗时日志

  • 慢 SQL 告警

  • 线程池和连接池监控

  • 压测覆盖

  • 变更审查

  • 降级预案

  • 核心链路容量评估

总结

订单查询接口从 100ms 变成 3 秒,本质上不是一个单点技术问题,而是一次完整的线上诊断。排查时最重要的是不要被表象牵着走,CPU 不高不代表系统没问题,日志没报错不代表链路没阻塞,数据库没挂也不代表 SQL 没变慢。

正确做法是先确认问题范围,再通过链路追踪拆分耗时,然后围绕数据库、缓存、下游服务、线程池、连接池、JVM 和发布变更逐层定位。排查过程中要始终坚持一个原则:不要靠猜,要靠证据;不要只止血,要能沉淀。

我一直认为,后端工程师的线上排查能力,核心不在于记住多少命令,而在于能不能把一个模糊的问题变成一个具体的问题。所谓"接口慢",最终要落到:哪一类请求,在什么时间点,因为哪一个依赖或哪一段代码,导致哪一种资源等待或耗时增加。 只有定位到这个程度,优化才有意义。否则所谓优化,很可能只是碰运气。

相关推荐
ltlovezh8 小时前
AAC 元数据:ADTS 与 ASC 的区别、转换和常见坑
后端·ffmpeg·音视频开发
zuowei28898 小时前
编程语言对比:C/C++/Java/C#/PHP
java·c语言·c++
百数平台8 小时前
功能更新——百数详情页“数据简报”与“关联标签页”配置指南
java·服务器·前端
接着奏乐接着舞9 小时前
java lambda表达式
java·开发语言·python
金銀銅鐵9 小时前
[Java] 自己写程序,来解析字段的 descriptor
java·后端
ch.ju9 小时前
Java程序设计(第3版)第四章——成员方法
java·开发语言
椰椰椰耶9 小时前
[SpringCloud][8]Spring Cloud LoadBanlancer快速上手以及LoadBalancer原理
后端·spring·spring cloud
Dicky-_-zhang9 小时前
微服务安全防护实战:OAuth2与JWT鉴权
java·jvm
牙牙学语的阿猿9 小时前
sentinel创建规则时的坑
java·开发语言·sentinel