Appearance
生产问题:接口 RT 飙升但 CPU 不高时怎么排查
很多线上接口超时问题,最迷惑人的地方在于:
- CPU 不高
- 机器负载不夸张
- 服务没有直接挂掉
- 但 RT 一直上涨,请求开始排队
这类问题往往不是“机器忙不过来”,而是某个关键资源被卡住了。
先说结论
接口 RT 飙升但 CPU 不高时,最实用的排查顺序通常是:
- 先确认是整体变慢,还是少数接口变慢
- 再看线程是不是在等待数据库、Redis、HTTP 或锁
- 再看线程池、连接池、队列有没有被占满
- 最后再回到 SQL、缓存命中率和下游依赖上找根因
这种问题大多数都属于:
- 等待时间太长
- 排队时间太长
- 少量慢点把整条链路拖住了
更进一步说,这类问题最重要的不是证明“机器不忙”,而是找出:
- 线程到底在等谁
- 请求到底卡在哪一段
- 是执行慢,还是排队慢
一、先把现象分清楚
先判断下面几件事:
- 是单个接口变慢,还是整个服务都慢
- 是偶发尖刺,还是持续性上涨
- 是入口 RT 变高,还是下游调用耗时变高
如果只有某一个接口慢,优先怀疑:
- SQL
- 缓存
- 远程调用
- 锁竞争
如果整个服务一起慢,优先怀疑:
- 线程池
- 数据库连接池
- Redis 连接池
- 某个公共下游抖动
还要补一个判断:
- 是高峰流量触发的
- 还是最近发版、配置变更、缓存失效触发的
二、为什么 CPU 不高也会超时
因为很多接口不是在做计算,而是在等。
常见等待点包括:
- 等数据库连接
- 等 SQL 返回
- 等 Redis 返回
- 等 HTTP 或 RPC 下游
- 等锁
- 等线程池排队
所以 CPU 低并不能说明服务没问题,它只能说明:
- 线程并没有一直在执行计算
但线程很可能大量卡在 WAITING、TIMED_WAITING 或阻塞 IO 上。
这也是很多线上排障最容易绕偏的地方:
- 机器不高,不代表服务不慢
- 线程没在算,不代表线程没被占住
三、先看线程在等什么
这一类问题里,线程栈通常很有价值。
重点看:
- 线程都卡在什么调用栈
- 是不是大量线程都停在同一个方法
- 是不是有很多线程卡在连接池获取、网络读写或锁等待
如果大量线程都卡在:
getConnectionsocketReadFuture.getCountDownLatch.awaitLockSupport.park
通常就说明:
- 不是 CPU 问题
- 是等待链路出了问题
如果线程栈里大量出现同一类等待点,通常已经能把问题从“整个系统都慢”缩小到某一层。
四、最常见的四类根因
1. 下游接口变慢
这是最常见的一类。
比如:
- 订单服务查用户画像
- 用户服务又调推荐服务
- 推荐服务再查 ES
只要中间某一跳 RT 抖起来,入口接口就会被一起拖慢。
这时常见特征是:
- 本服务 CPU 不高
- 业务线程都在等下游返回
- 超时数逐渐升高
2. 数据库慢查询或连接池耗尽
数据库问题的典型表现是:
- 某一批接口一起慢
- 线程大量卡在取连接或执行 SQL
- 应用日志出现连接获取超时
很多人会先加连接池大小,但如果根因是慢 SQL,连接数加大后往往只是把数据库压得更狠。
3. 缓存命中率下降
比如:
- 热 Key 失效
- 批量 Key 同时过期
- Redis 抖动导致大量请求回源
这时业务线程会突然把流量压到数据库上,进而出现:
- 数据库变慢
- 线程池排队
- 接口 RT 一起上涨
4. 线程池和异步编排设计不合理
很常见的链路是:
- 入口请求进来
- 扔到异步线程池
- 线程池里再调多个下游
- 某个下游超时后线程迟迟不释放
最终表现为:
- 线程池队列堆积
- 拒绝策略开始触发
- 主线程也被拖慢
5. 锁竞争或串行化热点
比如:
- 某个本地锁粒度太大
- 某个用户、订单、资源被串行处理
- 某个共享对象争用严重
这类问题 CPU 也不一定高,但等待会很明显。
五、一个更实用的排查顺序
第一步:看监控面板
先同时看:
- RT
- QPS
- 错误率
- 超时数
- 线程池活跃数
- 数据库连接池使用率
- Redis 和 HTTP 客户端连接池使用率
如果 RT 上涨而 QPS 没明显增加,通常更像是:
- 下游变慢
- 线程被卡住
第二步:看线程栈
目标不是看所有线程,而是找出:
- 大多数业务线程卡在哪一层
如果大量线程集中停在同一处,根因方向通常已经比较清楚了。
第三步:看入口日志与下游耗时
重点是把接口总耗时拆开:
- 业务计算耗时
- SQL 耗时
- Redis 耗时
- RPC 耗时
如果有网关、BFF、聚合层,还应该把:
- 网关排队
- 下游并发等待
- 序列化/反序列化
也拆出来看。
很多问题一拆就能看见:
- 真正慢的不是接口本身
- 而是某个子步骤突然变慢
第四步:确认有没有排队
要特别看:
- 线程池队列长度
- Tomcat 或 Undertow 工作线程状态
- 数据库连接池等待时间
接口超时很多时候不是执行慢,而是:
- 前面已经排了太久
第五步:看最近有没有变更
这一步很容易被忽略,但线上特别重要:
- 最近是否发版
- 是否改过线程池参数
- 是否改过超时配置
- 是否有缓存失效、索引变更、下游切流
很多 RT 问题不是自然发生,而是某个小改动把等待链路拉长了。
六、一个典型案例怎么理解
比如订单详情接口平时 80ms,某次活动期间涨到 2s。
排查后发现:
- CPU 只有 35%
- 线程池活跃线程接近上限
- 大量线程卡在调用用户画像服务
- 用户画像服务又因为 Redis 热点失效回源数据库
最后真正的问题不是订单接口本身,而是:
- 热点缓存失效
- 下游回源变慢
- 上游线程池被拖住
这类问题非常典型,说明接口超时要看整条调用链,而不是只盯入口接口。
七、怎么治理更稳
更稳妥的治理方式通常包括:
- 为下游调用设置明确超时
- 业务线程池按任务类型拆分
- 关键缓存做热点保护和过期抖动
- SQL 和慢查询治理常态化
- 入口增加降级、限流和兜底逻辑
如果是聚合接口,还要特别注意:
- 不要无限制并发下游
- 不要在异步线程里堆阻塞操作
还要补两件事:
- 给关键等待点做分段埋点
- 让线程池、连接池和下游超时有明确边界
一句话总结
接口 RT 飙升但 CPU 不高时,最常见的真相不是“服务没压力”,而是:
- 线程在等待
- 请求在排队
- 下游在变慢
把线程状态、连接池、线程池、排队时长和下游耗时一起看,通常会比只盯机器指标有效得多。