详谈CPU使用率

前面两篇博客已经说到很多的关于CPU方面的内容,这篇将进一步详述CPU的使用率。先抛出两个问题,作为最熟悉的CPU指标,你知道CPU使用率是怎么计算的吗?再有,诸如,像top、ps之类的性能工具展示的%user、%nice、%system、%iowait、%steal等,他们之间的区别是什么?

1. CPU使用率

在上一篇讲了CPU时间被分成很多的有很短的时间片供调度器分配,为了维护CPU时间,Linux通过事先定义的节拍率(内核中表示为HZ),触发时间中断,并使用全局变量 jiffies 记录开机以来的节拍数。每发生一次中断,jiffies 值加1。节拍率时是内核的可配置选项,可设置为100、250、1000等,表示每秒发生多少次中断,可以通过查看/boot/config内核选项来获悉内核中 HZ 的值,使用命令:grep 'CONFIG_HZ=' /boot/config-$(uname -r)。用户空间不能直接访问HZ,但是内核为用户空间提供了用户空间节拍率USER_HZ,是固定值100。两者定义的差别导致用户层与内核交互时,需要进行转换。

Linux通过/proc虚拟文件系统,为用户空间提供了系统内部的状态信息,而/proc/stat提供的就是系统CPU和任务统计信息,可以执行命令查看:

[root@test-server ~]# cat /proc/stat | grep cpu
cpu  348570 1261 2879118 1661745415 2613 0 494 0 0 0
cpu0 73339 127 2254881 413913150 579 0 42 0 0 0
cpu1 84049 333 183323 415910001 239 0 48 0 0 0
cpu2 88917 221 229402 415976742 904 0 116 0 0 0
cpu3 102264 578 211511 415945522 889 0 287 0 0 0

第一行表示累加,其他列表示不同场景下CPU的节拍数,单位是 1/USER_HZ,其实这样是可以算出CPU时间的。从man手册中可以查看到每一列的含义,这里也说明一下:

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

CPU使用率的计算公式:

                                          

根据这个公式,可以将/proc/stat中的数据计算出CPU使用率,甚至是每一个场景的CPU使用率(每个场景的CPU时间 / 总的CPU时间),但是算出来的是开机以来的平均CPU使用率,但这个一般没什么参考价值。真正有价值的是某一段特定时间内的CPU使用率,性能工具一般都是取一段时间的前后两次值,作差后,再计算:

                                       

同样,我们还可以算出每个进程的CPU使用率,计算的原始数据出自 /proc/[pid]/stat,计算方法类似。我们只要会用性能分析工具查看CPU使用率,不需要自己计算。但那时要注意,性能工具使用的某一段时间内平均的CPU使用率,不同工具的得到的值因为间隔时间可能不同因而得到的CPU使用率值也可能不同。例如,比较ps和top得到的CPU使用率,默认结果很可能不一样,因为top默认时间间隔是3秒,而ps则是进程的整个生命周期。

2. 查看CPU使用率 并且 CPU使用率过高时怎么办

查看首先想到的是top和ps两个命令。

[root@test-server ~]# top
top - 14:58:22 up 48 days,  5:08,  3 users,  load average: 0.13, 0.06, 0.05
Tasks: 115 total,   1 running, 114 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.0 us,  0.1 sy,  0.0 ni, 99.9 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  7901796 total,  7337964 free,   136384 used,   427448 buff/cache
KiB Swap:  1548284 total,  1548284 free,        0 used.  7475564 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                                                                                                                     
28693 root      20   0  162016   2240   1556 R   0.3  0.0   0:00.21 top                                                                                                                                         
    1 root      20   0   43584   3880   2584 S   0.0  0.0   0:31.87 systemd                                                                                                                                     
    2 root      20   0       0      0      0 S   0.0  0.0   0:00.57 kthreadd                                                                                                                                    
    3 root      20   0       0      0      0 S   0.0  0.0   0:00.14 ksoftirqd/0

从top的输出结果来说明:第三行%Cpu(s),就是 系统CPU使用率,具体含义上面讲过,不过这里显示的是所有CPU的平均值,按1,可以显示出所有CPU的使用率情况。继续往下看,空白行之后,显示的是,每个进程的实时信息,%CPU表示进程的CPU使用率,是用户态和内核态的总和,包括进程用户空间使用的CPU、通系统调用在内核空间的CPU、以及在就绪队列等待运行的CPU,在虚拟化环境中,还包括运行虚拟机占用的CPU。

