系统稳定性—CPU占用过高问题排查及解决

一、概述

Linux系统与Java应用排查中,CPU利用率和CPU负载是最易混淆的两个指标,也是定位性能瓶颈的关键入口。本文从基础概念切入,拆解“高负载低利用率”“高利用率低负载”“CPU 100%”等典型场景的排查逻辑,并结合实战案例演示从指标异常到代码根因的定位过程。

二、利用率与负载区别

要排查CPU问题,首先需明确“利用率”和“负载”的本质差异——前者反映CPU的“实时工作状态”,后者反映CPU的“任务排队情况”,二者无必然关联,但结合分析可快速定位瓶颈类型。

2.1 CPU时间片与多任务原理

现代操作系统(Linux/Windows/Mac)通过时间片轮转实现 “多任务并发”:

  • 单个CPU核心同一时刻只能执行1个任务;
  • 操作系统将CPU时间切分为短时间片(Linux默认5ms-800ms),让多个任务轮流占用CPU
  • 用户感知“同时运行”,本质是任务切换速度远超人类反应速度(如1秒切换100次)。

2.2 核心指标定义与计算

指标 定义 计算逻辑 核心意义
CPU利用率 单位时间内CPU被任务占用的百分比
(分用户态、系统态)
(任务占用时间片总和/总时间)×100% 反映CPU“当前是否繁忙”
CPU负载 单位时间内正在使用CPU等待使用
CPU
的平均任务数(分1/5/15分钟)
系统级任务队列长度的平均值 反映CPU“任务是否排队拥堵”

关键补充:用户态与系统态利用率

  • 用户态CPU利用率(%us:应用程序代码执行占用的CPU时间(如Java业务逻辑计算);
  • 系统态CPU利用率(%sy:操作系统内核调用占用的CPU时间(如文件IO、网络IO、线程切换);
  • 异常判断:若%sy持续 > 30%,可能存在IO密集型瓶颈(如频繁磁盘读写)或线程切换过多。

1.3 指标关联与实例对比

通过两个场景理解二者差异:

场景 CPU利用率 CPU负载(单核) 本质原因 用户感知
单任务死循环(计算密集) 100% ≈1 1个任务独占CPU,无排队 应用卡顿,其他任务延迟
10个IO等待任务 10% ≈10 10个任务等待 IO(如磁盘读
写),CPU空闲
应用响应缓慢,任务堆积

负载阈值判断(多核场景)

CPU负载的“合理阈值”与核心数强相关:

  • 公式:总核心数 = 物理CPU数 × 每CPU核数

  • 示例:2物理CPU×2核 = 4核心,负载 <4 为正常,>6 需警惕,>8 则严重拥堵;

  • 查看核心数命令:

# 物理CPU数
cat /proc/cpuinfo | grep "physical id" | sort | uniq | wc -l

# 每CPU核数
cat /proc/cpuinfo | grep "cpu cores" | uniq

# 逻辑CPU数(含超线程,可理解为“可用CPU资源数”)
cat /proc/cpuinfo | grep "processor" | wc -l

常用查看命令

# 查看负载(1/5/15分钟平均值)、用户/系统态利用率

uptime  # 简洁显示负载:12:34:56 up 100天,  2:30,  1 user,  load average: 0.80, 0.95, 1.00

w       # 同uptime,额外显示登录用户和进程

top     # 实时查看:%us(用户态)、%sy(系统态)、load average(负载)

三、高负载,低利用率

CPU负载很高,利用率却很低,说明处于等待状态的任务很多,负载越高,代表可能很多僵死的进程。通常这种情况是IO密集型的任务,大量任务在请求相同的IO,导致任务队列堆积。

生产环境造成CPU利用率低负载高的具体场景常见的有如下几种。

3.1 场景一:磁盘IO瓶颈(最高频)

进程在cpu上面运行需要访问磁盘文件,这个时候CPU会向内核发起调用文件的请求,让内核去磁盘取文件,这个时候CPU会切换到其他进程或者空闲,这个任务就会转换为不可中断睡眠状态。当这种读写请求过多就会导致不可中断睡眠状态的进程过多,从而导致负载高,CPU低的情况。

3.2 场景二:数据库IO瓶颈

我们都知道MySQL的数据是存储在硬盘中,如果需要进行sql查询,需要先把数据从磁盘加载到内存中。当在数据特别大的时候,如果执行的sql语句没有索引,就会造成扫描表的行数过大导致I/O阻塞,或者是语句中存在死锁,也会造成I/O阻塞,从而导致不可中断睡眠进程过多,导致负载过大。

同样,可以先通过top命令观察,假设发现现在确实是高负载低使用率。

6403.png

然后,再通过命令ps -aux查看是否存在状态为D的进程,这个状态指的就是不可中断的睡眠状态的进程。处于这个状态的进程无法终止,也无法自行退出,只能通过恢复其依赖的资源或者重启系统来解决。以下图中没有D状态的进程。

6404.png

Linux上进程的五种状态

  • R(task_running):可执行状态,只有在该状态的进程才可能在CPU上运行。而同一时刻可能有多个进程处于可执行状态。
  • S(task_interruptible):可中断的睡眠状态,处于这个状态的进程因为等待某某事件的发生(比如等待socket连接、等待信号量),而被挂起。
  • D(task_uninterruptible):不可中断的睡眠状态,进程处于睡眠状态,但是此刻进程是不可中断的。task_uninterruptible状态存在的意义就在于,内核的某些处理流程是不能被打断的。
  • T(task_stopped/task_traced):暂停状态或跟踪状态。
  • Z(task_dead - exit_zombie):退出状态,进程成为僵尸进程。进程已终止,但进程描述还在,直到父进程调用wait4()系统调用后释放。

线上除了上述问题外,还有可能是其他问题,可参考系统稳定性—线上常见问题排查

四、低负载,高利用率

这表示CPU的任务并不多,但是任务执行的时间很长,大概率就是你写的代码本身有问题,通常是计算密集型任务,生成了大量耗时短的计算任务。

怎么排查?直接top命令找到CPU使用率最高的进程,定位到去看看就行了。如果代码没有问题,那么过段时间CPU使用率就会下降的。

4.1 CPU利用率100%排查

步骤1:定位高CPU占用的Java进程

通过top找到CPU占用率高的进程

6401.png

步骤2:定位进程内高CPU占用的线程

通过top -Hp pid命令查看CPU占比靠前的线程ID

6402.png

步骤3:线程ID转换为十六进制(jstack需要)

printf "0x%x\n" 74317 # 输出示例:0x1224d(注意前缀0x)

步骤4:通过jstack定位问题代码

jstack 72700 | grep ‘0x1224d’ -C5 --color

注意:jstack的对象是java进程的PID,而不是java线程的PID。

4.2 案例:死循环导致CPU100%

将这段代码上传到linux服务器,并且使用nohup java JvmCpuTest &运行

package com.jvm;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class JvmCpuTest {
    
    private static ExecutorService service = Executors.newFixedThreadPool(5);
    private static Object lock = new Object();
    
    public static class TaskTest implements Runnable {
        @Override
        public void run() {
            synchronized (lock){
                long sum = 0L;
                while(true){
                    sum +=1;
                }
            }
        }
    }

    public static void main(String[] args) {
        TaskTest taskTest = new TaskTest();
        service.execute(taskTest);
    }
}

排查结果(线程栈关键信息)

2024-06-08 12:17:36
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.281-b09 mixed mode):
 
"Attach Listener" #10 daemon prio=9 os_prio=0 tid=0x00007f006c001000 nid=0xc7f waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
 
   Locked ownable synchronizers:
	- None
 
"DestroyJavaVM" #9 prio=5 os_prio=0 tid=0x00007f00a0009800 nid=0x6955 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
 
   Locked ownable synchronizers:
	- None
 
"pool-1-thread-1" #8 prio=5 os_prio=0 tid=0x00007f00a00f0000 nid=0x6960 runnable [0x00007f008b0ef000]
   java.lang.Thread.State: RUNNABLE
	at JvmCpuTest$TaskTest.run(JvmCpuTest.java:14)
	- locked <0x00000000f59dfcf0> (a java.lang.Object)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
 
   Locked ownable synchronizers:
	- <0x00000000f59e0ed0> (a java.util.concurrent.ThreadPoolExecutor$Worker)
 
"Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007f00a00d4800 nid=0x695e runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
 
   Locked ownable synchronizers:
	- None
 
"C1 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007f00a00b9800 nid=0x695d waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
 
   Locked ownable synchronizers:
	- None
 
"C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007f00a00b6800 nid=0x695c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
 
   Locked ownable synchronizers:
	- None
 
"Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007f00a00b5000 nid=0x695b runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
 
   Locked ownable synchronizers:
	- None
 
"Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007f00a0082000 nid=0x695a in Object.wait() [0x00007f008b6f5000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x00000000f5988ee0> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
	- locked <0x00000000f5988ee0> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
	at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)
 
   Locked ownable synchronizers:
	- None
 
"Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007f00a007d800 nid=0x6959 in Object.wait() [0x00007f008b7f6000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x00000000f5986c00> (a java.lang.ref.Reference$Lock)
	at java.lang.Object.wait(Object.java:502)
	at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
	- locked <0x00000000f5986c00> (a java.lang.ref.Reference$Lock)
	at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
 
   Locked ownable synchronizers:
	- None
 
"VM Thread" os_prio=0 tid=0x00007f00a0074000 nid=0x6958 runnable 
 
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f00a001e800 nid=0x6956 runnable 
 
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007f00a0020800 nid=0x6957 runnable 
 
"VM Periodic Task Thread" os_prio=0 tid=0x00007f00a00d7800 nid=0x695f waiting on condition 
 
JNI global references: 5

从下面的信息中就可以看到问题其实是出现在JvmCpuTest.java的14行左右,这里给出的是14行,但是实际情况是14行的附近,结合代码来看一下就很容易问题

"pool-1-thread-1" #8 prio=5 os_prio=0 tid=0x00007f00a00f0000 nid=0x6960 runnable [0x00007f008b0ef000]
   java.lang.Thread.State: RUNNABLE
	at JvmCpuTest$TaskTest.run(JvmCpuTest.java:14)
	- locked <0x00000000f59dfcf0> (a java.lang.Object)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)

五、总结

  1. 先通过“利用率 + 负载”差异,区分IO密集型(高负载低利用率)与计算密集型(低负载高利用率)瓶颈;​
  2. 再针对性选用工具:Linux命令(top/ps/cat)提取指标,Java工具(jstack)定位代码;​
  3. 最终实现从“指标异常”到“代码根因”的精准落地,为Linux+Java应用CPU问题提供可复用的解决方案。

参考文章

posted @ 2022-04-24 11:27  夏尔_717  阅读(4085)  评论(0)    收藏  举报