线上 CPU 高使用率的处理思路
本文最后更新于 2025年5月12日 09:47
CPU 使用率 100% 会导致什么问题
- 服务响应变慢甚至不可用:因为 CPU 资源被耗尽,导致无法及时处理请求,从而造成服务响应变慢甚至不可用,影响用户体验。
- 系统资源竞争加剧:多线程/多进程争抢CPU,锁竞争加剧,进一步拖慢系统。同时由于请求挤压,可能会最终会导致请求丢失。
- 其他资源受影响:高CPU通常伴随高内存、磁盘IO、网络IO压力,可能引发更多资源瓶颈问题。
导致 CPU 高使用率的原因
- 代码问题:代码中存在某些复杂算法、死循环、无限递归等问题。
- 等待任务过多:当工作线程需要访问外部资源时,如果外部资源响应非常慢,就会让工作线程一直处于等待状态,当这样的等待任务过多,CPU 就会被耗尽。
- 死锁:如果系统在发生死锁时使用的是自旋锁等忙等机制,就会导致这些线程持续占用 CPU,造成 CPU 使用率飙升。
- 多线程上下文切换:当系统中并发创建大量线程时,由于 CPU 资源有限,必须在众多线程之间频繁切换,最终大量 CPU 时间被消耗在上下文切换上,而没有执行真正的业务逻辑。
- CPU密集型任务:比如搜索推荐场景的模型训练和推理,视频、图片编解码等,都是 CPU 密集型而非 IO 密集型的业务。碰到这种 CPU 密集型的业务,CPU 100% 是正常的,任务结束之后 CPU 使用率会回归正常。
- 频繁 GC:代码中某个位置读取数据量较大,导致系统内存耗尽,从而导致Full GC次数过多,CPU 使用率飙升。
如何排查 CPU 高使用率问题
执行
top c
查看所有进程占系统 CPU 的排序。大多数情况下排第一个的就是 Java 进程,我们可以根据其进程号,查找该进程下的所有线程的 CPU 使用情况。下面举例:1
2
3
4
5
6top - 08:31:10 up 30 min, 0 users, load average: 0.73, 0.58, 0.34
KiB Mem: 2046460 total, 1923864 used, 122596 free, 14388 buffers
KiB Swap: 1048572 total, 0 used, 1048572 free. 1192352 cached Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
9 root 20 0 2557160 288976 15812 S 98.0 14.1 0:42.60 java在这个例子中,我们可以注意到:PID 为 9 的进程其 CPU 占用率已经高达 98%。所以我们应当重点关注该进程。
执行
top -Hp <进程号>
命令查看指定进程下所有线程的 CPU 使用情况。下面举例:1
2
3
4
5
6
7
8top - 08:31:16 up 30 min, 0 users, load average: 0.75, 0.59, 0.35
Threads: 11 total, 1 running, 10 sleeping, 0 stopped, 0 zombie
%Cpu(s): 3.5 us, 0.6 sy, 0.0 ni, 95.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem: 2046460 total, 1924856 used, 121604 free, 14396 buffers
KiB Swap: 1048572 total, 0 used, 1048572 free. 1192532 cached Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
10 root 20 0 2557160 289824 15872 R 79.3 14.2 0:41.49 java
11 root 20 0 2557160 289824 15872 S 13.2 14.2 0:06.78 java我们可以看到同一个Java应用的两个线程(或进程),它们共同消耗了约92.5%(79.3% + 13.2%)的CPU资源。其中 ID 为 10 的线程占用了 79.3% 的 CPU,我们需要重点排查。
线程号进制转换。由上面的例子可知我们要排查的是 ID 为 10 的线程,执行
printf "%x\n" 10
,可以得到输出0xa
,这样我们就将线程号由十进制转为了十六进制。执行
jstack <进程号> | grep <线程ID>
在某进程的堆栈信息中查找指定线程的状态。在jstack
输出的堆栈信息中,线程的nid
(即操作系统线程 ID)以十六进制表示。例如,如果线程的nid=0xa
,我们可以使用grep
查找该线程的堆栈信息。如果输出类似于"CleanExpireMsgScheduledThread_1" #123 prio=5 os_prio=0 tid=0x00007ff78ed5c800 nid=0x76 waiting on condition [0x00007ff69ab7c000]
,那么第一个双引号括起来的部分即为线程名。如果线程名为VM Thread
,则表示这是虚拟机的垃圾回收(GC)线程。通过这种方式,能够快速获取该线程的状态并分析其行为。同时,我们也可以考虑使用 Arthas 工具中的thread <线程ID>
命令来完成同样的操作。使用
jstat -gcutil <进程号> <统计间隔毫秒> <统计次数>(缺省代表一致统计)
命令监控 GC 动态变化,特别是观察 Full GC(FGC)次数的增加。如果FGC
数值很大且持续增大,说明频繁发生 Full GC,此时可以使用jmap -heap <进程号>
检查堆内存(特别是老年代)使用情况,判断是否内存溢出。