CPU 15%但延迟飙升?低CPU高延迟排查指南

发布时间:2026/9/8 4:26:49
CPU 15%但延迟飙升?低CPU高延迟排查指南 线上群在晚上十一点突然热闹起来。监控面板上核心接口的 p99 延迟从平时的 80 毫秒一路拉到 3 秒错误率开始抬头。你下意识地切到主机监控页想看看资源是不是被打满了。CPU 使用率15%。刷新一次还是 15%。这时候团队里很容易出现分歧有人觉得是监控打点出了问题有人开始怀疑网络被限速还有人直接想把集群扩容一倍。但在动手之前需要先把一个基本判断做对当 CPU 只有 15% 而接口延迟飙升时问题大概率不是“算不过来”而是“在等”。这不是一个反直觉的巧合而是线上排障里非常经典的一类现象。CPU 指标只描述了一件事这台机器每秒真正在执行指令的比例。它没有告诉你你的业务线程此刻有多少比例是阻塞的、等待的、挂起的。延迟是请求从进入到返回的总耗时它由“在 CPU 上执行的时间”和“不在 CPU 上执行的时间”共同组成。CPU 低恰恰说明大量线程根本没在跑而是卡在某个资源的门口排队。这篇文章不打算给你一个万能排查脚本而是想把这个问题的排查链路拆完整低 CPU 高延迟到底意味着什么、最容易藏在哪几个瓶颈里、应该按什么顺序查、查完之后怎么避免下次再来一次。1. 先别急着加机器这个问题真正要查的是“线程在等什么”很多同学拿到“CPU 15%延迟飙升”这个组合第一反应是扩容。我见过不止一次加完机器 P99 不但没降反而因为并发抬高把下游数据库压得更狠。原因很简单如果瓶颈不是算力加的每一台机器都只是给同一个拥堵点增加了更多的排队者。1.1 一次请求的延迟大部分时间可能不是在执行从线程状态的角度看一个请求从进入到返回线程只会在三种状态里切换RUNNABLE正在 CPU 上跑或者等待被调度到 CPU 上。WAITING / TIMED_WAITING主动或被动地等某个条件比如等锁、等队列、等 sleep 结束。BLOCKED被锁挡住拿不到 monitor。当 CPU 只有 15% 时意味着系统中还有 85% 的计算资源是空闲的但大量线程既不 RUNNABLE 也不在跑计算。它们停在那里唯一的解释是等某个东西。所以排查任务的核心就变成了回答一个问题线程在等什么这个问题CPU 监控答不了内存监控也答不了。能回答的只有线程转储thread dump、连接池指标、GC 日志和下游依赖的耗时画像。1.2 “CPU 15%”这个数字本身也可能是假象在深入排查之前先花三十秒确认这个数字到底是怎么测出来的。我见过多次监控没问题、数据链路出错的情况。多核机器上15% 可能是“全部核的平均值”。8 核机器 12.5% 就是满打满算跑满了一个核如果很多任务其实挤在单线程或单核路径上平均 CPU 就被稀释了。容器场景下CPU 使用率还要看 cgroup 的 quota 和 throttle。宿主机的 CPU 百分比和容器视角可能不一致出现过“外部监控 15%容器内其实一直在被限流”的情况。监控采集的是整机平均值而你的服务可能只部署在其中一台宿主机上或者采集间隔太长把瞬时尖峰抹平了。所以看到 CPU 15%不要直接跳进“资源没问题”的结论。先用top、uptime、pidstat这类命令确认负载均值load average、运行队列r 列、以及目标进程的真实 CPU 占用。如果这些也确实很低再往下走。排障的第一原则先确认现象本身是真实的再开始解释现象。数字没核实清楚后面每一步都可能是白做。2. 低 CPU 高延迟的四个常见“隐形瓶颈”当确认算力确实没打满接下来要逐一排查的就是那些能让线程“原地等待”的资源。以我个人经验来看出现频率最高的是四类线程池耗尽、连接池耗尽、锁竞争、IO 阻塞含网络和磁盘。它们有一个共同点完全不体现在 CPU 上只体现在线程状态和等待时长上。2.1 线程池耗尽活都排到队列里了机器却闲着Web 容器、RPC 框架、异步处理框架本质上都是一个线程池在接活。常见的情况是Tomcat 的 max-threads、Dubbo 的 threads、或者业务自定义线程池被打满后新的请求只能排队。特征很典型CPU 不高。接口响应时间上升但错误率未必马上涨因为请求在排队。如果队列有界排满后开始抛 RejectedExecutionException 或者连接直接超时。验证方式在 JVM 技术栈里非常直接jstack pid | grep -A 30 http-nio | head -50或者jcmd pid Thread.print /tmp/threaddump.txt看线程栈中大量线程是不是停在类似ThreadPoolExecutor.getTask()或AbstractQueuedSynchronizer.park()的位置。如果大量业务线程都是WAITING状态而它们的池子又是同一个基本可以判断线程池满了。注意线程池被打满往往不是线程池本身配得不对而是下游变慢导致每个任务执行时间变长。一个原本 100ms 的任务变成 3s同样的 QPS 下线程池占用自然飙升。2.2 连接池耗尽数据库连接、HTTP 连接、Redis 连接都在排队线程池满了往往能指向一个更底层的原因你的线程拿着连接在等一个慢查询或者等一个慢的第三方接口。这些连接长时间不释放连接池就被占满。最常见的现象是日志里出现HikariPool-1 - Connection is not available, request timed outJedisConnectionException: Could not get a resource from the poolorg.apache.http.conn.ConnectionPoolTimeoutException如果用的是数据库连接池去数据库侧看一眼就知道SHOW PROCESSLIST; -- 或者 PG 里查 pg_stat_activity如果看到大量连接处于Sleep但事务一直没提交或者有Lock wait timeout exceeded的提示说明瓶颈可能在数据库锁或慢 SQL 上。这里有个很容易被忽略的点连接池耗尽和线程池耗尽经常成对出现。线程拿到连接后要执行 SQLSQL 慢线程就占着连接不放连接池满了其他拿不到连接的线程就排队线程池被排队任务占满新请求进不来。表面上看起来是线程池问题根因却在数据库那一层。2.3 锁竞争请求不是先到先得而是“被锁拦住”锁引发的高延迟是一类更隐蔽的问题。因为它在监控图上几乎不留下痕迹只有线程转储的那一瞬间才能看清楚。两类锁最值得注意应用层锁JVM 内置锁synchronized、ReentrantLock、以及各种分布式锁。数据库层锁行锁、表锁、间隙锁、唯一键冲突。排查 JVM 锁同样靠线程转储。如果看到大量线程处于BLOCKED状态并且栈顶都停在同一个类的方法上那就是典型的锁竞争jstack pid | grep -B 10 locked monitor | head -50数据库锁则要查SELECT * FROM information_schema.innodb_trx; SELECT * FROM information_schema.innodb_lock_waits;分布式锁的竞争有时更隐蔽因为锁本身在 Redis 或数据库里线程转储只能看到一段等待锁的代码看不出锁的实际持有者。这种情况只能通过业务日志把锁的 key、持有时间、获取时间打出来再做聚合。我把这四个维度整理成一张速查表排障时可以对着看现象特征最可能瓶颈第一验证手段大量线程 WAITING任务排队线程池耗尽jstack / jcmd Thread.print日志报连接获取超时连接池耗尽连接池监控 数据库 SHOW PROCESSLIST大量线程 BLOCKED 在同一方法应用层锁竞争jstack 看 monitor 持有者偶发超时CPU、线程、连接都正常网络抖动 / GC / 磁盘sar、GC 日志、iostat容器内 CPU 很高但外部监控低cgroup 限流查看 /sys/fs/cgroup/cpu.stat2.4 IO 阻塞磁盘慢、网络丢包、GC 停顿都能让延迟凭空上涨还有一类问题既不占 CPU也不占线程池但就是会让接口变慢。磁盘 IO 是典型。日志落盘是同步写如果磁盘满了或者云盘出现抖动写入一段日志都可能要等几百毫秒。更麻烦的是有些框架在写日志时会同步刷盘一旦磁盘出问题整个请求链路都被拖住。排查工具是iostat -x 1重点看%util、await和svctm如果await远高于几毫秒说明磁盘在排队。网络同样如此。跨机房调用、依赖公网的第三方接口一旦出现丢包和重传TCP 重传时间会直接把超时拉满。排查常用sar -n DEV 1 5 netstat -s | grep -i retrans ss -sGC 停顿也是低 CPU 高延迟的重要来源。一个 Full GC 导致应用线程完全停止如果停止时间超过 1 秒接口延迟必然被拉高。奇怪的地方在于GC 线程本身只占少数核整体 CPU 平均值可能依然不高。所以务必看 GC 日志尤其是GC pause的持续时间而不是只看 CPU。3. 一套可复用的排障顺序从现象到线程状态再到根因很多团队的现状是监控大盘很全但遇到事故时还是靠感觉猜。低 CPU 高延迟这个问题完全可以用一套固定顺序来缩小范围。我一般建议按下面六步走每一步都以上一步的结果为依据不要跳步。3.1 第一步确认系统层面的运行队列和负载先看主机到底是闲还是忙uptime vmstat 1 5重点关注三列r运行队列长度。如果远大于 CPU 核数说明任务在排队等 CPU但 CPU 百分比不一定立刻打满。b处于不可中断睡眠D 态的进程数。持续不为 0几乎都是磁盘 IO 在等块设备。si/soswap 换入换出。如果一直不为 0内存可能不够进程在内存和磁盘之间颠簸。这一步能快速区分“CPU 排队”和“IO 等待”避免误判方向。3.2 第二步看目标进程和线程的 CPU 占用用top -Hp pid看一下进程内部各线程的 CPU。如果所有线程的 CPU 都很低说明确实不是算力问题如果某个线程 CPU 高比如 90%说明系统里有热点计算需要再往那个线程的栈里挖。这一步也能排除一种情况CPU 平均值低但其实是某一个核被打满其他核闲着。单线程瓶颈和整体算力不足处理方式完全不一样。3.3 第三步抓线程转储统计线程状态如果确认不是计算热点下一步就是抓两次线程转储间隔 5 到 10 秒然后统计状态分布for i in 1 2; do jstack pid /tmp/threaddump_$i.txt sleep 5 done grep -c java.lang.Thread.State: WAITING /tmp/threaddump_1.txt grep -c java.lang.Thread.State: BLOCKED /tmp/threaddump_1.txt两次对比的意义在于如果状态分布变化很大说明系统还在剧烈波动如果两次都很一致说明线程稳定卡在同一个状态问题很可能是持续性的资源瓶颈。然后打开文件看栈顶找同一段代码出现次数的峰值。哪个方法反复出现且大量线程停住就是当前的拥堵点。3.4 第四步查连接池和下游依赖耗时线程状态只能说明“在等”不能说明“在等谁”。所以下一步要查连接池指标和下游依赖。数据库连接池看 active 数是否长期接近 max看获取连接的平均等待时间。Redis / 缓存看 get/set 的 p99 耗时。HTTP / RPC 下游看每个下游调用的超时、成功率、耗时分布。如果公司内部已经有 APM 全链路追踪这一步会非常快。没有的话至少要在业务日志里把关键调用的耗时打出来按接口聚合。3.5 第五步检查 GC 日志和容器限流GC 停顿经常被当成“灵异事件”排在最后其实它应该排在连接池之前。看一眼 GC 日志里的停顿时间10 分钟之内如果有多次超过 500ms 的停顿基本可以下一个判断延迟尖峰和 GC 强相关。容器限流则要查cat /sys/fs/cgroup/cpu/cpu.stat看nr_throttled和throttled_time。如果 throttled_time 在持续增长说明容器的 CPU 配额撑不住了外部监控可能看不出但服务内部确实在被打断。3.6 第六步对照日志锁定首报时间点前后的变化所有工具都查完之后回到业务日志找延迟开始飙升的前后 5 分钟发生了什么有没有新上线的功能有没有定时任务在整点触发有没有下游依赖发布变更有没有数据量突然增长很多低 CPU 高延迟的根因是“平时跑得好好的某个变更触发了慢路径”。比如一个新的筛选条件导致 SQL 没走索引或者一次数据订正把某张表锁住了。这类问题工具能帮你缩小范围最终定位往往要靠日志级的复盘。4. 一次典型事故的复盘从 80ms 到 3s根因在连接池而不是 CPU说一个我印象很深的修复过程几乎就是低 CPU 高延迟的教科书案例。现象是某个订单查询接口在工作日晚高峰突然变慢P99 从 80ms 涨到 3s错误率缓慢上升。主机监控显示 CPU 15%内存正常网络流量也没有异常。第一轮排查团队里有人怀疑是数据库慢查询有人怀疑是新上线的缓存异步化改造出了问题。我没有急着下结论而是按上面的顺序走了一遍。先看vmstatr列和b列都很低说明主机层面没有明显的排队。再看进程内线程top -Hp显示所有线程 CPU 都很低。第三步抓jstack结果非常明显大量 Tomcat 工作线程处于WAITING栈顶停在 HikariCP 的getConnection上后面跟着Connection is not available, request timed out。到这里方向已经清晰线程池本身没满但所有线程都在等数据库连接。接下来查数据库侧SHOW PROCESSLIST显示有几十个连接处于Query状态执行的是一条看起来很久的UPDATE语句还有一部分连接处于Sleep状态但事务一直没有提交。继续追发现罪魁祸首是一个定时任务每晚这个时间点会跑一次全量数据订正对订单表做范围更新。由于更新条件和主索引不匹配触发了全表扫描并且对相关行加了写锁。查询接口的SELECT被这些写锁挡住连接迟迟不释放连接池被耗光后面的请求全部排队等连接。修复手段并不复杂停掉或者错峰运行这个定时任务。给 UPDATE 语句的 WHERE 条件补上合适的索引。把订正任务改成小批量循环避免长事务持锁时间过长。给连接池的获取连接设置更短的超时快速失败而不是无限排队。复盘的时候大家都在问为什么 CPU 不高因为数据库锁等待根本不消耗数据库 CPU也不消耗应用 CPU应用线程只是在WAITING。真正消耗资源的是锁竞争背后的长事务而它在 CPU 监控上完全隐形。这次事故给我最大的启发是低 CPU 高延迟不是“没有线索”而是线索不在一眼能看到的地方。线程状态、连接池水位、慢 SQL、锁等待这些才是真正需要常看的指标。建议在排障时不要只盯着 CPU、内存、带宽这些“资源类指标”还要盯“等待类指标”线程池活跃数、连接池活跃数、锁等待次数、GC 停顿时长。后者才是判断这类问题的关键。5. 什么时候才是真的 CPU 问题别把方向带偏谈了这么多非 CPU 原因也要说清楚边界确实存在一些情况CPU 指标就是主要线索只是容易被“15%”这个数字带偏。5.1 用户态 CPU 高真的在算如果top里进程 CPU 常驻 90% 以上线程栈里热点集中在某个方法比如大量的 JSON 序列化、加密解密、正则匹配或大对象拷贝那就是典型的 CPU 密集问题。低 CPU 高延迟这套排查思路用不上应该直接做热点分析和代码优化。5.2 内核态 CPU 高系统调用或上下文切换异常如果top里%sy占比很高超过 30%需要查是不是有大量的系统调用、线程频繁切换、网络包处理异常。pidstat -w 1可以看上下文切换次数正常情况下每秒几千次以内如果到了几十万次说明线程调度出了问题。5.3 负载高但 CPU 低D 状态进程在等 IO这种情况用vmstat的b列一眼就能看出来。进程处于不可中断睡眠等于“卡在 IO 上”负载会被拉高但 CPU 是空闲的。处理方向是磁盘、文件系统、存储挂载而不是应用代码。5.4 容器里 CPU 配额被打满外部监控不敏感容器化部署后宿主机层面的 CPU 监控可能一直是低的但容器内部因为 cgroup 配额不足被打断。体现在服务上就是接口抖动、延迟升高看宿主机的 CPU 完全没有意义。所谓“CPU 只有 15%”可能只是宿主机很闲你的容器已经用完了自己的配额。判断方法就是看cpu.stat里的nr_throttled增量。所以真正适合“加机器”的低 CPU 高延迟场景其实很少。除非你确认了是容器配额不足、单核热点、或者确实某个中间件资源被打满否则扩容只是在拖延问题。6. 把这次经验沉淀成监控、告警和应急预案一次排障解决的是一次事故但如果没有沉淀下个月同一类问题换一层皮会再回来。低 CPU 高延迟这类问题特别适合提前通过监控避免因为它的几个主要瓶颈都有明确的量化指标。6.1 至少要把这些指标纳入监控线程池活跃线程数、队列深度、拒绝任务数。不要只看线程池最大值要看它离最大值有多近。数据库连接池活跃连接数、等待获取连接的次数和平均等待时间。下游依赖每个依赖的调用量、成功率、P99 耗时。GCGC 次数、单次停顿最长耗时、Full GC 频率。系统层load average、运行队列长度、D 态进程数、磁盘 await、网络重传率。6.2 设置合理的告警阈值阈值要根据自身业务调整但有几个通用原则线程池活跃数连续 5 分钟超过 80%就值得关注而不是等到 100% 才告警。数据库连接池等待时间持续超过 200ms大概率是慢 SQL 或长事务。GC 单次停顿超过 300ms 就要告警说明堆或回收策略已经不适合当前流量。容器nr_throttled持续增长要提前考虑调整配额。不要给 CPU 设一个“超过 80% 才告警”的规则就完事。低 CPU 高延迟恰恰说明很多事故发生时 CPU 都很安静真正该告警的是线程池和连接池这些“看不见的水位”。6.3 沉淀一份排障 Runbook把上面第三节的六步顺序固化成一份文档写清以下内容什么现象触发这套流程低 CPU 高延迟 错误率上升。每一步要执行的命令以及预期的输出形态。每一步查完什么结果时下一步应该往哪走。最后如何从根因回到修复方案。Runbook 的价值不在于每步都写得多么精细而在于它把一次事故的“分析过程”从个人经验变成了团队资产。下次有人值班遇到同样现象时不再是从零开始猜而是直接按顺序执行。6.4 复盘时多问一层“为什么”每次事故复盘除了“这次怎么修好的”还要追加两个问题这个瓶颈为什么没有被更早发现如果我们不改代码什么样的流量增长会再次触发类似问题连接池耗尽和线程池耗尽这类问题往往是容量规划的阴影面。CPU 可以按核数估算容量但连接数、线程数、锁粒度、下游并发上限这些都需要在压测阶段就暴露出来。否则线上 CPU 再低也拦不住一次慢 SQL 引发的雪崩。7. 回到本质学会观察“等待”而不是只观察“计算”低 CPU 高延迟这个组合很容易让人产生一种误解系统很闲但很慢说明问题很玄。其实它一点都不玄它只是说明我们平时构建的监控体系偏向了“计算资源”而忽略了“等待资源”。计算机系统里真正影响接口延迟的往往不是你用了多少 CPU而是你的请求在某些关键路径上等了多少。等数据库连接、等线程池空闲、等一把锁释放、等一次 GC 结束、等磁盘把数据刷下去。这些等待几乎都不产生 CPU 开销却都能让一个接口从几十毫秒变成几秒。所以下一次再遇到“CPU 才 15%接口延迟却飙升”时不要急着加机器也不要先猜网络。按顺序做三件事确认数字真实、抓线程转储看线程在等什么、然后沿着等待链路往后查连接池、锁、GC 和下游依赖。把“等待”变成可观测、可量化的指标这类问题才能真正被根治。线上系统很少会毫无理由地变慢它只是在提醒你你还没看到它真正在等的东西。