news 2026/9/8 4:26:30

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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%,不要直接跳进“资源没问题”的结论。先用topuptimepidstat这类命令,确认负载均值(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 out
  • JedisConnectionException: Could not get a resource from the pool
  • org.apache.http.conn.ConnectionPoolTimeoutException

如果用的是数据库连接池,去数据库侧看一眼就知道:

SHOW PROCESSLIST; -- 或者 PG 里查 pg_stat_activity

如果看到大量连接处于Sleep但事务一直没提交,或者有Lock wait timeout exceeded的提示,说明瓶颈可能在数据库锁或慢 SQL 上。

这里有个很容易被忽略的点:连接池耗尽和线程池耗尽经常成对出现。线程拿到连接后要执行 SQL,SQL 慢,线程就占着连接不放;连接池满了,其他拿不到连接的线程就排队;线程池被排队任务占满,新请求进不来。表面上看起来是线程池问题,根因却在数据库那一层。

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.stat

2.4 IO 阻塞:磁盘慢、网络丢包、GC 停顿,都能让延迟凭空上涨

还有一类问题,既不占 CPU,也不占线程池,但就是会让接口变慢。

磁盘 IO 是典型。日志落盘是同步写,如果磁盘满了或者云盘出现抖动,写入一段日志都可能要等几百毫秒。更麻烦的是,有些框架在写日志时会同步刷盘,一旦磁盘出问题,整个请求链路都被拖住。排查工具是iostat -x 1,重点看%utilawaitsvctm,如果await远高于几毫秒,说明磁盘在排队。

网络同样如此。跨机房调用、依赖公网的第三方接口,一旦出现丢包和重传,TCP 重传时间会直接把超时拉满。排查常用:

sar -n DEV 1 5 netstat -s | grep -i retrans ss -s

GC 停顿也是低 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/so:swap 换入换出。如果一直不为 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_throttledthrottled_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被这些写锁挡住,连接迟迟不释放,连接池被耗光,后面的请求全部排队等连接。

修复手段并不复杂:

  1. 停掉或者错峰运行这个定时任务。
  2. 给 UPDATE 语句的 WHERE 条件补上合适的索引。
  3. 把订正任务改成小批量循环,避免长事务持锁时间过长。
  4. 给连接池的获取连接设置更短的超时,快速失败而不是无限排队。

复盘的时候大家都在问:为什么 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

这种情况用vmstatb列一眼就能看出来。进程处于不可中断睡眠,等于“卡在 IO 上”,负载会被拉高,但 CPU 是空闲的。处理方向是磁盘、文件系统、存储挂载,而不是应用代码。

5.4 容器里 CPU 配额被打满:外部监控不敏感

容器化部署后,宿主机层面的 CPU 监控可能一直是低的,但容器内部因为 cgroup 配额不足被打断。体现在服务上就是接口抖动、延迟升高,看宿主机的 CPU 完全没有意义。所谓“CPU 只有 15%”可能只是宿主机很闲,你的容器已经用完了自己的配额。判断方法就是看cpu.stat里的nr_throttled增量。

所以,真正适合“加机器”的低 CPU 高延迟场景其实很少。除非你确认了是容器配额不足、单核热点、或者确实某个中间件资源被打满,否则扩容只是在拖延问题。

6. 把这次经验沉淀成监控、告警和应急预案

一次排障解决的是一次事故,但如果没有沉淀,下个月同一类问题换一层皮会再回来。低 CPU 高延迟这类问题,特别适合提前通过监控避免,因为它的几个主要瓶颈都有明确的量化指标。

6.1 至少要把这些指标纳入监控

  • 线程池:活跃线程数、队列深度、拒绝任务数。不要只看线程池最大值,要看它离最大值有多近。
  • 数据库连接池:活跃连接数、等待获取连接的次数和平均等待时间。
  • 下游依赖:每个依赖的调用量、成功率、P99 耗时。
  • GC:GC 次数、单次停顿最长耗时、Full GC 频率。
  • 系统层:load average、运行队列长度、D 态进程数、磁盘 await、网络重传率。

6.2 设置合理的告警阈值

阈值要根据自身业务调整,但有几个通用原则:

  • 线程池活跃数连续 5 分钟超过 80%,就值得关注,而不是等到 100% 才告警。
  • 数据库连接池等待时间持续超过 200ms,大概率是慢 SQL 或长事务。
  • GC 单次停顿超过 300ms 就要告警,说明堆或回收策略已经不适合当前流量。
  • 容器nr_throttled持续增长,要提前考虑调整配额。

不要给 CPU 设一个“超过 80% 才告警”的规则就完事。低 CPU 高延迟恰恰说明,很多事故发生时 CPU 都很安静,真正该告警的是线程池和连接池这些“看不见的水位”。

6.3 沉淀一份排障 Runbook

把上面第三节的六步顺序固化成一份文档,写清以下内容:

  1. 什么现象触发这套流程(低 CPU + 高延迟 + 错误率上升)。
  2. 每一步要执行的命令,以及预期的输出形态。
  3. 每一步查完什么结果时,下一步应该往哪走。
  4. 最后如何从根因回到修复方案。

Runbook 的价值不在于每步都写得多么精细,而在于它把一次事故的“分析过程”从个人经验变成了团队资产。下次有人值班遇到同样现象时,不再是从零开始猜,而是直接按顺序执行。

6.4 复盘时多问一层“为什么”

每次事故复盘,除了“这次怎么修好的”,还要追加两个问题:

  • 这个瓶颈为什么没有被更早发现?
  • 如果我们不改代码,什么样的流量增长会再次触发类似问题?

连接池耗尽和线程池耗尽这类问题,往往是容量规划的阴影面。CPU 可以按核数估算容量,但连接数、线程数、锁粒度、下游并发上限,这些都需要在压测阶段就暴露出来。否则,线上 CPU 再低,也拦不住一次慢 SQL 引发的雪崩。

7. 回到本质:学会观察“等待”,而不是只观察“计算”

低 CPU 高延迟这个组合,很容易让人产生一种误解:系统很闲但很慢,说明问题很玄。其实它一点都不玄,它只是说明我们平时构建的监控体系偏向了“计算资源”,而忽略了“等待资源”。

计算机系统里,真正影响接口延迟的往往不是你用了多少 CPU,而是你的请求在某些关键路径上等了多少。等数据库连接、等线程池空闲、等一把锁释放、等一次 GC 结束、等磁盘把数据刷下去。这些等待几乎都不产生 CPU 开销,却都能让一个接口从几十毫秒变成几秒。

所以,下一次再遇到“CPU 才 15%,接口延迟却飙升”时,不要急着加机器,也不要先猜网络。按顺序做三件事:确认数字真实、抓线程转储看线程在等什么、然后沿着等待链路往后查连接池、锁、GC 和下游依赖。把“等待”变成可观测、可量化的指标,这类问题才能真正被根治。

线上系统很少会毫无理由地变慢,它只是在提醒你:你还没看到它真正在等的东西。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 4:26:18

Android AAB手动打包全指南:命令行出包与签名验证详解

先说结论&#xff1a;AAB 手动打包这事&#xff0c;平时不常用&#xff0c;但一旦遇到必须用命令行出包的场景&#xff0c;你会发现网上能讲清楚的文章真没几篇。我不是说那些一键打包的教程没用&#xff0c;而是很多人没意识到&#xff0c;手动打包的核心价值在于“可控”——…

作者头像 李华
网站建设 2026/9/8 4:26:13

H5刮刮乐免公众号直运营与多级分佣系统技术拆解

简介&#xff1a;面向微信生态运营者与PHP二次开发人员&#xff0c;这套H5幸运刮刮乐抽奖系统提供免公众号直运营的多级分佣方案&#xff0c;内置后台管理与支付接入框架&#xff0c;适合快速落地抽奖活动或扩展分销玩法。资源共2013个文件&#xff0c;压缩包仅33.83MB&#xf…

作者头像 李华
网站建设 2026/9/8 4:24:43

AI视频素材生产背后的算力基建:从GPU服务器到算电协同

AI 视频在商业项目里批量交付&#xff0c;已经不是新鲜事了。广告分镜、产品演示片、电商主图视频&#xff0c;越来越多的素材由生成式模型直接产出。很多团队把注意力放在提示词、模型选型和后期流程上&#xff0c;我却想提醒你另一条线&#xff1a;真正决定你能不能在截止日期…

作者头像 李华
网站建设 2026/9/8 4:24:23

大气地方门户网站管理系统v1.0:架构设计与实现全解析

简介&#xff1a;这套基于ASP与ACCESS数据库开发的地方门户网站管理系统&#xff0c;专为个人和企业快速搭建地方信息类网站设计&#xff0c;提供从前台内容展示到后台全智能化管理的一体化解决方案。系统内置文章一键发布、用户权限管理、自定义分类、模板切换及SEO优化等模块…

作者头像 李华
网站建设 2026/9/8 4:23:48

苹果AI图像生成集成实战:ImagePlayground框架在iOS App中的接入指南

苹果在 2024 年 WWDC 上首次公布 Apple Intelligence 时&#xff0c;图像生成能力就是最吸睛的部分之一。到了 WWDC 2026 的时间节点&#xff0c;这套能力已经不只是系统自带应用里的“新玩法”&#xff0c;而是真正开放给第三方开发者的系统级框架。本文就以中文讲解的方式&am…

作者头像 李华
网站建设 2026/9/8 4:20:31

JavaScript 删除对象属性全指南:从 delete 到性能优化

前几天在交流群里看到有人问&#xff1a;JS 里怎么删除一个对象的属性&#xff1f;底下一片回答&#xff1a;delete。这个回答对不对&#xff1f;对&#xff0c;但远远不够。如果这段代码写在热循环里&#xff0c;或者你试图删掉一个不可配置的属性&#xff0c;直接写 delete 很…

作者头像 李华