不知道大家有没有遇到过服务器的CPU使用率达到了100%的情况,在实际生产环境中如果遇到了这种情况我们该怎么办?

接下来我就跟大家探讨一下:当CPU的使用率达到了100%的时候我们该如何排查、定位、找出问题根源。

CPU使用率

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

CPU使用率:就是CPU处在非空闲状态的时间占总CPU时间的百分比

而CPU使用率中也有很多相关的重要指标,我们可以通过 top 命令简单列举出来

  • 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 时间。

如何查看CPU使用率

在介绍查看CPU使用率的工具之前,我们先来想一个问题:这些性能工具是怎么计算CPU使用率的?

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

这个公式,就是大多数性能工具所看到的CPU使用率的实际计算方法。所以,在使用这些工具的时候我们要注意间隔时间的设置

top 工具和 ps 工具

top 和 ps 是最常用的性能分析工具,其中:

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

ps 显示每个进程的资源使用情况

sysstat 工具

除了上面这两个最常用的工具之外,我们还可以使用 sysstat 工具中的 pidstat 命令来查看进程的CPU使用率

格式:

pidstat [ 选项 ] [ <时间间隔> ] [ <次数> ]

# 常用选项
-u:默认的参数,显示各个进程的cpu使用统计
-r:显示各个进程的内存使用统计
-d:显示各个进程的IO使用情况
-p:指定进程号
-w:显示每个进程的上下文切换情况
-t:显示选择任务的线程的统计信息外的额外信息

上面这些工具你可以轻松找出哪个CPU使用率较高的进程,但是找到进程还不够,我们还想找出是哪个具体进程或者具体函数占用了如此高的CPU时间,这样才能进行更好的优化、

这里我推荐一个可以在第一时间分析进程的CPU问题的工具——perf

perf top

perf 是一种 Linux 内置的性能分析工具。它以性能事件采样为基础,不仅可以分析系统的各种事件和内核进程,还可以用来分析指定应用程序的性能问题

perf top 类似于 top ,它能够实时显示占用CPU时钟最多的函数或者指令,因此可以查找出热点函数

$ 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

从输出结果我们可以看出:

第一行包含三个数据:采样数(Samples)、事件类型(event)和事件总数量(Event count)。这个例子中 perf 总共采集了833个CPU时钟时间,而总事件数为97742399

接着我们从列的角度来看:

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

perf record 、perf report

perf top 虽然实时展示了系统的性能信息,但是它并不能保存数据,就意味着无法用于离线或者后续的分析

而perf record 则提供了保存数据的功能,保存后的数据,需要你用 perf report 解析展示。

$ perf record # 按Ctrl+C终止采样
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.452 MB perf.data (6093 samples) ]

$ perf report # 展示类似于perf top的报告。有时候还会加上-g参数来开启调用关系的采样

案例

下面我将用极客时间里的一个例子,来展现当我们发现CPU使用率过高的问题后,要怎么使用各种性能工具找出异常的进程,又要怎么利用各种工具找出引发性能问题的函数

这次案例里面,我们预先安装了 sysstat、perf、ab 等工具

左边这台用作 Web 服务器,来模拟性能问题;右边用作客户端,来给 Web 服务器增加压力请求

首先运行我们的 web 服务器,运行之后验证一下 Nginx 是否正常开启

$curl 192.168.1.1:80

It works!

接下来我们在客户端测试一下 web 服务器的性能

# 并发10个请求测试Nginx性能,总共测试100个请求
$ ab -c 10 -n 100 http://192.168.1.1:80
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)
...

从ab的输出可以看到,Nginx能承受的每秒平均请求数只有11.63。这个结果说明Nginx 目前的性能不尽人意,我们先用top和pidstat看一下到底是哪里出了问题

运行ab命令,持续给Nginx压力,方便我们使用性能分析工具

#并发10个请求,总共10000个请求
$ ab -c 10 -n 10000 http://192.168.1.1:80

运行 top 命令,按下数字1,切换到每个CPU使用情况(单核系统不需要按)

$ 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的用户态使用率(us)也已经超过了98%

所以我们可以得出结论:用户空间的php-fpm进程,导致了CPU使用率骤升

接下来我们需要找出 php-fpm 进程里面哪个函数导致了CPU使用率的升高

首先运行我们的 perf top 命令,实时分析进程的CPU问题

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

按方向键切换到php-fpm,再按下回车键开php-fpm的调用关系,你会发现,调用关系最终到了sqrt 和add_ function函数。

找出了函数后我们再查看源码并修改优化

优化后我们再测试一下

$ ab -c 10 -n 10000 http://192.168.1.1:80
...
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