认识CPU使用率

前面学习了平均负载和CPU上下文切换,已经对CPU的性能有了初步的了解;我们此前总是用CPU使用率来描述系统CPU的性能的;

CPU使用率是单位时间内CPU使用情况的统计, 以百分比的方式展示;

那么,作为最常用的也是最熟悉的CPU指标,CPU使用率是如何计算的?然后,例如top, ps之类的性能工具展示的%user, %nice, %system, %iowait, %steal等等, 它们之间又有什么不同呢?

下来将了解CPU使用率的内容,案例以我们最常用的反向代理服务器Nginx为例,在一步步操作和分析中深入理解。

CPU使用率

Linux作为一个多任务操作系统,将每个CPU的时间划分为很短的时间片,再通过调度器轮流分配给每个任务使用,因此造成多任务同时运行的错觉;

为了维护CPU时间,Linux通过事先定义的节拍率(内核表示为HZ),触发时间中断,并使用全局变量Jiffies记录开机依赖的节拍数。每发生一次时间中断,Jiffies的值就加1.

节拍率HZ是内核的可配选项,可以设置为100、250、1000等。不同的系统可能设置不同数值,可以通过查询/boot/config内核选项来查看它的配置值。如本系统中,节拍率设置成了250,也就是每秒钟触发250次时间中断;

1
grep 'CONFIG_HZ=' /boot/config-$(uname -r)

image-20230702232224177

同时,正因为节拍率HZ是内核选项,所以用户空间程序并不能直接访问。为了方便用户空间程序,内核还提供了一个用户节拍率USER_HZ, 它总是固定为100, 也就是1/100秒;这样,用户空间程序并不需要关心内核中HZ被设置成了多少,因为它看到的总是固定值

USER_HZ。

Linux通过/proc虚拟文件系统,向用户空间提供了系统内部状态的信息,而/proc/stat提供的就是系统的CPU和任务统计信息。比方说,如果你只关注CPU的话,可以执行下面的命令,

grep 'CONFIG_HZ=' /boot/config-$(uname -r)

image-20230702232736710

这里的输出结果是一个表格;其中,第一列表示的是CPU编号,如cpu0, cpu1, 而第一行没有编号的cpu, 表示的是所有CPU的累加。其他列则表示不同场景下CPU的累加节拍数,它的单位是USER_HZ, 也即是10ms (1/100秒),所以这其实就是不同场景下的CPU时间;这里面的每一列不需要背,有需要的时候,查询man proc即可;不过,要清楚man proc文档中的每一列的含义,它们都是CPU使用率相关的重要指标,也可以在很多其他的性能工具中看到它们。

下面,依次解读一下:

  • user (通常缩写为us),代表用户态CPU时间。注意,它不包括下面的nice时间,但包括了guest时间;
  • nice (通常缩写为ni), 代表低优先级用户态CPU时间,也就是进程的nice值被调整成1-19之间时的CPU时间。这里注意,nice可取值范围是-20 ~ 19, 数值越大,优先级反而低;
  • system (通常缩写为sys), 代表内核态CPU时间。
  • idle (通常缩写为id),代表空闲时间。注意,它不包括等待I/O的时间(iowait)。
  • iowait (通常缩写为wa), 代表等待I/O的CPU时间;
  • irq (通常缩写为hi),代表处理硬中断的CPU时间;
  • softirq (通常缩写为si),代表处理软中断的CPU时间;
  • steal (通常缩写为st), 代表当系统运行在虚拟机中的时候,被其他虚拟机占用的CPU时间;
  • guest (通常缩写为guest),代表通过虚拟化运行其他操作系统的时间,也就是运行虚拟机的CPU时间;
  • guest_nice (通常缩写为gnice), 代表以低优先级运行虚拟机的时间;

而我们通常所说的CPU使用率就是除了空闲时间外的其他时间占总CPU时间的百分比

image-20230702235142092

根据这个公式,可以从/proc/stat中的数据,很容易地计算出CPU使用率。当然,也可以用每一个场景的CPU时间,除以总的CPU时间,计算出每个场景的CPU使用率;

