场景:
线上有一个订单查询接口,平时响应 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 抖动,甚至可能是数据库被其他业务拖慢。
如果链路追踪显示数据库查询耗时明显升高,我会按下面的顺序继续排查:
-
先看慢 SQL 日志
确认是不是某条 SQL 在问题时间点变慢,而不是所有 SQL 都变慢。
-
再看执行计划
重点关注是否走索引、扫描行数是否异常、是否出现
Using filesort、Using temporary。 -
检查索引是否匹配查询条件
例如接口按
user_id + order_status + create_time查询订单列表,但数据库只有user_id单列索引,数据量变大后就可能扫描大量记录。 -
检查 SQL 写法是否破坏索引
常见问题包括对索引列使用函数、隐式类型转换、前置模糊匹配、
OR条件不合理等。 -
检查连接池和锁等待
如果 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,也可以用 trace、watch、tt 这类命令观察方法耗时。不过我对这类工具的态度是谨慎使用,尤其是在高峰期,不要为了排查问题给线上系统增加新的风险。工具是放大镜,不是方向盘,前面的监控和链路分析仍然是基础。
七、线程池和连接池是线上慢请求的高发区
订单查询接口本身可能没有使用显式线程池,但整个系统一定依赖各种池化资源,例如 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_id、status、update_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 和发布变更逐层定位。排查过程中要始终坚持一个原则:不要靠猜,要靠证据;不要只止血,要能沉淀。
我一直认为,后端工程师的线上排查能力,核心不在于记住多少命令,而在于能不能把一个模糊的问题变成一个具体的问题。所谓"接口慢",最终要落到:哪一类请求,在什么时间点,因为哪一个依赖或哪一段代码,导致哪一种资源等待或耗时增加。 只有定位到这个程度,优化才有意义。否则所谓优化,很可能只是碰运气。