你的准备
下面我们就以 Nginx + PHP 的 Web 服务为例,来看看当你发现 CPU 使用率过高的问题后,要怎么使用 top 等工具找出异常的进程,又要怎么利用 perf 找出引发性能问题的函数。
・机器配置:4 CPU, 2GB内存
[root@www ~]# cat /proc/cpuinfo | grep 'physical id' | sort | uniq
physical id : 0
physical id : 2
physical id : 4
physical id : 6
•预先安装 sysstat、perf、ab 等工具
[root@www ~]# yum install httpd-tools perf sysstat -y
Package httpd-tools-2.4.6-93.el7.centos.x86_64 already installed and latest version
Package perf-3.10.0-1127.13.1.el7.x86_64 already installed and latest version
Package sysstat-10.1.5-19.el7.x86_64 already installed and latest version
我先简单介绍一下这次新使用的工具 ab。ab(apache bench)是一个常用的 HTTP 服务性能测试工具,这里用来模拟 Ngnix 的客户端。由于 Nginx 和 PHP 的配置比较麻烦,我把它们打包成了两个 Docker 镜像,这样只需要运行两个容器,就可以得到模拟环境。
你可以看到,其中一台用作Web服务器,来模拟性能问题,另一台用作Web服务器的客 端,来给Web服务增加压力请求。使用两台虚拟机是为了相互隔离,避免"交叉感染”.接下来,我们打开两个终端,分别SSH登录到两台机器上,并安装上面提到的工具。
不过,操作之前,我还想再说一点。这次案例中PHP应用的核心逻辑比较简单,大部分人一眼 就可以看出问题,但你要知道,实际生产环境中的源码就复杂多了。
所以,我希望你在按照步骤操作之前,先不要查看源码(避免先入为主),而是把它当成一个黑盒来分析。这样你可以更好地理解整个解决思路,怎么从系统的资源使用问题出发,分析出瓶颈所在的应用以及瓶颈在应用中的大概位置。
操作和分析
首先,在第一个终端执行下面的命令来运行 Nginx 和 PHP 应用:
[root@localhost ~]# docker run --name nginx -p 10000:80 -itd feisky/nginx
464c559c39caa60d8669487b8fb817eedbe6c75af030afd87550c08ff2dac495
[root@localhost ~]# docker run --name phpfpm -itd --network container:nginx feisky/php-fpm
310458478b2a12effbfa3e8bdb65a796b62a81cde984f6efcc785ba18d1080c9
[root@localhost ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
310458478b2a feisky/php-fpm "php-fpm -F --pid /o…" About a minute ago Up About a minute phpfpm
464c559c39ca feisky/nginx "nginx -g 'daemon of…" About a minute ago Up About a minute 0.0.0.0:10000->80/tcp nginx
在第一个终端使用curl访问http://[VM1的IP]:10000,确认Nginx已正常启动,你应该可以看到It works!的响应。
[root@localhost ~]# curl 192.168.179.99:10000
It works!
接着,我们来测试一下这个Nginx服务的性能。在第二个终端运行下面的ab命令:
#并发10个请求测试Nginx性能,总共测试100个请求
[root@localhost ~]# ab -c 10 -n 10000 http://192.168.179.99:10000/
.......................
Requests per second: 7.25 [#/sec] (mean)
Time per request: 1379.778 [ms] (mean)
.......................
从ab的输出结果我们可以看到,Nginx能承受的每秒平均请求数只有7.25。你一定在吐槽, 这也太差了吧。那到底是哪里出了问题呢?我们用top和pidstat再来观察下。
这次,我们在第二个终端,将测试的请求总数增加到10000。这样当你在第一个终端使用性能分析工具时,Nginx的压力还是继续。
[root@localhost ~]# ab -c 10 -n 10000 http://192.168.179.99:10000/
接着,回到第一个终端运行top命令,并按下数字1 ,切换到每个CPU的使用率:
[root@localhost ~]# top
top - 13:23:33 up 1 day, 13:44, 2 users, load average: 2.26, 0.58, 0.23
Tasks: 113 total, 6 running, 107 sleeping, 0 stopped, 0 zombie
%Cpu0 : 99.7 us, 0.3 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 : 99.7 us, 0.3 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 1765672 total, 145092 free, 176300 used, 1444280 buff/cache
KiB Swap: 524284 total, 477760 free, 46524 used. 1287080 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
28847 bin 20 0 336684 7352 1616 R 41.2 0.4 0:13.14 php-fpm
28849 bin 20 0 336684 7352 1616 R 40.9 0.4 0:12.95 php-fpm
28848 bin 20 0 336684 7352 1616 R 38.9 0.4 0:13.27 php-fpm
28850 bin 20 0 336684 7356 1620 R 38.9 0.4 0:12.50 php-fpm
28846 bin 20 0 336684 7356 1620 R 37.9 0.4 0:12.53 php-fpm
28845 root 20 0 58776 3000 2240 S 1.0 0.2 0:00.13 ab
17478 root 20 0 627036 70424 27348 S 0.7 4.0 2:44.93 dockerd
28674 101 20 0 33092 2116 776 S 0.3 0.1 0:00.23 nginx
28851 root 20 0 157584 2140 1508 R 0.3 0.1 0:00.07 top
这里可以看到,系统中有几个php-fpm进程的CPU使用率加起来接近200%,而每个CPU的用户使用率(us)也已经超过了 99%,接近饱和。这样我们就可以确认,正是用户空间的 php-fpm进程,导致CPU使用率骤升。
那再往下走,怎么知道是php-fpm的哪个函数导致了 CPU使用率升高呢?我们来用perf分析 一下。在第一个终端运行下面的perf命令:
# -g开启调用关系分析,-p指定php-fpm的进程号21515
[root@localhost ~]# perf top -g -p 21515
按方向键切换到php-fpm,再按下回车键展开php-fpm的调用关系,你会发现,调用关系最 终到了 sqrt和emalloc看来,我们需要从这两个函数入手了。
Samples: 39K of event 'cpu-clock', 4000 Hz, Event count (approx.): 5609807665 lost: 0/0 drop: 0/0
Shared Object Symbol
- php-fpm [.] execute_ex
- 21.39% execute_ex
- 13.08% ZEND_DO_FCALL_SPEC_CONST_HANDLER
- 15.10% zend_do_fcall_common_helper_SPEC
+ 4.28% _emalloc
+ 4.17% zif_sqrt
+ 2.95% zend_vm_stack_clear_multiple
1.57% __sqrt
我们拷贝出 Nginx 应用的源码,看看是不是调用了这两个函数:
从容器phpfpm中将PHP源码拷贝出来
[root@localhost ~]# docker cp phpfpm:/app .
[root@localhost ~]# ls
anaconda-ks.cfg app
# 使用grep查找函数调用
[root@localhost ~]# grep sqrt -r app/
app/index.php: $x += sqrt($x);
OK,原来只有sqrt函数在app/index.php文件中调用了。那最后一步,我们就该看看这个文件的源码了:
[root@localhost ~]# cat app/index.php
<?php
// test only.
$x = 0.0001;
for ($i = 0; $i <= 1000000; $i++) {
$x += sqrt($x);
}
echo "It works!"
修改代码将for循环去掉 ,在跑一下
[root@localhost ~]# docker rm -f phpfpm
phpfpm
[root@localhost ~]# docker run --name phpfpm -itd --network container:nginx -v /root/index.php:/app/index.php feisky/php-fpm
4d8ba484bb910c813c5552b663e058edd0ed506890de30df0a17292d5b9fa5a2
[root@localhost ~]# docker exec -it phpfpm /bin/bash
root@464c559c39ca:/app# ls
404.html index.php ok.php phpinfo.php
root@464c559c39ca:/app# cat index.php
<?php
// test only.
echo "It works!"
?>
[root@localhost ~]# ab -c 10 -n 10000 http://192.168.179.99:10000/
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Requests per second: 3008.24 [#/sec] (mean)
Time per request: 3.324 [ms] (mean)
从这里你可以发现,现在每秒的平均请求数,已经从原来的7.25变成了3008.24。
你看这么很傻的小问题却会极大的影响性能,并且査找起来也并不容易吧。当然找到问题后,解决方法就简单多了,删除测试代码就可以了。
小结
CPU 使用率是最直观和最常用的系统性能指标,更是我们在排查性能问题时,通常会关注的第一个指标。所以我们更要熟悉它的含义,尤其要弄清楚用户(%user)、Nice(%nice)、系统(%system) 、等待 I/O(%iowait) 、中断(%irq)以及软中断(%softirq)这几种不同 CPU 的使用率。比如说:
- 用户 CPU 和 Nice CPU 高,说明用户态进程占用了较多的 CPU,所以应该着重排查进程的性能问题
- 系统 CPU 高,说明内核态占用了较多的 CPU,所以应该着重排查内核线程或者系统调用的性能问题
- I/O 等待 CPU 高,说明等待 I/O 的时间比较长,所以应该着重排查系统存储是不是出现了 I/O 问题
- 软中断和硬中断高,说明软中断或硬中断的处理程序占用了较多的 CPU,所以应该着重排查内核中的中断服务程序
碰到 CPU 使用率升高的问题,你可以借助 top、pidstat 等工具,确认引发 CPU 性能问题的来源,再使用 perf 等工具,排查出引起性能问题的具体函数。