但是,如果直接用/proc/stat的数据,它是开机以来的节拍数累加值,所以直接算出来的是,开机以来的平均CPU使用率,一般就没啥价值;

事实上,为了计算CPU使用率,性能工具一般会取间隔时间(比如3s)的两次值,作差后,再计算这段时间内的平均CPU使用率,即:

image-20230702235730069

上面这个公式, 就是我们用各种性能工具所看到的CPU使用率的实际计算方法;

现在,我们知道了系统CPU使用率的计算方法, 那进程的呢? 跟系统的指标类似, Linux也给每个进程提供了运行情况的统计信息, 也就是/proc/[pid]/stat, 不过, 这个文件包含的数据就比较丰富了, 总共有52列数据; 实际上, 不用知道每列的含义, 需要的时候, 查man proc就行;

那么, 是否要查看CPU的使用率, 就必须先读取/proc/stat/proc/[pid]/stat 这两个文件,然后再按照上面的公式计算出来呢?

其实不需要, 现在各种各样的性能分析工具已经帮我们计算好了, 不过要注意的是, 性能分析工具给出的都是间隔一段时间内的平均CPU使用率, 所以要注意间隔时间的设置, 特别是用多个分析工具对比分析时, 一定要保证他们用的是相同的间隔时间;

比如, 对比top和ps两个工具报告的CPU使用率, 默认结果可能就很不一样, 因为top默认使用3秒时间间隔, 而ps使用的却是进程的整个生命周期;

如何查看CPU使用率

说到查看CPU使用率的工具, 一般第一反应肯定是top和ps, 事实上, top和ps确实是最常见的性能分析工具;

  • top显示了系统总体的CPU和内存使用情况,以及各个进程的资源使用情况;
  • ps则只显示了每个进程的资源使用情况;

image-20230709110424498

top的这个输出结果中, 第三行%CPu就是系统的CPU使用率,每列含义上面也做了注解;不过top默认显示的是所有CPU平均

值,这时候可以按下数字1,就可以切换到每个CPU的使用率。

空白行之后是进程的实时信息,每个进程都有一个%CPU列,表示进程的CPU使用率。它是用户态和内核态CPU使用率的总和,包括进程空间使用的CPU、通过系统调用执行的内核空间CPU、以及在就绪队列等待运行的CPU;在虚拟化环境中,它还包括了运行虚拟机占用的CPU。

到这我们可以发现,top并没有细分进程的用户态CPU和内核态CPU。那如何查看每个进程的详细情况?可以用前面的pidstat,它正是一个专门分析每个进程CPU使用情况的工具;

如下面的pidstat命令,就间隔1秒展示了进程的5组CPU使用率,包括:

  • 用户态CPU使用率(%usr)
  • 内核态CPU使用率(%system)
  • 运行虚拟机CPU使用率(%guest)
  • 等待CPU使用率(%wait);
  • 以及总的CPU使用率(%CPU)

最后的Average部分,还计算了5组数据的平均值;

image-20230709115128472

CPU使用率过高

通过top, ps, pidstat等工具,可以很方便的找到CPU使用率较高(比如100%)的进程。

紧接着又想知道,占据CPU的到底是代码中的哪个函数呢?找到它,才能更高效、更针对性的进行优化;

有人说,应该用GDB(The GNU Project Debugger),这个功能强大的程序调试器,的确,GDB在调试程序错误方面很强大,但GDB不适合在性能分析的早期;

因为GDB调试程序 的过程中会中断程序运行,这在生产环境是不被允许的。所以,GDB只适合用在性能分析的后期,当你找到了出问题的大致函数后,线下再借助它来进一步调试函数内部的问题;

那么哪种工具适合在第一时间分析进程的CPU问题呢?可以使用perf, perf是Linux 2.6.31以后内置的性能分析工具。它以性能事件采样为基础,不仅可以分析系统的各种事件和内核性能,还可以用来分析指定应用程序的性能问题。

使用perf分析CPU性能问题,介绍两种最常见的用法;

第一种常见用法是perf top, 类似top, 它能够实时显示占用CPU时钟最多的函数或者指令,因此可以用来查找热点函数,使用界面如下:

image-20230709130013896