我们想要查看进程具体情况,使用pidstat命令可以查看:

[root@test-server ~]# pidstat -u 1 5
Linux 3.10.0-957.el7.x86_64 (test-server) 	12/18/2019 	_x86_64_	(4 CPU)

03:10:03 PM   UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
03:10:04 PM     0     28708    0.00    1.96    0.00    0.00    1.96     1  pidstat

... ...

03:10:07 PM   UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
03:10:08 PM     0     13418    0.00    1.00    0.00    0.00    1.00     1  kworker/1:2
03:10:08 PM     0     28708    0.00    1.00    0.00    0.00    1.00     1  pidstat

Average:      UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
Average:        0     13418    0.00    0.20    0.00    0.00    0.20     -  kworker/1:2
Average:        0     28708    0.40    1.39    0.00    0.00    1.79     -  pidstat

分别表示用户态、内核态、运行虚拟机、等待CPU、总的CPU使用率,最后Average,给出了5组数据的平均值。

通过以上工具,我们可以轻松找到CPU使用率过高的进程,但是我们想要知道,占用CPU到底是代码中的哪个函数,这样才能更高效的、更针对性地进行优化。

GDB:在程序错误调试方面功能很强大,但不适合性能分析的早期应用。因为GDB调试会中断程序运行,线上环境是不允许的,所以GDB只适合性能分析的后期,当你找到出问题的大致函数后,线下再借助它调试函数内部的问题。

那么那种工具适合第一时间分析进程的CPU时间呢?推荐perf。perf是Linux 2.6.31以后版本的内置的性能分析工具,以性能时间采样为基础,不仅能分析系统的各种事件和内核性能,还可以分析具体应用的的性能问题。下面说说他的两种使用方法:

一、perf top

$ perf top
Samples: 833  of event 'cpu-clock', Event count (approx.): 97742399
Overhead  Shared Object       Symbol   
7.28%  perf                [.] 0x00000000001f78a4   
4.72%  [kernel]            [k] vsnprintf   
4.32%  [kernel]            [k] module_get_kallsym   
3.65%  [kernel]            [k] _raw_spin_unlock_irqrestore
...

输出结果,第一行包含三个数据,采样数、事件类型、事件总数量。比如这个例子,采集了833个CPU时钟事件、总采样数是97742399。特别注意一下,如果采样数过少,比如十几个,就没什么参考价值了。再往下,是表格式的数据:

overhead:该符号的性能事件在所有采样中的比例;

shared:该函数或者指令所在的动态共享对象,如内核、进程名、动态链接库名、内核模块名等;

object:动态共享对象的类型,比如 [ . ] 表示用户空间的可执行程序、或者动态链接库,而[ k ] 表示内核空间;

symbol:符号名,也就是函数名,当函数未知,用十六进制地址表示。

二、perf record、perf report

perf top实时展示数据但并未保存,无法用于离线或者后续分析。而perf record保存了数据,之后再用perf report解析展示。在实际使用中,还为他们加上-g,方便根据调用链来分析性能问题。

3. 案例分析

这里即将使用的工具ab (apache bench),它是常用的HTTP性能测试工具,这里来模拟nginx的客户端。  

                                          

使用两台虚拟机,一台虚拟机,用作web服务,模拟性能问题,另一条用作web客户端,给web服务增加压力请求,使用虚拟机是为了隔离,避免相互影响。

场景一、能直接找到CPU使用率高的应用

首先在一个虚拟机的终端,运行nginx和php的docker镜像。

在第二个虚拟机的终端,使用curl 执行命令:curl http://192.168.0.10:10000/,确认一下nginx是否正常启动。

然后,在使用ab工具:

