1、理解

CPU使用率是单位时间内CPU使用情况的统计,以百分比的方式展示。
Linux 作为一个多任务操作系统,将每个 CPU 的时间划分为很短的时间片,再通过调度器轮流分配给各个任务使用,因此造成多任务同时运行的错觉。
为了维护CPU时间,Linux通过事先定义的节拍率(内核中表示为HZ),触发时间中断,并使用全局变量 Jiffies 记录了开机以来的节拍数。每发生一次时间中断,Jiffies 的值就加 1。
节拍率HZ是内核的可配置选项,可以设置为100、250、1000等。不同的系统可能设置不同的值,可以通过查询 /boot/config 内核选项来查看它的配置值。比如,节拍率设置为 250,也就是每秒触发 250次时间中断。
image.png
同时,因为节拍率HZ是内核选项,所以用户空间不能直接访问,为了方便用户空间程序,内核提供了一个用户空间节拍率USER_HZ,固定100,也就是 1/100 秒。这样,用户空间程序不关心内核中HZ被设置成了多少,因为它看到的总是固定值USER_HZ。
Linux通过 /proc 虚拟文件系统,向用户空间提供了系统内部状态信息,而 /proc/stat 提供的就是系统的CPU和任务统计信息。
image.png
其中,第一列表示的是 CPU 编号,如 cpu0、cpu1 ,而第一行没有编号的 cpu ,表示的是所有 CPU 的累加。其他列则表示不同场景下 CPU 的累加节拍数,它的单位是 USER_HZ,也就是 10 ms(1/100 秒),所以这其实就是不同场景下的 CPU 时间。

2、proc参数含义

名称 缩写 含义 备注
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 以低优先级运行虚拟机的事件

3、CPU使用率计算

CPU使用率,就是除了空闲时间外的其他时间占总CPU时间的百分比,
image.png
根据这个公式,可以从 /proc/stat 中的数据,计算出CPU使用率。以及每个场景的CPU使用率。
但是,/proc/stat 的数据,是开机以来的节拍数累加的值,所以直接算出来的时,开机以来的平均CPU使用率。
一般,性能工具会取间隔一段时间(比如3s)的两次值,通过差值计算。
image.png
单个进程的计算的话,可以/proc/[pid]/stat中查看。

  • top:默认是3秒的时间间隔
  • ps:进程的整个生命周期

    4、CPU使用率查看

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

  • ps 则只显示了每个进程的资源使用情况。

image.png
按下1之后,切换到每个CPU的使用率查看。
top并没有细分进程的用户态CPU和内核态CPU。这里就需要用 pidstat。
image.png

  • %usr:用户态CPU使用率
  • %system:内核态CPU使用率
  • %guest:运行虚拟机CPU使用率
  • %wait:等待CPU使用率
  • %CPU:总得CPU使用率

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

5、CPU使用率过高怎么办

  1. # 能够实时显示占用CPU时钟最多的函数或指定,可以用来查看热点函数
  2. perf top

image.png
包含三个数据:采样数(Samples)、事件类型(event)和事件总数量(Event count)。
如图中,perf总共采集了271个CPU时钟事件、而总时间数为42634171
下面表格中,每行四列数据,分别是:

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

也可以生成报告

  1. $ perf record # 按Ctrl+C终止采样
  2. [ perf record: Woken up 1 times to write data ]
  3. [ perf record: Captured and wrote 0.452 MB perf.data (6093 samples) ]
  4. $ perf report # 展示类似于perf top的报告

为 perf top 和 perf record 加上 -g 参数,开启调用关系的采样,方便我们根据调用链来分析性能问题。

6、Demo:easy

1、准备

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

  1. apt install docker.io sysstat linux-tools-common apache2-utils

docker安装Nginx和PHP应用:

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

2、分析