输出结果中,第一行包含三个数据,分别是采样数(Samples), 事件类型(event)和事件总数量(Event Count)。

我们这个case中,perf总共采集了380个CPU时钟事件,而总事件数则为57350182.

另外,采样数需要我们特别注意。如果采样数过少(比如只有十几个),那下面的排序和百分比就没有实际参考价值了;

再往下看就是一个表格样的数据,每一行包含四列,分别是:

  • 第一列 Overhead, 是该符号的性能事件在所有采样中的比例,用百分比来表示;
  • 第二列Shared, 是该函数或指令所在的动态共享对象(Dynamic Shared Object), 如内核、进程名、动态链接库名、内核模块名等。
  • 第三列Object,是动态共享对象的类型。比如[.]表示用户空间的可执行程序、或者动态链接库,而[k]则表示内核空间;
  • 最后一列Symbol是符号名,也就是函数名。当函数名未知时,用十六进制的地址来表示;

还有以上面的输出为例,占用CPU时钟没有什么异常,说明系统并没有CPU性能问题。

接着来看第二种常见用法,也就是perf record和perf report。perf top虽然实时展示了系统的性能信息,但它的缺点是并不保存数据,也就无法用于离线或者后续的分析。而perf record则提供了保存数据的功能,保存后的数据,需要用perf report来解析展示;

在实际使用中,我们还经常为perf top和perf record加上-g参数,开启调用关系的采样,方便我们根据调用链来分析性能问题;

案例

下面我们以Nginx + PHP 的web服务为例,来看看当发现CPU使用率过高的问题后,如何使用top等工具找出异常的进程,又要怎么利用perf找出引发性能问题的函数。

准备

以下案例基于Ubuntu 18.04, 同样适用于其他的Linux系统。使用的案例环境如下所示:

  • 机器配置: 2CPU, 8GB内存

  • 预先安装docker、sysstat、perf、ab等工具

这次使用的工具ab, ab(apache bench)是一个常见的HTTP服务性能测试工具,这里用来模拟Nginx的客户端;

由于Nginx和PHP配置比较麻烦,将他们打包成了两个Docker镜像,这样,只需要运行两个容器,就可以得到模拟环境;

image-20230716230804611

从上图可以看到,其中一台用 作Web服务器,来模拟性能问题;另一台用作Web服务器的客户端,来给Web服务器增加压力请求。使用两台虚拟机时为了相互隔离,避免"交叉感染";

本例中使用的PHP应用的核心逻辑比较简单,大部分一眼就可以看出问题,但是实际上生产环境的代码是极其复杂的;

因此,在案例操作步骤之前,先不要查看源码(避免先入为主),而是要把它当作是一个黑盒来分析。这样,可以更好的理解整个解决思路,怎么从系统的资源使用问题出发,分析出瓶颈所在的应用,以及瓶颈在应用中的大概位置;

操作和分析

首先,在终端1执行下面的命令来运行Nginx和PHP应用:

1
2
docker run --name nginx -p 10000:80 -itd feisky/nginx
docker run --name phpfpm -itd --network container:nginx feisky/php-fpm

image-20230716231759779

image-20230716231824833

然后,在第二个终端使用curl命令来访问vm1中的nginx, 确认Nginx已经正常启动, 可以看到It works!的响应;

image-20230716232009334

接下来,我们来测试这个Nginx服务的性能。在终端2运行下面的ab命令:

1
2
# 并发10个请求测试Nginx的性能,总共测试100个请求
ab -c 10 -n 100 http://192.168.44.138:10000/

image-20230716232653527

从ab的输出结果可以看到,Nginx能承受的每秒平均请求数只有22.85, 这个性能是有点差了,那么到底问题是出在哪里了呢?

我们用top和pidstat来分析下;

这样,我们在第二个终端,将测试的请求总数增加到10000,这样当在第一个终端使用性能工具,Nginx的压力还是继续, 便于观察;

1
2
# 并发10个请求测试Nginx的性能,总共测试10000个请求
ab -c 10 -n 10000 http://192.168.44.138:10000/

接着,回到第一个终端运行top命令,并按下数字1,切换到每个CPU的使用率:

image-20230719160527526