# 并发10个请求测试Nginx性能,总共测试100个请求
$ ab -c 10 -n 100 http://192.168.0.10:10000/
This is ApacheBench, Version 2.3 <$Revision: 1706008 $>Copyright 1996 Adam Twiss, Zeus Technology Ltd, 
...
Requests per second:    11.63 [#/sec] (mean)
Time per request:       859.942 [ms] (mean)
...

从输出可以看到:nginx能承受的每秒请求数只有11.63,太低了,将100提高到10000,回到第一个终端,执行top命令:

$ top
...
%Cpu0  : 98.7 us,  1.3 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  : 99.3 us,  0.7 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
...  
PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
21514 daemon    20   0  336696  16384   8712 R  41.9  0.2   0:06.00 php-fpm
21513 daemon    20   0  336696  13244   5572 R  40.2  0.2   0:06.08 php-fpm
21515 daemon    20   0  336696  16384   8712 R  40.2  0.2   0:05.67 php-fpm
21512 daemon    20   0  336696  13244   5572 R  39.9  0.2   0:05.87 php-fpm
21516 daemon    20   0  336696  16384   8712 R  35.9  0.2   0:05.61 php-fpm

这里可以看到,几个php-fpm进程的CPU使用率加起来达到200%,两个CPU的用户态都到了98%,接近饱和,我们可以确认正是进程php-fpm导致了CPU使用率飙升。再往下走:

# -g开启调用关系分析,-p指定php-fpm的进程号21515
$ perf top -g -p 21515

按上下键切换到php-fpm进程,再按下回车键展开php-fpm的调用关系,你会发现,调用关系最终到了 sqrt 和 add_function。

                                 

# 从容器phpfpm中将PHP源码拷贝出来
$ docker cp phpfpm:/app .

# 使用grep查找函数调用
$ grep sqrt -r app/     #找到了sqrt调用
app/index.php:  $x += sqrt($x);
$ grep add_function -r app/    #没找到add_function调用,这其实是PHP内置函数

函数sqrt 只在文件app/index.php中调用了,看看源码:

$ cat app/index.php
<?php
// test only.
$x = 0.0001;
for ($i = 0; $i <= 1000000; $i++) {
  $x += sqrt($x);
}
echo "It works!"

找到问题了,有段测试代码没删就发布应用了。你可以删了它,再重新测试一次:

$ ab -c 10 -n 10000 http://10.240.0.5:10000/
...
Complete requests:      10000
Failed requests:        0
Total transferred:      1720000 bytes
HTML transferred:       90000 bytes
Requests per second:    2237.04 [#/sec] (mean)
Time per request:       4.470 [ms] (mean)
Time per request:       0.447 [ms] (mean, across all concurrent requests)
Transfer rate:          375.75 [Kbytes/sec] received
...

现在每秒请求数上升到2237次/sec。

CPU使用率,通常是我们排查性能问题关注的第一个指标要熟悉它的含义,尤其是这些指标:%user、%nice、%system、%iowait、%irq及%softirq。比如:

%user、%nice这两个指标高,说明用户态进程占用CPU较多,着重排查进程的性能问题。

%system指标高,说明内核态占用较多CPU,着重排查内核线程,和系统调用的性能问题。

%iowait指标高,说明等待IO的CPU时间较长,着重排查系统存储的IO问题。

%irq、%softirq指标高,说明硬中断或软中断的处理程序占用较多CPU,着重排查内核中的中断服务程序。

场景二、不能直接查出CPU使用率高的应用,但是系统的CPU使用率就是高

与上次操作不同的是,这次并发100个请求,怎共1000个请求,测试nginx性能:


# 并发100个请求测试Nginx性能,总共测试1000个请求
$ ab -c 100 -n 1000 http://192.168.0.10:10000/
This is ApacheBench, Version 2.3 <$Revision: 1706008 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, 
...
Requests per second:    87.86 [#/sec] (mean)
Time per request:       1138.229 [ms] (mean)
...

   发现nginx每秒只能承受87个请求,明显太少, 查查性能问题出在哪里?

首先接着使用nginx压力测试,这次并发只有5个请求,只是请求时长设置为10分钟,这样使用性能工具的时候,nginx压力还是继续的。使用以下命令:
$ ab -c 5 -t 600 http://192.168.0.10:10000/


$ top
...
%Cpu(s): 80.8 us, 15.1 sy,  0.0 ni,  2.8 id,  0.0 wa,  0.0 hi,  1.3 si,  0.0 st
...

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 6882 root      20   0    8456   5052   3884 S   2.7  0.1   0:04.78 docker-containe
 6947 systemd+  20   0   33104   3716   2340 S   2.7  0.0   0:04.92 nginx
 7494 daemon    20   0  336696  15012   7332 S   2.0  0.2   0:03.55 php-fpm
 7495 daemon    20   0  336696  15160   7480 S   2.0  0.2   0:03.55 php-fpm
10547 daemon    20   0  336696  16200   8520 S   2.0  0.2   0:03.13 php-fpm
10155 daemon    20   0  336696  16200   8520 S   1.7  0.2   0:03.12 php-fpm
10552 daemon    20   0  336696  16200   8520 S   1.7  0.2   0:03.12 php-fpm
15006 root      20   0 1168608  66264  37536 S   1.0  0.8   9:39.51 dockerd
 4323 root      20   0       0      0      0 I   0.3  0.0   0:00.87 kworker/u4:1
...

并没有发现CPU使用率过高的进程,但是你看%Cpu一行,us列已经达到80.8%,sy列为15.1%,id列为2.8,目前还查不出过高的用户态CPU使用率高的原因,接着往下查:


# 间隔1秒输出一组数据(按Ctrl+C结束)
$ pidstat 1
...
04:36:24      UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
04:36:25        0      6882    1.00    3.00    0.00    0.00    4.00     0  docker-containe
04:36:25      101      6947    1.00    2.00    0.00    1.00    3.00     1  nginx
04:36:25        1     14834    1.00    1.00    0.00    1.00    2.00     0  php-fpm
04:36:25        1     14835    1.00    1.00    0.00    1.00    2.00     0  php-fpm
04:36:25        1     14845    0.00    2.00    0.00    2.00    2.00     1  php-fpm
04:36:25        1     14855    0.00    1.00    0.00    1.00    1.00     1  php-fpm
04:36:25        1     14857    1.00    2.00    0.00    1.00    3.00     0  php-fpm
04:36:25        0     15006    0.00    1.00    0.00    0.00    1.00     0  dockerd
04:36:25        0     15801    0.00    1.00    0.00    0.00    1.00     1  pidstat
04:36:25        1     17084    1.00    0.00    0.00    2.00    1.00     0  stress
04:36:25        0     31116    0.00    1.00    0.00    0.00    1.00     0  atopacctd
...

所有进程CPU使用率加起来也才20%多,远达不到80%。没办法,再详细的查看top的输出:


$ top
top - 04:58:24 up 14 days, 15:47,  1 user,  load average: 3.39, 3.82, 2.74
Tasks: 149 total,   6 running,  93 sleeping,   0 stopped,   0 zombie
%Cpu(s): 77.7 us, 19.3 sy,  0.0 ni,  2.0 id,  0.0 wa,  0.0 hi,  1.0 si,  0.0 st
KiB Mem :  8169348 total,  2543916 free,   457976 used,  5167456 buff/cache
KiB Swap:        0 total,        0 free,        0 used.  7363908 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 6947 systemd+  20   0   33104   3764   2340 S   4.0  0.0   0:32.69 nginx
 6882 root      20   0   12108   8360   3884 S   2.0  0.1   0:31.40 docker-containe
15465 daemon    20   0  336696  15256   7576 S   2.0  0.2   0:00.62 php-fpm
15466 daemon    20   0  336696  15196   7516 S   2.0  0.2   0:00.62 php-fpm
15489 daemon    20   0  336696  16200   8520 S   2.0  0.2   0:00.62 php-fpm
 6948 systemd+  20   0   33104   3764   2340 S   1.0  0.0   0:00.95 nginx
15006 root      20   0 1168608  65632  37536 S   1.0  0.8   9:51.09 dockerd
15476 daemon    20   0  336696  16200   8520 S   1.0  0.2   0:00.61 php-fpm
15477 daemon    20   0  336696  16200   8520 S   1.0  0.2   0:00.61 php-fpm
24340 daemon    20   0    8184   1616    536 R   1.0  0.0   0:00.01 stress
24342 daemon    20   0    8196   1580    492 R   1.0  0.0   0:00.01 stress
24344 daemon    20   0    8188   1056    492 R   1.0  0.0   0:00.01 stress
24347 daemon    20   0    8184   1356    540 R   1.0  0.0   0:00.01 stress
...

发现就绪队列有6个Running状态的进程,而且nginx和php-fpm进程都处于Sleep状态,真正处于R状态的是几个stress进程,来看看这些stress进程干嘛的?


$ pidstat -p 24344

16:14:55      UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command


# 从所有进程中查找PID是24344的进程
$ ps aux | grep 24344
root      9628  0.0  0.0  14856  1096 pts/0    S+   16:15   0:00 grep --color=auto 24344


$ top
...
%Cpu(s): 80.9 us, 14.9 sy,  0.0 ni,  2.8 id,  0.0 wa,  0.0 hi,  1.3 si,  0.0 st
...

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 6882 root      20   0   12108   8360   3884 S   2.7  0.1   0:45.63 docker-containe
 6947 systemd+  20   0   33104   3764   2340 R   2.7  0.0   0:47.79 nginx
 3865 daemon    20   0  336696  15056   7376 S   2.0  0.2   0:00.15 php-fpm
  6779 daemon    20   0    8184   1112    556 R   0.3  0.0   0:00.01 stress
...

发现进程没了,但是top输出的us列还是高居不下,而且有新的stress进程产生。进程PID在变,说明,要么进程在重启,要么是全新的进程,这无非两个原因:

  • 进程在不断崩溃重启,比如配置错误、段错误等,这是进程不断地被监控系统自动重启。
  • 短时进程,进程一运行就结束了。

stress,前面提到过,长用的压测工具。这里PID在变化,像是在被其他进程调用,所以这里需要先找到它的父进程。用pstree命令就可以用树状形式显示所有的进程之间的关系:


$ pstree | grep stress
        |-docker-containe-+-php-fpm-+-php-fpm---sh---stress
        |         |-3*[php-fpm---sh---stress---stress]

找打了父进程php-fpm,直接进入器内部分析源码:


# 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);



$ cat app/index.php
<?php
// fake I/O with stress (via write()/unlink()).
$result = exec("/usr/local/bin/stress -t 1 -d 1 2>&1", $output, $status);
if (isset($_GET["verbose"]) && $_GET["verbose"]==1 && $status != 0) {
  echo "Server internal error: ";
  print_r($output);
} else {
  echo "It works!";
}
?>

可以看到源码里每个请求都在调用stress,模拟IO压力,但是在top输出并没有看到iowait列值升高,接着往下走:


$ curl http://192.168.0.10:10000?verbose=1
Server internal error: Array
(
    [0] => stress: info: [19607] dispatching hogs: 0 cpu, 0 io, 0 vm, 1 hdd
    [1] => stress: FAIL: [19608] (563) mkstemp failed: Permission denied
    [2] => stress: FAIL: [19607] (394) <-- worker 19608 returned error 1
    [3] => stress: WARN: [19607] (396) now reaping child worker processes
    [4] => stress: FAIL: [19607] (400) kill error: No such process
    [5] => stress: FAIL: [19607] (451) failed run completed in 0s
)

在命令中加入了verbose=1参数,可以看到stress的输出。这里可以看到错误信息mkstemp failed: Permission denied,以及failed run completed in 0s。原来stress进程没有执行成功,因为权限问题失败而退出。可以猜测,正是由于权限错误,大量stress进程启动时初始化失败,进而导致CPU使用率升高。这里只是猜测,但是以上使用的工具并不能看到这些stress进程。

前面已经讲过perf工具用来分析CPU性能事件这里尝试看看:


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

# 查看报告
$ perf report

    

stress占了所有CPU时钟事件的77%,而stress中调用最多的是random(),看来确实是CPU使用率高的元凶。只要修复权限问题,并减少或删除stress的调用,就能解决问题。

但是在实际生产环境中,即使你找到触发瓶颈问题的命令行后,却发现他是核心逻辑的一部分,并不能轻易减少或删除。那你就要继续排查为什么被调用的命令,会导致CPU使用率或者IO的升高。

到这里我们是不是感觉以上排查过程好生复杂,那有没有更好监控方法呢?答案是肯定的。execsnoop就是专为短时进程设计的工具,他通过ftrace实时监控进程的exec()行为,并输出短时进程 的基本信息,ftrace是一种常用动态追踪技术,一般用于分析Linux内核的运行是行为。如上述案例,可以直接看到stress进程的父进程PID,命令行参数,且有大量stress进程在不停启动。


$ execsnoop
PCOMM            PID    PPID   RET ARGS
sh               30394  30393    0
stress           30396  30394    0 /usr/local/bin/stress -t 1 -d 1
sh               30398  30393    0
stress           30399  30398    0 /usr/local/bin/stress -t 1 -d 1
sh               30402  30400    0
stress           30403  30402    0 /usr/local/bin/stress -t 1 -d 1
sh               30405  30393    0
stress           30407  30405    0 /usr/local/bin/stress -t 1 -d 1
...

以上是学习极客时间专栏(倪朋飞:Linux性能优化实战)的个人总结

https://time.geekbang.org/column/intro/140

发布了37 篇原创文章 · 获赞 20 · 访问量 4945

猜你喜欢

转载自blog.csdn.net/qq_24436765/article/details/103595502