在 Linux 中, CPU 主要用于中断、内核及用户进程的任务处理,优先级为中断 > 内核 > 用户进程,在学习如何分析 CPU 消耗状况前,还有三个重要的概念要阐述。

##概念

上下文切换

每个 CPU (或多核心 CPU 中的每核 CPU )同一时间只能执行一个线程, Linux 采用的是抢占式调度,即为每个线程分配一定的执行时间。线程执行时间到达而切换到其它线程时,就会触发上下文切换。上下文切换过多会造成内核占据较多的 CPU 使用,使得应用的响应速度下降。

对于JAVA应用,典型的是进行文件IO操作、网络IO操作、锁等待或线程sleep时,当线程进入阻塞或等待,会触发上下文切换

运行队列

每个 CPU 都维护了一个可运行的线程队列。

例如:4核CPU,JAVA应用启动了8个线程,且都是出于运行状态,那么在平均分配情况下,每个CPU中的运行队列就有2个线程

通常系统的load由CPU运行队列来决定,运行队列值越大则线程需要消耗越长的时间才能执行完

通常建议控制在每个 CPU 核上的运行队列为 1-3 个。

利用率

CPU 利用率为 CPU 在用户进程、内核、中断处理、IO等待以及空亲五个部分使用的百分比。

建议用户进程和内核进程消耗的 CPU 比率在 65%-70%/30%-35% 左右。

分析方式

在linux中,通过top或pidstat来查看进程中线程的CPU消耗情况

top

top命令可以查看CPU消耗情况
image.png

参数 意义
us 表示用户进程处理所占的百分比
sy 表示内核进程所占的百分比
ni 表示被 nice 命令改变优先级的任务所占的百分比
id 表示 CPU 空闲时间所占的百分比
wa 表示为在执行的过程中等待 IO 所占的百分比
hi 表示为硬件中断所占的百分比
si 表示为软件中断所占的百分比

对于多核CPU,上图显示的为多个CPU占用百分比总和,如果需要查看每个核的消耗情况

进入top视图后,按 1

image.png

默认情况下,top视图显示为进程CPU的消耗情况。

top 视图按 shift+h 后,可按线程查看 CPU 的消耗状况

image.png

pidstat

pidstat 是 SYSSTAT 中的工具,如需使用,请先安装

pidstat 1 2 ,在 console 会每隔 1 秒输出目前活动进程的 CPU 消耗状况,共 2 次。

image.png
其中CPU表示当前进程使用到的CPU个数

如需查看某进程中线程的CPU消耗:

pidstat –p [PID] –t 1 5 可查看某进程中线程的 CPU 消耗状况

image.png
图中TID为线程ID,pidstat好处:可查看每个线程的具体CPU使用率的情况

除了top和pidstat外,还可以采用vmstat来采样,查看CPU的上下文切换、运行队列和使用率情况

CPU消耗严重时的表现

当 CPU 消耗严重时,主要体现在 us 、 sy 、 wa 或 hi 的值变高。对 JAVA 应用而言,主要体现在 us 、 sy 两个值上:

us

us 值过高,表示运行的应用消耗了大部分的 CPU ,如下是找出具体消耗 CPU 的线程的办法:

  • 首先通过 top 或 pidstat 命令找出消耗 CPU 严重严重的线程及其 ID ,将些 ID 转化为十六进制的值。
  • 之后通过 kill -3 [javapid]或jstack的方式dump出应用的java线程信息,通过之前转化的十六进制的值找到对应的nid值的线程。该线程即为消耗 CPU 的线程。
  • 可多执行几次上述过程,以确保找到真实的消耗 CPU 的线程。

原因
  • 线程一直处于可运行状态(Runnable),通常是线程在执行无阻塞、循环、正则或纯粹的计算等
  • 频繁的GC
    • 如每次请求分配较多内存,访问量高时,出现频繁GC。 进而堆积更多的请求,消耗的内存严重。 最严重时,导致full gc

频繁GC的情况要通过JVM内存的消耗来查找原因

sy

sy 值高时,表示系统花费了更多的时间在进行线程切换,JAVA应用造成这种现象的主要原因是启动的线程比较多,且这些线程多数都处于不断的阻塞和执行状态的变化过程中,导致系统不断的切换线程,产生大量的上下文切换。

此时,对JAVA应用而言,主要是找到线程不断切换的原因。可以采用

kill -3 [javapid]或jstack -l [javapid]的方式dump出程序的线程信息,找出等待状态或锁过多的线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class SyHighDemo {
private static int threadCount = 500;
private Random random = new Random();
private Object[] locks;
public static void main(String[] args) {
if(args.length == 1) {
threadCount = Integer.parseInt(args[0]);
}
SyHighDemo demo = new SyHighDemo();
demo.runTest();
}

private void runTest() throws Exception {
locks = new Object[threadCount];
for(int i=0; i<threadCount; i++) {
locks[i] = new Object();
}
for(int i=0; i<threadCount; i++) {
new Thread(new ATask(i)).start();
new Thread(new BTask(i)).start();
}
}

class ATask implement Runnable {
private Object lockObject = null;
public ATask(int i) {
lockObject = locks[i];
}
public void run() {
while(true) {
try {
synchronized (lockObject) {
lockObject.wait(random.nextInt(10);
}
} catch(Exception e) {

}
}
}
}

class BTask implement Runnable {
private Object lockObject = null;
public BTask(int i) {
lockObject = locks[i];
}
public void run() {
while(true) {
synchronized (lockObject) {
lockObject.notifyAll();
}
try {
Thread.sleep(random.nextInt(5));
} catch(Exception e) {

}
}
}
}
}

image.png

由上可知:CPU在cs和sy上消耗很大,运行时使用jstack -l 查看程序状况,可知启动了很多线程,并且很多线程都处于TIMED_WAITING(on object monitor)状态和runnable状态的转换中。

通过on object monitor对应的堆栈信息,可知:系统中锁竞争激烈,这就是导致线程上下文切换频繁的原因

总结

  • 如果us过高,表明应用程序占用了过多cpu
    • 可以用top命令查看到是哪个进程的哪些线程耗费了过多cpu,然后用jstack -l pid导出dump,然后查找对应pid的十六进制,看看是哪个线程调了什么方法。
    • 解决方法:增加线程sleep,优化算法。
  • 如果sy过高,表明大量的用户线程运行堵塞导致系统 频繁进行上下文切换。
    • 可用jstack -l 查看线程运行状况,并配合vmstat 查看系统执行情况及cpu情况。
    • 解决方法:减少线程数,引入协程