这里可以看到,系统中有几个php-fpm进程的CPU使用率加起来接近200%; 而每个CPU的使用率(us)也已经超过了96%,接近饱和。这样,我们就可以确认,正是用户空间的php-fpm进程,导致CPU使用率飙升;

那么接着分析,怎么知道是php-fpm的哪个函数导致了CPU使用率升高呢?我们用perf分析一下,在第一个终端运行一下下面的perf命令:

1
# -g开启调用关系分析, -p指定 php-fpm的进程号 3727

image-20230719161844633

上面这张图的结果我没看到,看不到函数名而是一些十六进制的地址,那是因为:

应用程序运行在容器中,它的依赖也都在容器内部,故而perf无法找到PHP符号表。一个简单的解决办法是使用perf record生成perf.data拷贝回到容器内部perf report.

我们可以拷贝出Nginx应用的源码,看看是不是调用了这两个函数:

1
2
3
4
5
6
7
# 从容器phpfpm中将PHP源码拷贝出来
docker cp phpfpm:/app .

# 使用grep查找函数调用
grep sqrt -r app/ # 找到了sqrt调用

#grep add_function -r app/  # 没找到add_function调用,这其实是PHP内置函数

image-20230719162525788

这样,我们就知道了,原来只有sqrt函数在app/index.php文件中调用了,那最后一步,我们就该看看这个文件的源码了:

image-20230719162708739

问题就出现在那段注释的测试代码也直接发布应用了。这里也有个修复后的应用以供测试;

1
2
3
4
5
# 停掉并删除之前的docker应用
docker rm -f nginx phpfpm
# 运行优化后的应用
docker run --name nginx -p 10000:80 -itd feisky/nginx:cpu-fix
docker run --name phpfpm -itd --network container:nginx feisky/php-fpm:cpu-fix

image-20230719163904528

接着,到终端二来验证一下修复后的效果。

 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
bomir1@morn:~$ ab -c 10 -n 10000 http://192.168.44.138:10000/
This is ApacheBench, Version 2.3 <$Revision: 1807734 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 192.168.44.138 (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests

Server Software:        nginx/1.15.6
Server Hostname:        192.168.44.138
Server Port:            10000

Document Path:          /
Document Length:        9 bytes

Concurrency Level:      10
Time taken for tests:   4.002 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      1720000 bytes
HTML transferred:       90000 bytes
Requests per second:    2498.56 [#/sec] (mean)
Time per request:       4.002 [ms] (mean)
Time per request:       0.400 [ms] (mean, across all concurrent requests)
Transfer rate:          419.68 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.4      0      17
Processing:     0    4   2.1      3      38
Waiting:        0    3   2.0      3      37
Total:          1    4   2.2      4      38

Percentage of the requests served within a certain time (ms)
  50%      4
  66%      4
  75%      4
  80%      5
  90%      6
  95%      7
  98%      9
  99%     12
 100%     38 (longest request)

从这里我们可以看出,现在每秒的请求数,已经从原来的22.85变成了2498.56;

本例中,就是这么一个小小测试代码的问题,导致了极大的性能影响,并且查找起来不太容易;找到问题后,处理起来就比较容易了;

小结

CPU使用率是最直观和最常用的系统性能指标,更是我们在排查性能问题时,通常会关注的第一个指标。所以我们要熟悉它的含义,有其是要弄清楚用户(%user)、Nice(%nice)、系统(%system)、等待I/O(%iowait)、中断(%irq)以及软中断(%softirq)这几种不同CPU的使用率。比如说,

  • 用户CPU和Nice CPU高,说明用户态进程占用了较多的CPU,所以应该着重排查进程的性能问题
  • 系统CPU高,说明内核态占用了较多的CPU,所以应该着重排查内核线程或者系统调用的性能问题
  • IO等待CPU高,说明等待IO的时间长,所以应该着重排查系统存储是不是出现了IO问题
  • 软中断和硬中断高,说明软中断或者硬中断的处理程序占用了较多的CPU,所以应该着重排查内核中的中断服务程序

碰到CPU使用率升高的问题,可以借助top、pidstat等工具,确认引发CPU性能问题的来源;再使用perf等工具,排查出引起性能问题的具体函数。