image.png
通过ab命令,并发10个请求测试Nginx性能,总共测试100个请求,可以看到Nginx能承受的每秒平均请求数只有17 ,是非常差劲的。将请求增加到10000个
image.png
通过top命令,可以看到每个CPU的用户使用率(us)都超过了98%,系统中有几个 php-fpm 进程的CPU使用率加起来接近 200%,可以确定,正是用户空间的 php-
image.png
定位查找这两个函数

  1. # 从容器phpfpm中将PHP源码拷贝出来
  2. $ docker cp phpfpm:/app .
  3. # 使用grep查找函数调用
  4. $ grep sqrt -r app/ #找到了sqrt调用
  5. app/index.php: $x += sqrt($x);
  6. $ grep add_function -r app/ #没找到add_function调用,这其实是PHP内置函数

那就在app/index.php中查找问题即可
image.png.
很明显,是这里,循环的问题,因为该逻辑是个测试逻辑。

3、修复

  1. # 停止原来的应用
  2. $ docker rm -f nginx phpfpm
  3. # 运行优化后的应用,去掉循环的测试逻辑
  4. $ docker run --name nginx -p 10000:80 -itd feisky/nginx:cpu-fix
  5. $ docker run --name phpfpm -itd --network container:nginx feisky/php-fpm:cpu-fix

image.png
可以看到,每秒的平均请求数,已经从原来的17 变成了 689 。

7、Demo:hard

1、准备

  1. docker run --name nginx -p 10000:80 -itd feisky/nginx:sp
  2. docker run --name phpfpm -itd --network container:nginx feisky/php-fpm:sp
  1. # 并发100个请求测试Nginx性能,总共测试1000个请求
  2. ab -c 100 -n 1000 http://127.0.0.1:10000/

image.png
Nginx能承受的每秒平均请求书,只有147 。

2、分析

1、top+pidstat

  1. # 加上 -t 参数,设置请求时长,10分钟
  2. ab -c 5 -t 600 http://127.0.0.1:10000/

通过 toppidstat来查看
image.png
image.png
可以看到CPU使用率在60%以上,但是我们的服务,nginx和php-fpm确只占3%左右。
pidstat中看到,所有的任务的CPU使用率加起来也才 30% 左右。
这里需要注意:
tasks的任务队列中,有5个正在跑(running)的任务,可是占用cpu前几的任务都是大部分处于睡眠(sleep)的状态。
再观察top,会发现有一个 stress 的进程很奇怪。
使用 pidstat -p 24344ps aux | grep 24344都发现,这个进程又不见了,说明该进程在不停的更新。要么,进程在不停地崩溃、重启,要么,都是短时进程,如exec调用外部命令。
image.png
通过pstree查看进程调用关系,查找到 stress 的父进程是php-fpm。就需要分析该进程的源码逻辑。

# 拷贝源码到本地
$ docker cp phpfpm:/app .

# grep 查找看看是不是有代码在调用stress命令
$ grep stress -r app
app/index.php:// fake I/O with stress (via write()/unlink()).
app/index.php:$result = exec("/usr/local/bin/stress -t 1 -d 1 2>&1", $output, $status);

image.png
发现每个请求都会调用一个 stress 命令,模拟 I/O 压力。
image.png
再发现错误消息 mkstemp failed: Permission denied ,以及 failed run completed in 0s。stress 命令因为权限问题退出了。

2、perf

上面方法考验对异常进程的识别能力,因此,可以使用

# 记录性能事件,等待大约15秒后按 Ctrl+C 退出
perf record -g

# 查看报告
perf report

image.png
可以看到stress占了所有CPU时钟的63.40%,其调用栈最高额是random()函数,这样便很快的找到了了原因。进行进一步优化即可。

8、小结

  1. 用户CPU(%user)和Nice CPU(%nice)高,说明用户态进程占用了较多的CPU,所以应该着重排查进程的性能问题
  2. 系统CPU(%system)高,说明内核态占用了较多的CPU,所以应该着重排查内核线程或者系统调用的性能问题
  3. I/O 等待(%iowait)CPU高,说明等待 I/O 的时间比较长,所以应该着重排查系统存储是不是出现了 I/O 问题
  4. 软中断(%irq)和硬中断(%softirq)高,说明软中断或硬中断的处理程序占用了较多CPU,所以应该着重排查内核中的中断服务程序