前面两篇博客已经说到很多的关于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性能优化实战)的个人总结