CPU占用不高但接口延迟高?从等待阻塞到根因排查指南 “监控面板上 CPU 只有 15%接口平均延迟却从 50ms 涨到 5sp99 更夸张。你第一反应是看 CPUCPU 不高看内存内存没满看磁盘磁盘没满。但用户端的体感就是慢甚至超时。”这种故障在线上排障里非常典型也是最容易走弯路的一类。先放一个结论接口延迟高不代表 CPU 一定要高。CPU 15% 是现象不是根因。它至少说明当前系统没有在大量执行计算问题大概率出在“等待”上——等待锁、等待 IO、等待连接池、等待下游服务、等待 JVM 的 GC 停顿结束。这篇文章围绕“CPU 不高但接口延迟高”这一个排障命题把现象的拆解方式、常用命令、判断依据和应急手段完整讲清楚。文章不只讲原理还会给出一套从系统层、JVM 层、中间件层逐层下探的排查路径以及一份可以直接拿去用的命令速查和应对清单。1. 故障画像与排障思路速览先给这种故障做一个统一的画像方便对号入座。观测项现象推断方向CPU 整体使用率15% 左右计算不是瓶颈接口 RT明显上涨甚至超时存在等待或阻塞QPS可能不高也可能在波动流量不一定是主因内存/磁盘表面看可能都正常容易漏掉 IO 和 GC线程状态大量 BLOCKED / WAITING锁竞争、连接池、线程池排队调用链某一环耗时突刺慢 SQL、Redis、下游服务容器场景CPU 配额被限流cgroup throttle排障的核心思路可以压缩成一句话延迟 执行时间 等待时间。CPU 低说明执行时间没有爆炸那一定是在等什么。基于这个思路排查路径一般按下面几条线走先确认 CPU 统计没有看错区分 us / sy / wa / st并检查多核热点。用调用链或日志拆解接口耗时分布定位耗时集中段。抓线程栈看业务线程到底卡在哪个方法、哪种状态。顺着“等待源”检查数据库、缓存、线程池、GC、下游依赖、容器限流。保留现场先恢复服务再做根因治理。2. 适用场景什么时候会遇到“CPU 低但延迟高”这类问题不是只在极端大促场景出现日常运维里遇到的频率比想象中高得多。比较典型的触发场景包括线上偶发慢请求CPU 和内存看起来都正常。压测时 QPS 还没打上去RT 先涨了一截。高峰期服务出现大量超时但监控面板 CPU 一直不高。同一个服务里只有某个接口慢其他接口正常。还有一类很误导人的情况系统本身的 CPU 基线就低。比如 IO 密集型的网关服务、订单服务、内容服务CPU 常态可能就在 5% 到 15% 之间。这种系统如果 CPU 突然飙到 80%反而是非常明确的信号但如果 CPU 维持 15% 而 RT 持续上升就说明系统正在被某种等待资源拖住。反过来也要说清楚这套排查思路不适用于什么场景。如果是纯粹的 CPU 密集计算型瓶颈比如图片处理、加解密、复杂算法CPU 会直接打满根本不会停留在 15%。那类问题优先看的是代码热点和线程栈不是本文讨论的“等待型瓶颈”。3. 第一步别被整体 CPU 骗了——细分指标与多核热点很多排障从第一步就错了。看到一个 15% 就认为“CPU 没压力”然后开始怀疑网络或者瞎重启。正确的做法是先拆解 CPU 统计确认这个 15% 到底是什么状态下的 15%。3.1 top 看总体分布执行top后重点看 %Cpu(s) 这一行的几个值us用户态 CPU真正跑业务代码的。sy内核态 CPU系统调用、线程切换、中断处理。wa等待 IO包括磁盘、网络文件系统等。st被宿主机偷走的时间常见于云主机和容器。如果 us 不高但 sy 或者 wa 占了很大比例那么“CPU 不高”这个结论就不等于“CPU 不忙”。比如 wa 高说明大量线程在等磁盘读写这时候 RT 高是必然的但 CPU 统计看起来确实不高。top按1可以看到每个逻辑核的使用率按x可以高亮排序。如果整体 15%但某个核已经 100%就是典型的单线程热点问题。3.2 多核场景下“15%”不等于“没有热点”一台 32 核的机器整体 CPU 15% 意味着大概有 4.8 个核在运行。如果你的应用是单线程模型或者某个热点逻辑集中在一个线程上这个线程把一个核打满整体 CPU 也只有 3% 左右。这个热点会被整体百分比完全掩盖。所以要抓到单核热点用top -H按线程维度看top -H -p pid这里pid是应用进程号。如果某个线程长期占用 CPU记下线程号然后通过jstack把线程号转成十六进制在堆栈里搜索定位到具体代码。3.3 vmstat 看运行队列和上下文切换CPU 百分比之外还要看系统的调度和 IO 等待情况。vmstat是关键命令vmstat 1 3重点看这几列r可运行线程数。如果这个值长期大于 CPU 核数说明线程在排队等待调度。b不可中断睡眠状态的线程数。这个值不为 0 时往往说明有线程在等磁盘 IO 或内核资源。cs上下文切换次数。如果这个值非常高说明系统在频繁切换线程大量 CPU 时间被消耗在切换本身而不是实际计算上。waIO 等待占比。pidstat可以按进程或线程看上下文切换pidstat -w -p pid 1如果某一线程的 cswch主动切换或者 nvcswch被动切换非常高说明这个线程频繁被抢占或者在频繁让出 CPU。4. 第二步用调用链拆解耗时分布确认系统层指标之后下一步是回答一个关键问题接口总耗时到底花在了哪一段定位延迟问题最理想的工具是分布式链路追踪比如 SkyWalking、Zipkin、Jaeger。只要一个 Trace 能还原完整的 Span 列表就可以直接看到数据库耗时、Redis 耗时、下游 RPC 耗时分别是多少。如果公司有成熟的调用链平台这一步是最快的。如果没有链路追踪可以在应用日志中按阶段手动打印耗时。下面是一段 Java 伪代码示例实际项目替换成自己业务阶段即可long t0 System.currentTimeMillis(); // 第一阶段解析 token String userId userService.getUserIdByToken(token); long t1 System.currentTimeMillis(); // 第二阶段查询订单列表 ListOrder orders orderService.queryByUserId(userId); long t2 System.currentTimeMillis(); // 第三阶段组装返回结果 OrderVO vo orderConverter.convert(orders); long t3 System.currentTimeMillis(); log.info(orderQuery cost | tokenParse{}ms | orderQuery{}ms | convert{}ms, t1 - t0, t2 - t1, t3 - t2);这样一次请求的耗时分布就一目了然。如果orderQuery阶段耗时占了 80%下一步就直奔数据库如果tokenParse阶段耗时异常则要查认证服务和 Redis。除了应用侧网络侧的耗时也可以用curl拆分。-w参数可以直接输出 DNS、TCP 建连、首字节时间和总耗时curl -s -o /dev/null -w dns%{time_namelookup} connect%{time_connect} ttfb%{time_starttransfer} total%{time_total}\n http://127.0.0.1:8080/api/xxx如果connect很高说明网络建连或 socket backlog 有问题如果connect正常但ttfb很高说明服务端处理慢问题在应用后面。这里还要强调一点不要只看平均延迟。CPU 15% 可能是长时间均值但高延迟可能只集中在某个时间窗口。一定要看 p50、p95、p99 的时间序列。如果一个接口 p50 是 50msp99 是 5s说明只有少数请求被阻塞这通常对应线程池满、锁竞争、GC 停顿这类“局部阻塞”问题而不是整体资源不足。5. 第三步线程 Dump 确认等待方向系统指标和调用链能把范围缩小到一个阶段但真正定位线程卡在哪里还需要抓线程栈。这一步几乎是低 CPU 高延迟排障的“必经之路”。抓线程栈建议连续抓三次间隔 5 秒for i in 1 2 3; do jstack -l pid /tmp/jstack_$(date %s).txt sleep 5 done连续抓三次的核心目的是排除瞬时噪声。如果三次都看到同一批线程卡在同一个位置基本可以断定那里就是瓶颈。拿到线程栈后重点看java.lang.Thread.State和堆栈里的方法名RUNNABLE不一定在计算也可能是在网络读取等 IO 操作。BLOCKED (on object monitor)在等锁可能是业务锁、数据库行锁、分布式锁。WAITING (parking)在等待被唤醒常见于线程池或LockSupport.park。TIMED_WAITING在 sleep 或者带超时的等待。如果堆栈中出现大量BLOCKED需要结合锁对象判断。如果大量线程卡在“获取数据库连接”的位置下一步查连接池和慢 SQL如果卡在 Redis 调用下一步查 Redis 可用性如果卡在某个业务 synchronized 或 ReentrantLock 上那就要看这个锁的竞争范围。Arthas 在线上排查锁问题时有优势thread -n 3 thread -bthread -b可以直接找出阻塞其他线程的线程省去人工比对线程栈的时间。还有一个细节容易被忽略线程状态是RUNNABLE不代表线程在干活。比如一个线程正在等待网络 socket 数据时Java 线程状态也是RUNNABLE。所以不要一看到 RUNNABLE 就认定它是 CPU 热点要结合 CPU 占用和堆栈中的系统调用方法一起看。6. 低 CPU 高延迟的八类高频根因基于前面的排查路径这类问题最终通常落在这八个根因上。下面逐一拆解每个根因都会给出现象、定位方式和解决方向。6.1 数据库慢查询与连接池排队这是最常见的根因之一。业务线程池里的线程原本有 200 个但数据库连接池只有 50 个一旦 50 个连接全被慢 SQL 占住后面的 150 个线程只能排队等连接。在监控上数据库 CPU 可能不高但连接池活跃连接数直接打满接口 RT 全线飙高。定位方式看数据源监控里的活跃连接数和等待获取连接数。执行SHOW PROCESSLIST查看当前数据库会话。开慢查询日志找耗时高的 SQL。SHOW PROCESSLIST;如果看到大量Sleep或者同一类 SQL 长时间Sending data基本就是慢 SQL 或锁等待。解决方向包括补索引、优化 SQL 写法、拆分大事务、增加只读副本、调整连接池参数。6.2 缓存击穿/雪崩导致流量打到数据库缓存场景下如果热点 key 在同一时间失效或者 Redis 集群出现抖动大量请求直接穿透到数据库。数据库连接一旦被打满后续请求全部排队。这时候 CPU 同样不会很高因为线程都卡在等待数据库返回。定位方式看缓存命中率确认是否出现断崖式下降。看 Redis 慢日志和客户端报错。看数据库活跃连接数是否异常上涨。redis-cli --latency redis-cli SLOWLOG GET 10解决方向包括缓存预热、缓存过期时间加随机值、热点 key 用互斥锁重建、加多级缓存做兜底。6.3 线程池/信号量打满Tomcat 线程池、业务线程池、RPC 线程池都有容量上限。如果某个下游接口变慢调用它的线程全部阻塞等待线程池很快就满。新的请求进不来直接排队甚至触发拒绝策略。定位方式看线程池监控中的活跃线程数、队列长度、拒绝次数。线程 Dump 中看到大量线程WAITING (parking)。看日志里有没有RejectedExecutionException。解决方向是给不同业务隔离线程池下游慢时不影响核心链路同时考虑异步化把非核心逻辑从同步调用链中摘出去。线程池本身要加上监控告警不能等打满了才发现。6.4 锁竞争严重锁竞争是低 CPU 高延迟里比较隐蔽的一类。并发不高时每次拿锁很快高并发一上来同一把锁被多个线程争抢线程大量进入BLOCKED状态。CPU 不高因为线程不是在计算而是在排队等锁。定位方式jstack中大量BLOCKED (on object monitor)。Arthas 的thread -b直接找阻塞源。如果是分布式锁看 Redis 的锁等待耗时。解决方向包括缩小锁粒度、用读写锁替代互斥锁、用 CAS 或无锁数据结构替代锁、避免在锁内做 IO 操作。数据库层面的行锁竞争则需要从事务和索引设计上解决。6.5 JVM GC 停顿GC 停顿是“CPU 看似不高但延迟飙高”的经典场景。Full GC 或频繁 Young GC 会导致业务线程长时间冻结。GC 发生时 CPU 会有一波波动但如果你看的是一段时间平均值很可能显示只有 15%根本抓不到那波尖峰。定位方式看 GC 日志重点看 FGC 次数和 FGC 耗时。用jstat实时观察堆内存和 GC 指标。jstat -gcutil pid 1000 5输出里的FGC和FGCT如果持续上涨说明老年代频繁回收。再看堆内存分配情况确认是否存在大对象、内存泄漏、过大的缓存未清理等问题。解决方向包括调整堆大小、更换适合的 GC 器、优化对象分配必要时用内存 dump 分析泄漏点。6.6 磁盘 IO 抖动与内存换页磁盘 IO 是另一个系统性根因。日志写入量突然变大、临时文件写盘、或者系统开始使用 swap都可能导致线程阻塞在 IO 上。此时 CPU 不高因为线程都在等磁盘但 RT 明显上涨。定位方式vmstat看 wa 列和 si/so 列。iostat -x 1 3看磁盘 util 和 await。dmesg查看是否有线程长时间阻塞的日志。iostat -x 1 3解决方向包括日志异步化、减少无用日志输出、把临时文件放到内存盘、调整 swap 策略、排查是否存在磁盘满或 inode 耗尽。6.7 下游依赖变慢与重试放大微服务架构下一个接口往往依赖多个下游服务。下游服务一个节点变慢上游所有调用线程都会被拖住。如果再配置了不合理的超时重试故障还会被放大。定位方式调用链上确认下游 Span 耗时。RPC 监控里看下游成功率、耗时和重试次数。看应用日志里下游调用的耗时分布。解决方向是给所有下游调用设置合理的超时时间重试要做退避且只在幂等接口上开启。核心链路要对非核心依赖做熔断降级避免一个下游拖垮整个服务。6.8 容器 CPU 配额限流容器场景下还有一种容易被忽略的情况宿主机 CPU 占用不高但容器本身的 CPU 配额已经用满了。调度器会对容器做 CPU throttle线程明明可以执行却要等 CPU 时间片延迟自然升高。定位方式查看容器的 CPU 限额和实际使用。查看 cgroup CPU 统计观察 nr_throttled 是否持续增长。cat /sys/fs/cgroup/cpu.stat如果nr_throttled和throttled_time持续增长说明容器 CPU 配额已经成为瓶颈。解决方向是调整容器规格、优化代码减少 CPU 消耗或者把高峰流量分散到多个实例。7. 常用排查命令与判断口径速查下面把排障中会用到的高频命令汇总到一起。建议直接存在本地备忘遇到同类问题时按顺序执行。7.1 系统层# 总体状态CPU/负载/内存 top # 按线程看 CPU top -H -p pid # 运行队列、IO 等待、上下文切换 vmstat 1 3 # 磁盘 IO iostat -x 1 3 # 按进程看上下文切换 pidstat -w -p pid 1看到的现象判断方向wa 高磁盘 IO 瓶颈st 高宿主机 CPU 争抢r 大于核数线程调度排队cs 持续很高线程切换频繁或锁竞争严重7.2 JVM 与应用层# GC 情况 jstat -gcutil pid 1000 5 # 线程栈建议连续抓三次 jstack -l pid /tmp/jstack_$(date %s).txt # Java 8 之前查看堆配置 jmap -heap pid # 堆转储用于后续分析 jmap -dump:live,formatb,file/tmp/heap.hprof pidArthas 两个高频命令thread -n 3 thread -b7.3 中间件与网络# MySQL 当前会话 mysql -e SHOW PROCESSLIST; # MySQL 连接数 mysql -e SHOW GLOBAL STATUS LIKE Threads_connected; # Redis 延迟和慢日志 redis-cli --latency redis-cli SLOWLOG GET 10 # 本机连接数 ss -s ss -antp | grep port # HTTP 接口分阶段耗时 curl -s -o /dev/null -w dns%{time_namelookup} connect%{time_connect} ttfb%{time_starttransfer} total%{time_total}\n http://127.0.0.1:8080/api/xxx8. 快速排障清单从现象到根因故障发生时建议按这张清单顺序走可以节省大量试错时间。步骤动作观察到什么初步结论1top看 us/sy/wa/stwa 高磁盘 IO 瓶颈2vmstat看 r/b/csr 高或 b 不为 0线程排队或 IO 阻塞3抓三次线程 Dump多数线程 BLOCKED锁竞争或连接池满4抓三次线程 Dump多数线程 WAITING线程池排队或等待资源5SHOW PROCESSLIST同一 SQL 长时间执行慢 SQL 或行锁6查看 GC 日志/jstatFGC 次数和耗时上涨内存分配或泄漏问题7查看调用链下游 Span 耗时长下游依赖变慢8查看容器 cpu.statthrottled 持续增长容器 CPU 配额限流如果排查后仍然没有定位不要继续猜。把线程 Dump、GC 日志、慢 SQL 日志、网络抓包一起保留找更多现场信息后再深入分析。9. 故障中的应急手段与现场保留排障首先要保证业务恢复其次才是根因分析。故障正在进行时动作优先级建议如下限流降级如果确认是流量冲击或依赖恶化先在入口限流把异常流量挡在系统外。摘除异常节点如果某个实例表现异常先从负载均衡中摘掉让流量打到健康节点。切换依赖如果是数据库或下游服务问题考虑切到从库或备用集群。扩容或提升配额如果是容器 CPU 配额或线程池不足快速扩容实例比修改代码快得多。保留现场这一步容易遗漏。在重启或变更前至少保存线程 Dump、GC 日志、慢查询日志和监控截图。这里要特别提醒不要一上来就重启。重启可以恢复服务但也会销毁线程栈、堆状态这些宝贵的排障证据。如果最终必须重启请先执行“三连 jstack”和jstat -gcutil把这些命令的输出保存下来再操作。10. 长效治理与下一步低 CPU 高延迟这类问题单靠一次排障解决不了所有隐患。故障恢复后建议从下面几个方向做长期治理关键接口要输出分阶段耗时日志至少要能看到数据库、Redis、下游调用的耗时分布。把 p50、p95、p99 纳入监控只看平均延迟会漏掉大部分问题。线程池、连接池、GC 指标都要有监控和告警不要等打满了才发现。GC 日志和线程 Dump 要定时归档至少保留 7 天。下游依赖必须配置超时和熔断重试策略要做退避。容器环境要监控 cgroup 的 CPU throttle不能只看宿主机指标。定期做全链路压测压出“CPU 不高但 RT 飙高”的拐点。下次监控面板再出现 CPU 15%、RT 突刺的情况按这个顺序执行先top看 us/sy/wa/st再vmstat看 r/b/cs然后三连jstack再SHOW PROCESSLIST和redis-cli SLOWLOG。十分钟内基本能把根因范围缩小到数据库、缓存、锁、GC、下游、容器限流中的一个。把这套命令存下来比临时翻监控面板有用得多。