Linux CPU 性能优化

1. 什么是平均负载

uptime 指令显示内容如下:

1
22:02:00 up 17 days, 10:57,  1 user,  load average: 0.00, 0.01, 0.05

前面:当前时间、系统运行时间、正在运行用户数。后面分别 1 分钟、5 分钟、15 分钟的平均负载(Load Average)。

平均负载是什么?是 CPU 的使用率吗?明显不对,平均负载是指单位时间内,系统处于可运行状态和不可中断状态的平均进程数,也就是平均活跃进程数,它和 CPU 使用率并没有直接关系

可运行状态的进程,是指正在使用 CPU 或者正在等待 CPU 的进程,也就是我们常用 ps 命令看到的,处于 R 状态(Running 或 Runnable)的进程。

不可中断状态的进程则是正处于内核态关键流程中的进程,并且这些流程是不可打断的,比如最常见的是等待硬件设备的 I/O 响应,也就是我们在 ps 命令中看到的 D 状态(Uninterruptible Sleep,也称为 Disk Sleep)的进程。

平均负载不仅包括了正在使用 CPU 的进程,还包括等待 CPU 和等待 I/O 的进程。只有当 CPU 密集或大量等待 CPU 的时候导致平均负载升高时,平均负载和 CPU 使用率的情况可以对应,在 IO 密集型中平均负载上升而 CPU 使用率不一定高。

既然平均的是活跃进程数,那么最理想的,就是每个 CPU 上都刚好运行着一个进程,这样每个 CPU 都得到了充分利用。

如何知道 CPU 数呢?通过 grep 'model name' /proc/cpuinfo | wc -l 有几行就是几个 CPU 。

2. 平均负载高的不同情况

2.1. CPU 密集型进程

  1. 先用 stress --cpu 1 --timeout 600 模拟一个 CPU 使用率 100% 的场景。
  2. 在第二个终端中用 watch -d uptime 查看平均负载的变化情况,其中 -d 表示高亮变化部分。
  3. 在第三个终端中用 mpstat -P ALL 5 查看 CPU 使用率的变化情况,其中 -P ALL 表示监控所有 CPU,可以发现一个 CPU 的负载会达到 100%。
    • 1 分钟的平均负载会慢慢增加到 1.00,而从终端三中还可以看到,正好有一个 CPU 的使用率为 100%,但它的 iowait 只有 0。这说明,平均负载的升高正是由于 CPU 使用率为 100% 。
  4. 通过 pidstat -u 5 1 可以查看具体进程情况,其中 -u 5 1 表示每 5 秒输出一次。
    • 从这里可以明显看到,stress 进程的 CPU 使用率为 100%。

2.2 IO 密集型进程

  1. 使用 stress -i 1 --timeout 600 模拟 IO 压力。
  2. 同理第二个终端用 watch -d uptime 查看平均负载。
  3. 依然用 mpstat -P ALL 5 1 可以发现 CPU 的使用率情况。
    • 从这里可以看到,1 分钟的平均负载会慢慢增加到 1.06,其中一个 CPU 的系统 CPU 使用率升高到了 23.87,而 iowait 高达 67.53%。这说明,平均负载的升高是由于 iowait 的升高。
  4. 最后也是通过 pidstat -u 5 1 找到相应的 pid。

2.3 大量进程的场景

  1. 使用 stress -c 8 --timeout 600 模拟 8 个进程,阿里云上只有一个 CPU,显然会出现负载过高。
  2. 同理用 watch -d uptime 查看平均负载。
    • 由于系统只有 1 个 CPU,明显比 8 个进程要少得多,因而,系统的 CPU 处于严重过载状态,平均负载高达 8 或 9;
  3. 通过 pidstat -u 5 1 发现有 8 个 stress 进程占用了 CPU。
    • 可以看出,8 个进程在争抢 1 个 CPU,每个进程等待 CPU 的时间(也就是代码块中的 %wait 列)高达 75%。这些超出 CPU 计算能力的进程,最终导致 CPU 过载。

3. CPU 上下文切换

3.1 CPU 上下文切换的三个种类

如上为 CPU 上下文的抽象图,根据任务的执行形式的不同,CPU 的下上文切换有:进程上下文切换、线程上下文切换、中断上下文切换这三类。

进程上下文切换,是指从一个进程切换到另一个进程运行;而系统调用(特权模式切换)过程中一直是同一个进程在运行,属于同进程内的 CPU 上下文切换。

进程的切换只能发生在内核态,进程的上下文切换需要比系统调用多做一步:在保存当前进程的内核状态和 CPU 寄存器之前,需要先把该进程的虚拟内存、栈等保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。

频繁的进程上下文切换会出现性能隐患,可能出现切换的场景:

  1. 时间片耗尽,当前进程必须挂起;
  2. 资源不足的,在获取到足够资源之前进程挂起;
  3. 进程 sleep 挂起进程;
  4. 高优先级进程导致当前进度挂起;
  5. 硬件中断,导致当前进程挂起;

进程有上下文切换,线程也有,线程是调度的基本单位,而进程则是资源拥有的基本单位。线程的上下文切换有两种情况:

  1. 前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样;
  2. 前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,只需要切换线程间不共享的数据。

除了前面两种上下文切换,还有一个场景也会切换 CPU 上下文,那就是中断。

快速响应硬件的事件,中断处理会打断进程的正常调度和执行。同一 CPU 内,硬件中断优先级高于进程。切换过程类似于系统调用的时候,不涉及到用户运行态资源。大量的中断上下文切换同样可能引发性能问题。

3.2 CPU 上下文切换实验

vmstat 是一个常用的系统性能分析工具,用来分析系统的内存使用情况,也可以分析 CPU 上下文切换和中断的次数。

1
2
3
4
5
6
7
8
9
[root@koonchen ~]# vmstat 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
6 0 0 128740 140324 1337544 0 0 12 6 15 16 1 0 99 0 0
0 0 0 128740 140332 1337544 0 0 0 2 309 879 0 0 99 0 0
0 0 0 128740 140332 1337544 0 0 0 0 294 859 0 0 100 0 0
0 0 0 128740 140332 1337544 0 0 0 0 295 861 0 0 99 0 0
0 0 0 128244 140332 1337544 0 0 0 0 323 912 0 0 99 0 0
0 0 0 128196 140336 1337544 0 0 0 114 297 866 0 0 100 0 0
  • cs(context switch)是每秒上下文切换的次数。
  • in(interrupt)则是每秒中断的次数。
  • r(Running or Runnable)是就绪队列的长度,也就是正在运行和等待 CPU 的进程数。
  • b(Blocked)则是处于不可中断睡眠状态的进程数。

vmstat 只能看到总体的情况,每个进程的详细情况需要使用 pidstat -w

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@koonchen ~]# pidstat -w 5 1
Linux 3.10.0-514.26.2.el7.x86_64 (koonchen) 06/18/2020 _x86_64_ (1 CPU)

01:44:20 AM UID PID cswch/s nvcswch/s Command
01:44:25 AM 0 3 0.40 0.00 ksoftirqd/0
01:44:25 AM 0 6 0.20 0.00 kworker/u2:0
01:44:25 AM 0 9 12.45 0.00 rcu_sched
01:44:25 AM 0 10 0.20 0.00 watchdog/0
01:44:25 AM 0 745 0.40 0.00 aliyun-service
01:44:25 AM 0 956 10.04 0.00 AliYunDun
01:44:25 AM 0 1672 8.43 0.00 kworker/0:2
01:44:25 AM 0 1673 0.20 0.00 sshd
01:44:25 AM 0 1700 0.20 0.00 pidstat
01:44:25 AM 0 9668 10.04 0.00 AliSecGuard
  • cswch 每秒自愿上下文切换(voluntary context switches)的次数;
    • 自愿上下文切换,是指进程无法获取所需资源,导致的上下文切换。
    • 比如说, I/O、内存等系统资源不足时,就会发生自愿上下文切换。
  • nvcswch 每秒非自愿上下文切换(non voluntary context switches)的次数。
    • 进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。
    • 比如说,大量进程都在争抢 CPU 时,就容易发生非自愿上下文切换。

sysbench 是一个多线程的基准测试工具,一般用来评估不同系统参数下的数据库负载情况。

  1. 第一个终端使用 sysbench --threads=10 --max-time=300 threads run 表示以 10 个线程运行 5 分钟的基准测试,模拟多线程切换的问题。
  2. 第二个终端使用 vmstat 1 表示每隔 1 秒输出 1 组数据。
    • cs(上下文切换的次数)的数值达到了 160 万左右。
    • r(就绪队列的长度)的数值达到了 8 左右,超过了 CPU 的个数 1 。
    • us 表示用户态,sy 表示内核态,可以看到 us 在 10% 左右,sy 在 90% 左右。
    • in(每秒中断的次数)的数值在 1300 左右,说明中断处理是一个潜在的问题。
    • 综上,系统的就绪队列过长,也就是正在运行和等待 CPU 的进程数过多,导致了大量的上下文切换,而上下文切换又导致了系统 CPU 的占用率升高。
  3. 第三个终端使用 pidstat -w -u 1-w 参数表示输出进程切换指标,而 -u 参数则表示输出 CPU 使用指标。
    • 可以发现占用 CPU 最高的是 sysbench
    • 但是在这里自愿与非自愿的上下文切换加起来也远远达不到 vmstat 中 160 万的数值,这是因为 Linux 的调度基本单位是线程, sysbench 模拟的也是线程调度,所以在 pidstat 中加上 -t 参数才会显示线程指标。
  4. 剩下的是中断问题,为什么中断达到了 1300 左右,在 /proc/interrupts 文件汇总可以读取,它提供了中断的使用情况,通过 watch -d cat /proc/interrupts 查看。
    • 在单核下没有 CPU 切换的展示,事实上这里应该转变最快的是 RES(重调度中断),它表示唤醒空闲状态的 CPU 来调度新的任务运行。

stress 基于多进程的,会 fork 多个进程,导致进程上下文切换,导致 us 开销很高;

sysbench 基于多线程的,会创建多个线程,单一进程基于内核线程切换,导致 sy 的内核开销很高;

首先通过 uptime 查看系统负载,然后使用 mpstat 结合 pidstat 来初步判断到底是 CPU 计算量大还是进程争抢过大或者是 io 过多,接着使用 vmstat 分析切换次数,以及切换类型,来进一步判断到底是 io 过多导致问题还是进程争抢激烈导致问题。

4. 应用 CPU 利用率 100% 怎么办?

4.1 什么是 CPU 使用率?

Linux 事先定义了节拍率 HZ 每当触发一次时间中断,全局变量 Jiffies 就会加 1。这个值在 /boot/config 中被定义。

1
2
[root@koonchen ~]# grep 'CONFIG_HZ=' /boot/config-3.10.0-1127.10.1.el7.x86_64
CONFIG_HZ=1000

这里的 1000 表示每秒钟触发 1000 次时间中断,节拍率是内核选项,用户空间节拍率是 USER_HZ,它总是 100,Linux 通过 /proc 文件向用户提供系统内部的信息,在 /proc/stat 提供系统的 CPU 和任务统计信息。

1
2
3
[root@koonchen ~]# cat /proc/stat | grep ^cpu
cpu 2070722 339 978764 233911909 69735 0 11212 0 0 0
cpu0 2070722 339 978764 233911909 69735 0 11212 0 0 0

这里的第一行是之后每一行的累加结果,具体含义:

  • 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),代表以低优先级运行虚拟机的时间。

重点在于时间间隔,比如 top 里的时间间隔是 3s 的,而 ps 则是进程的整个生命周期。

top 对于每个进程没有细分用户态和内核态的 CPU 使用率,可以通过 pidstat 查看每个进程的 CPU 使用详情,包括:

  • 用户态 CPU 使用率 (%usr);
  • 内核态 CPU 使用率(%system);
  • 运行虚拟机 CPU 使用率(%guest);
  • 等待 CPU 使用率(%wait);
  • 以及总的 CPU 使用率(%CPU)。

4.2 CPU 使用率过高怎么办?

通过 top ps pidstat 等工具可以找到 CPU 高的进程,但是是哪个函数呢?首先能想到的是 GDB(The GNU Project Debugger),但是它不适合在性能分析的早期使用,因为 GDB 会中断程序,在线上是不允许的,它适合在分析的后期,在线下调试函数内部的问题,这里推荐使用 perf,它是 Linux 2.6.31 后内置的性能分析工具。

1
2
3
4
5
6
7
8
9
10
11
12
[root@koonchen ~]# perf top
Samples: 2K of event 'cpu-clock', 4000 Hz, Event count (approx.): 310098968 lost: 0/0 drop: 0/0
Overhead Shared Object Symbol
9.09% [kernel] [k] finish_task_switch
7.96% [kernel] [k] _raw_spin_unlock_irqrestore
5.37% perf [.] rb_next
5.15% perf [.] __symbols__insert
3.99% [kernel] [k] run_timer_softirq
2.70% [kernel] [k] tick_nohz_idle_enter
1.92% [kernel] [k] __do_softirq
1.52% [kernel] [k] kallsyms_expand_symbol.constprop.1
1.47% perf [.] rb_insert_color

使用 perf top 的效果如上所示,显示占用 CPU 时钟最多的函数或指令,第一行分别是:采样数、事件类型、事件总量。这里是 2000 个采样,类型是 cpu-clock,事件总量是 310098968。

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

此外还能使用 perf recordperf report 进行保存数据、离线分析,输出的报告和 perf top 类似。

4.3 案例分析

在虚拟机(1 CPU 2 GB)上运行 Nginx + PHP 的 Web 服务,本地通过 apache bench 进行压力测试,使用并发 10 请求,共发送 100 个请求:

1
2
3
ab -c 10 -n 100 http://xxx:10000/
Requests per second: 11.68 [#/sec] (mean)
Time per request: 856.416 [ms] (mean)

发现每秒平均只有 11.68 ,然后将请求总数提高到 10000,在第一个终端用 top 查看 CPU 的情况。

1
2
3
4
5
6
7
 PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
4731 bin 20 0 336684 9372 1692 R 19.9 0.5 0:06.03 php-fpm
4730 bin 20 0 336684 9368 1688 R 19.6 0.5 0:06.14 php-fpm
4732 bin 20 0 336684 9364 1684 R 19.6 0.5 0:05.94 php-fpm
4733 bin 20 0 336684 9364 1684 R 19.6 0.5 0:05.94 php-fpm
4734 bin 20 0 336684 9364 1684 R 19.6 0.5 0:05.85 php-fpm
956 root 10 -10 160856 44608 5880 S 0.7 2.4 217:43.96 AliYunDun

可以发现用户空间的 php-fpm 使用了几乎 100% 的 CPU,然后通过 perf 查看函数。因为 PHP 与 Nginx 在 docker 内执行,如果直接通过 perf top 是看不到具体方法的,只能看到 16 进制的地址,需要通过 perf record -g -p <pid> 保存结果,然后 docker cp perf.data xxx:/tmp 拷贝,使用 docker exec -i -t xxx bash 进入容器,然后安装 perf 工具即 apt-get update && apt-get install -y linux-perf linux-tools procps,最后使用 perf report 查看报告。

可以发现 sqrt 和 add_function 函数有问题,可以将源码拷出来使用 docker cp phpfpm:/app .。接着从源码发现,问题出在了 sqrt 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@koonchen ~]# grep sqrt -r app/
app/index.php: $x += sqrt($x);
[root@koonchen ~]# grep add_function -r app/
[root@koonchen ~]# cat app/index.php
<?php
// test only.
$x = 0.0001;
for ($i = 0; $i <= 1000000; $i++) {
$x += sqrt($x);
}

echo "It works!"
?>

使用修复结果进行 ab 测试,每秒处理数从 11 变成 271:

1
2
3
ab -c 10 -n 10000 -k http://xxx:10000/
Requests per second: 271.09 [#/sec] (mean)
Time per request: 36.888 [ms] (mean)

5. 找到 CPU 利用率高的应用

当 CPU 使用率高的时候,我们不一定能找到相应的高 CPU 使用率进程,现在在虚拟机上启动一个 Nginx 和 PHP 然后通过本地访问 curl:

1
2
3
4
5
docker run --name nginx -p 10000:80 -itd feisky/nginx:sp
docker run --name phpfpm -itd --network container:nginx feisky/php-fpm:sp

curl http://xxx:10000/
It works!

通过 ab 进行并发 100 共 1000 的请求:

1
2
3
4
ab -c 100 -n 1000 http://xxx:10000/

Requests per second: 70.60 [#/sec] (mean)
Time per request: 1416.448 [ms] (mean)

每秒请求数只有 70,接着就来处理这个问题了,现在把并发改成 5,时间为 10 分钟。现在去服务器,发现使用 top 以后 CPU 使用率为 80%,但是显示的进程占用都不高,然后用 pidstat 查看。

1
2
3
4
5
6
7
8
9
10
11
[root@koonchen ~]# pidstat 1
Linux 3.10.0-514.26.2.el7.x86_64 (koonchen) 06/21/2020 _x86_64_ (1 CPU)

01:12:35 PM UID PID %usr %system %guest %CPU CPU Command
01:12:36 PM 0 956 5.00 2.00 0.00 7.00 0 AliYunDun
01:12:36 PM 101 7560 1.00 1.00 0.00 2.00 0 nginx
01:12:36 PM 1 12214 0.00 2.00 0.00 2.00 0 php-fpm
01:12:36 PM 1 12221 1.00 0.00 0.00 1.00 0 php-fpm
01:12:36 PM 1 12228 1.00 1.00 0.00 2.00 0 php-fpm
01:12:36 PM 1 12238 1.00 1.00 0.00 2.00 0 php-fpm
01:12:36 PM 1 12239 0.00 1.00 0.00 1.00 0 php-fpm

所有进程的 CPU 使用率也不高,回到 top 再看看是否有遗漏信息。发现 Nginx 和 PHP 进程都处于 Sleep 状态,而 Running 状态的是 stress 进程,随便找到一个查看一下 pidstat -p 25858 竟然不存在,用 ps aux | grep 25858 状态已经变成了 S+,已经暂停了,同时在 top 中这个进程不见了,可能是因为:

  1. 进程不断崩溃重启;
  2. 进程是短时进程。

如果想要找到这个 stress 进程是怎么被调用的,就需要找到父进程,可以使用 pstree 工具。

1
2
[root@koonchen ~]# pstree | grep stress
| |-containerd-shim-+-php-fpm-+-2*[php-fpm---sh---stress---stress]

发现 stress 进程是被 PHP 调用的,将 PHP 源码拷贝到本地:

1
2
3
4
5
[root@koonchen ~]# docker cp phpfpm:/app .

[root@koonchen ~]# 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);

找到 index.php 源码:

1
2
3
4
5
6
7
8
9
10
<?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 压力,但是之前只看到 CPU 使用率升高,通过 verbose 参数但因日志:

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

因为 stress 命令没有成功,因为权限问题失败了,因此大量 stress 进程初始化失败,导致 CPU 升高。这些都仅仅是猜测,下一步是验证,通过 perf record -gperf report 查看报告,注意因为在 docker 中运行的进程,需要将报告复制进容器查看,可以发现是 stress 中 random() 函数占用了大量的 CPU 时钟。

像这类短时进程问题,可以用 execsnoop 工具,它可以直接找到 stress 进程的父进程 PID 以及命令行参数,之后会用到。

6. 大量不可用中断进程与僵尸进程

当 iowait 升高时进程很可能因为得不到硬件相应而处于不可中断状态,从 pstop 都发现它们属于 D 状态,也就是 Uninterruptible Sleep 状态。

  • R 是 Running 或 Runnable 的缩写,表示进程在 CPU 的就绪队列中,正在运行或者正在等待运行。
  • D 是 Disk Sleep 的缩写,也就是不可中断状态睡眠(Uninterruptible Sleep),一般表示进程正在跟硬件交互,并且交互过程不允许被其他进程或中断打断。
  • Z 是 Zombie 的缩写,它表示僵尸进程,也就是进程实际上已经结束了,但是父进程还没有回收它的资源(比如进程的描述符、PID 等)。
    • 一旦父进程没有处理子进程的终止,还一直保持运行状态,那么子进程就会一直处于僵尸状态。
  • S 是 Interruptible Sleep 的缩写,也就是可中断状态睡眠,表示进程因为等待某个事件而被系统挂起。当进程等待的事件发生时,它会被唤醒并进入 R 状态。
  • I 是 Idle 的缩写,也就是空闲状态,用在不可中断睡眠的内核线程上。
    • 前面说了,硬件交互导致的不可中断进程用 D 表示,但对某些内核线程来说,它们有可能实际上并没有任何负载,用 Idle 正是为了区分这种情况。
    • 要注意,D 状态的进程会导致平均负载升高, I 状态的进程却不会。
  • T 或者 t,也就是 Stopped 或 Traced 的缩写,表示进程处于暂停或者跟踪状态。
    • 向一个进程发送 SIGSTOP 信号,它就会因响应这个信号变成暂停状态(Stopped);再向它发送 SIGCONT 信号,进程又会恢复运行。
    • 而当你用调试器(如 gdb)调试一个进程时,在使用断点中断进程后,进程就会变成跟踪状态,这其实也是一种特殊的暂停状态,只不过你可以用调试器来跟踪并按需要控制进程的运行。
  • X,也就是 Dead 的缩写,表示进程已经消亡,所以你不会在 top 或者 ps 命令中看到它。

6.1 案例表现

使用 docker run --privileged --name=app -itd feisky/app:iowait 可以运行一个不断产生僵尸进程的应用,通过 ps aux | grep /app 可以看到有一个 Ss+ 和多个 D+ 的应用。

--privileged 参数使容器拥有了访问任何其它设备的权限。

  • S 表示可中断睡眠状态;
  • D 表示不可中断睡眠状态;
  • s 表示这个进程是一个会话的领导进程;
    • 表示前台进程组。

使用 top 看到如下信息(这里直接用教程的例子了,用单核服务器会卡死):

1
2
3
4
5
6
7
8
9
10
11
12
13
$ top
top - 05:56:23 up 17 days, 16:45, 2 users, load average: 2.00, 1.68, 1.39
Tasks: 247 total, 1 running, 79 sleeping, 0 stopped, 115 zombie
%Cpu0 : 0.0 us, 0.7 sy, 0.0 ni, 38.9 id, 60.5 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 : 0.0 us, 0.7 sy, 0.0 ni, 4.7 id, 94.6 wa, 0.0 hi, 0.0 si, 0.0 st
...

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
4340 root 20 0 44676 4048 3432 R 0.3 0.0 0:00.05 top
4345 root 20 0 37280 33624 860 D 0.3 0.0 0:00.01 app
4344 root 20 0 37280 33624 860 D 0.3 0.4 0:00.01 app
1 root 20 0 160072 9416 6752 S 0.0 0.1 0:38.59 systemd
...
  • 平均负载正在逐渐升高,说明系统很可能已经有了性能瓶颈;
  • 有 1 个正在运行的进程,但僵尸进程比较多,而且还在不停增加,说明有子进程在退出时没被清理;
  • 用户 CPU 和系统 CPU 都不高,但 iowait 分别是 60.5% 和 94.6%;
  • 每个进程的情况,CPU 使用率最高的进程只有 0.3%,看起来并不高;但有两个进程处于 D 状态,它们可能在等待 I/O,但光凭此并不能确定是它们导致了 iowait 升高。

通过这四个发现可以得到以下两个结论:

  1. iowait 太高了,导致系统的平均负载升高,甚至达到了系统 CPU 的个数;
  2. 僵尸进程在不断增多,说明有程序没能正确清理子进程的资源。

6.2 案例分析

查询系统的 IO 情况可以使用 dstat 工具,它可以同时查看 CPU 和 IO 两种资源,使用 dstat 1 10 展示信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 间隔1秒输出10组数据
dstat 1 10
--total-cpu-usage-- -dsk/total- -net/total- ---paging-- ---system--
usr sys idl wai stl| read writ| recv send| in out | int csw
0 0 96 4 0|1219k 408k| 0 0 | 0 0 | 42 885
0 0 2 98 0| 34M 0 | 198B 790B| 0 0 | 42 138
0 0 0 100 0| 34M 0 | 66B 342B| 0 0 | 42 135
0 0 84 16 0|5633k 0 | 66B 342B| 0 0 | 52 177
0 3 39 58 0| 22M 0 | 66B 342B| 0 0 | 43 144
0 0 0 100 0| 34M 0 | 200B 450B| 0 0 | 46 147
0 0 2 98 0| 34M 0 | 66B 342B| 0 0 | 45 134
0 0 0 100 0| 34M 0 | 66B 342B| 0 0 | 39 131
0 0 83 17 0|5633k 0 | 66B 342B| 0 0 | 46 168
0 3 39 59 0| 22M 0 | 66B 342B| 0 0 | 37 134

首先要处理 IO 高的问题,从上表发现,当 wai 升高即 iowait 上高时,read 请求会很大,可能是磁盘读导致的。通过 top 找到 D 状态的进程,可以找到其 PID,而查看一个进程的使用情况,就可以使用 pidstat,使用 -d 参数可以输出 IO 情况。但是可以发现 D 状态进程不一定存在问题,因此可以直接查看所有进程的 IO 情况。

这时可以发现 app 进程有大量的 kB_rd/s,表示每秒读的 KB 数,找到了进程后可以用 strace -p [pid] 来查看进程的追踪信息,然而追踪失败了,一般这种情况下,需要看看进程状态是否正常

回到 ps aux | grep [pid] 发现其状态变成了 Z,就是说进程变成了僵尸进程,用 perf record -gperf report 找到僵尸进程的调用栈,发现 app 应用中 IO 高是因为应用直接访问了磁盘,更改后的进程 iowait 会明显降低,但是僵尸进程的问题还是存在。

接下来僵尸进程的问题是因为父进程的调用错误,通过 pstree -aps <pid> 查看进程父子关系,a 表示输出命令行选项,p 表示 PID,s 表示指定进程的父进程,发现指向了 app 应用,查看其中调用 wait()waitpid() 的地方,修复问题。

至此,iowait 高和大量僵尸进程的问题被处理。iowait 高不一定代表 I/O 有性能瓶颈。当系统中只有 I/O 类型的进程在运行时,iowait 也会很高,但实际上,磁盘的读写远没有达到性能瓶颈的程度。

  • 碰到 iowait 升高时,需要先用 dstat、pidstat 等工具,确认是不是磁盘 I/O 的问题,然后再找是哪些进程导致了 I/O。
  • 等待 I/O 的进程一般是不可中断状态,所以用 ps 命令找到的 D 状态(即不可中断状态)的进程,多为可疑进程。
  • 但这个案例中,在 I/O 操作后,进程又变成了僵尸进程,所以不能用 strace 直接分析这个进程的系统调用。
  • 这种情况下,我们用了 perf 工具,来分析系统的 CPU 时钟事件,最终发现是直接 I/O 导致的问题。这时,再检查源码中对应位置的问题,就很轻松了。
  • 而僵尸进程的问题相对容易排查,使用 pstree 找出父进程后,去查看父进程的代码,检查 wait() / waitpid() 的调用,或是 SIGCHLD 信号处理函数的注册就行了。

7. 理解 Linux 中断

进程的不可中断状态是系统的一种保护机制,短时间的不可中断状态是正常的。除了 iowait,软中断 softirq 使得 CPU 使用率升高也是一种常见的性能问题。

中断其实是一种异步的事件处理机制,可以提高系统的并发处理能力。由于中断处理程序会打断其他进程的运行,所以,为了减少对正常进程运行调度的影响,中断处理程序就需要尽可能快地运行。如果有两个中断一前一后,前一个中断时间长,后一个中断时间短,可能会造成第二个中断处理丢失的情况。

为此,Linux 将中断处理过程分成了两个阶段,也就是上半部和下半部:

  • 上半部用来快速处理中断,它在中断禁止模式下运行,主要处理跟硬件紧密相关的或时间敏感的工作。
  • 下半部用来延迟处理上半部未完成的工作,通常以内核线程的方式运行。

可以通过 /proc/softirqs 查看软中断,通过 /proc/interrupts 查看硬中断。

软中断的下半部分是内核线程的形式运行的,因此可以通过 ps aux | grep softirq 查看它们的运行状况。

ps 的输出中,名字在中括号里的,一般都是内核线程,无法获取它们的命令行参数。

8. 软中断 CPU 使用率上升处理

案例需要用到三个新的工具:

  • sar 是一个系统活动报告工具,既可以实时查看系统的当前活动,又可以配置保存和报告历史统计数据。
  • hping3 是一个可以构造 TCP/IP 协议数据包的工具,可以对系统进行安全审计、防火墙测试等。
  • tcpdump 是一个常用的网络抓包工具,常用来分析各种网络问题。

操作流程如下:

  1. 首先在云服务器上运行一个 Nginx 使用 docker run -itd --name=nginx -p 80:80 nginx,如果下载速度慢可以在 /etc/docker 下设置换源。
  2. 启动后在本地使用 curl 查看是否可以访问,然后本地通过 hping 进行请求:hping3 -S -p 80 -i u100 xxx,其中 -S 表示 TCP 协议的 SYN 同步序列号,-i u100 表示每隔 100 ms 发送一个网络帧。
    • 这是通过 hping3 模拟 SYN FLOOD 攻击。
  3. 此时云服务器会有卡顿现象出现,通过 top 发现平均负载很低,CPU 使用率也很低,并且来自 ksoftirqd 的进程。
  4. 通过 watch -d cat /proc/softirqs 监控软负载,发现 TIMER、NET_RX、SCHED、RCU 都在不停变化,只有 NET_RX 变化最快,其他都是 Linux 必须的调度。
  5. 通过 sar -n DEV 1 查看系统的网络收发以及每秒的情况,,-n DEV 表示显示网络收发的报告。
    • 第一列:表示报告的时间。
    • 第二列:IFACE 表示网卡。
    • 第三、四列:rxpck/s 和 txpck/s 分别表示每秒接收、发送的网络帧数,也就是 PPS。
    • 第五、六列:rxkB/s 和 txkB/s 分别表示每秒接收、发送的千字节数,也就是 BPS。
1
2
3
4
5
6
$ sar -n DEV 1
15:03:46 IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s %ifutil
15:03:47 eth0 12607.00 6304.00 664.86 358.11 0.00 0.00 0.00 0.01
15:03:47 docker0 6302.00 12604.00 270.79 664.66 0.00 0.00 0.00 0.00
15:03:47 lo 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
15:03:47 veth9f6bbcd 6302.00 12604.00 356.95 664.66 0.00 0.00 0.00 0.05
  1. 在 eth0 中接收的 PPS 比较大,达到 12607,而接收的 BPS 却很小,只有 664 KB。
    • 664*1024/12607 = 54 字节,说明平均每个网络帧只有 54 字节,这显然是很小的网络帧,也就是我们通常所说的小包问题。
  2. 通过 tcpdump -i eth0 -n tcp port 80 可以抓包,-i eth0 只抓取 eth0 网卡,-n 不解析协议名和主机名,tcp port 80 表示只抓取 tcp 协议并且端口号为 80 的网络帧。
1
2
3
$ tcpdump -i eth0 -n tcp port 80
15:11:32.678966 IP 192.168.0.2.18238 > 192.168.0.30.80: Flags [S], seq 458303614, win 512, length 0
...
  1. Flags [S] 则表示这是一个 SYN 包,现在可以确定这是从哪儿来的 SYN FLOOD 攻击。

9. 分析 CPU 瓶颈

CPU 的性能指标那么多,在实际场景中应该观察什么信息,使用什么工具?

9.1 CPU 性能指标

首先应该想到的是 CPU 使用率,具体包括用户 CPU、系统 CPU、等待 I/O CPU、软中断和硬中断等。

  • 用户 CPU 使用率,包括用户态 CPU 使用率(user)和低优先级用户态 CPU 使用率(nice),表示 CPU 在用户态运行的时间百分比。用户 CPU 使用率高,通常说明有应用程序比较繁忙。
  • 系统 CPU 使用率,表示 CPU 在内核态运行的时间百分比(不包括中断)。系统 CPU 使用率高,说明内核比较繁忙。
  • 等待 I/O 的 CPU 使用率,通常也称为 iowait,表示等待 I/O 的时间百分比。iowait 高,通常说明系统与硬件设备的 I/O 交互时间比较长。
  • 软中断和硬中断的 CPU 使用率,分别表示内核调用软中断处理程序、硬中断处理程序的时间百分比。它们的使用率高,通常说明系统发生了大量的中断。
  • 除了上面这些,还有在虚拟化环境中会用到的窃取 CPU 使用率(steal)和客户 CPU 使用率(guest),分别表示被其他虚拟机占用的 CPU 时间百分比,和运行客户虚拟机的 CPU 时间百分比。

其次应该想到是平均负载,主要包括三个数值,分别指过去 1 分钟、过去 5 分钟和过去 15 分钟的平均负载。

  • 理想情况下,平均负载等于逻辑 CPU 个数,这表示每个 CPU 都恰好被充分利用。如果平均负载大于逻辑 CPU 个数,就表示负载比较重了。

接着是进程上下文切换,包括了无法获取资源而导致的自愿上下文切换和被系统强制调度导致的非自愿上下文切换。

  • 过多的上下文切换,会将原本运行进程的 CPU 时间,消耗在寄存器、内核栈以及虚拟内存等数据的保存和恢复上,缩短进程真正运行的时间,成为性能瓶颈。

还有一个指标是 CPU 缓存命中率,CPU 在访问内存的时候,免不了要等待内存的响应。为了协调这两者巨大的性能差距,CPU 缓存(通常是多级缓存)就出现了。

  • CPU 缓存的速度介于 CPU 和内存之间,缓存的是热点的内存数据。
  • 缓存按照大小不同分为 L1、L2、L3 等三级缓存,其中 L1 和 L2 常用在单核中, L3 则用在多核中。
  • 从 L1 到 L3,三级缓存的大小依次增大,相应的,性能依次降低(当然比内存还是好得多)。
  • 它们的命中率,衡量的是 CPU 缓存的复用情况,命中率越高,则表示性能越好。

9.2 案例汇总

  • 首先,平均负载的案例。
    • 我们先用 uptime, 查看了系统的平均负载;
    • 而在平均负载升高后,又用 mpstatpidstat ,分别观察了每个 CPU 和每个进程 CPU 的使用情况,进而找出了导致平均负载升高的进程,也就是我们的压测工具 stress。
  • 第二个,上下文切换的案例。
    • 我们先用 vmstat ,查看了系统的上下文切换次数和中断次数;
    • 然后通过 pidstat ,观察了进程的自愿上下文切换和非自愿上下文切换情况;
    • 最后通过 pidstat -t ,观察了线程的上下文切换情况,找出了上下文切换次数增多的根源,也就是我们的基准测试工具 sysbench。
  • 第三个,进程 CPU 使用率升高的案例。
    • 我们先用 top ,查看了系统和进程的 CPU 使用情况,发现 CPU 使用率升高的进程是 php-fpm;
    • 再用 perf top ,观察 php-fpm 的调用链,最终找出 CPU 升高的根源,也就是库函数 sqrt() 。
  • 第四个,系统的 CPU 使用率升高的案例。
    • 我们先用 top 观察到了系统 CPU 升高,但通过 toppidstat ,却找不出高 CPU 使用率的进程;
    • 于是,我们重新审视 top 的输出,又从 CPU 使用率不高但处于 Running 状态的进程入手,找出了可疑之处;
    • 最终通过 perf recordperf report ,发现原来是短时进程在捣鬼。
    • 另外,对于短时进程,还介绍了一个专门的工具 execsnoop ,它可以实时监控进程调用的外部命令。
  • 第五个,不可中断进程和僵尸进程的案例。
    • 我们先用 top 观察到了 iowait 升高的问题,并发现了大量的不可中断进程和僵尸进程;
    • 接着我们用 dstat 发现是这是由磁盘读导致的;
    • 于是又通过 pidstat 找出了相关的进程;
    • 但我们用 strace 查看进程系统调用却失败了;
    • 最终还是用 perf 分析进程调用链,才发现根源在于磁盘直接 I/O 。
  • 最后一个,软中断的案例。
    • 我们通过 top 观察到,系统的软中断 CPU 使用率升高;
    • 接着查看 /proc/softirqs, 找到了几种变化速率较快的软中断;
    • 然后通过 sar 命令,发现是网络小包的问题;
    • 最后再用 tcpdump ,找出网络帧的类型和来源,确定是一个 SYN FLOOD 攻击导致的。

9.3 从性能指标出发

9.4 从工具出发

9.5 工具的联动

10. CPU 性能优化

10.1 性能优化方法论

在进行性能优化时应该想想三个问题:

  1. 首先,既然要做性能优化,那要怎么判断它是不是有效呢?特别是优化后,到底能提升多少性能呢?
  2. 第二,性能问题通常不是独立的,如果有多个性能问题同时发生,你应该先优化哪一个呢?
  3. 第三,提升性能的方法并不是唯一的,当有多种方法可以选择时,你会选用哪一种呢?是不是总选那个最大程度提升性能的方法就行了呢?

比如我们发现是因为一个进程的直接 I/O ,导致了 iowait 高达 90%。那是不是用“直接 I/O 换成缓存 I/O”的方法,就可以立即优化了呢?

  1. 第一个问题,直接 I/O 换成缓存 I/O,可以把 iowait 从 90% 降到接近 0,性能提升很明显。
  2. 第二个问题,我们没有发现其他性能问题,直接 I/O 是唯一的性能瓶颈,所以不用挑选优化对象。
  3. 第三个问题,缓存 I/O 是我们目前用到的最简单的优化方法,而且这样优化并不会影响应用的功能。

10.2 性能优化评估

怎么评估性能优化的效果:

  1. 确定性能的量化指标。
  2. 测试优化前的性能指标。
  3. 测试优化后的性能指标。

不要局限在单一维度的指标上,你至少要从应用程序和系统资源这两个维度,分别选择不同的指标。比如,以 Web 应用为例:

  1. 应用程序的维度,我们可以用吞吐量请求延迟来评估应用程序的性能。
  2. 系统资源的维度,我们可以用 CPU 使用率来评估系统的 CPU 使用情况。

10.3 多个性能问题

80% 的问题都是由 20% 的代码导致的。

  • 第一,如果发现是系统资源达到了瓶颈,比如 CPU 使用率达到了 100%,那么首先优化的一定是系统资源使用问题。完成系统资源瓶颈的优化后,我们才要考虑其他问题。
  • 第二,针对不同类型的指标,首先去优化那些由瓶颈导致的,性能指标变化幅度最大的问题。比如产生瓶颈后,用户 CPU 使用率升高了 10%,而系统 CPU 使用率却升高了 50%,这个时候就应该首先优化系统 CPU 的使用。

10.4 多种优化方式

性能优化并非没有成本。性能优化通常会带来复杂度的提升,降低程序的可维护性,还可能在优化一个指标时,引发其他指标的异常。

综合多方面的因素。切记,不要想着“一步登天”,试图一次性解决所有问题;也不要只会“拿来主义”,把其他应用的优化方法原封不动拿来用,却不经过任何思考和分析。

10.5 CPU 优化

程序优化:

  • 编译器优化:很多编译器都会提供优化选项,适当开启它们,在编译阶段你就可以获得编译器的帮助,来提升性能。比如, gcc 就提供了优化选项 -O2,开启后会自动对应用程序的代码进行优化。
  • 算法优化:使用复杂度更低的算法,可以显著加快处理速度。比如,在数据比较大的情况下,可以用 O(nlogn) 的排序算法(如快排、归并排序等),代替 O(n^2) 的排序算法(如冒泡、插入排序等)。
  • 异步处理:使用异步处理,可以避免程序因为等待某个资源而一直阻塞,从而提升程序的并发处理能力。比如,把轮询替换为事件通知,就可以避免轮询耗费 CPU 的问题。
  • 多线程代替多进程:前面讲过,相对于进程的上下文切换,线程的上下文切换并不切换进程地址空间,因此可以降低上下文切换的成本。
  • 善用缓存:经常访问的数据或者计算过程中的步骤,可以放到内存中缓存起来,这样在下次用时就能直接从内存中获取,加快程序的处理速度。

系统优化:

  • CPU 绑定:把进程绑定到一个或者多个 CPU 上,可以提高 CPU 缓存的命中率,减少跨 CPU 调度带来的上下文切换问题。
  • CPU 独占:跟 CPU 绑定类似,进一步将 CPU 分组,并通过 CPU 亲和性机制为其分配进程。这样,这些 CPU 就由指定的进程独占,换句话说,不允许其他进程再来使用这些 CPU。
  • 优先级调整:使用 nice 调整进程的优先级,正值调低优先级,负值调高优先级。适当降低非核心应用的优先级,增高核心应用的优先级,可以确保核心应用得到优先处理。
  • 为进程设置资源限制:使用 Linux cgroups 来设置进程的 CPU 使用上限,可以防止由于某个应用自身的问题,而耗尽系统资源。
  • NUMA(Non-Uniform Memory Access)优化:支持 NUMA 的处理器会被划分为多个 node,每个 node 都有自己的本地内存空间。NUMA 优化,其实就是让 CPU 尽可能只访问本地内存。
  • 中断负载均衡:无论是软中断还是硬中断,它们的中断处理程序都可能会耗费大量的 CPU。开启 irqbalance 服务或者配置 smp_affinity,就可以把中断处理过程自动负载均衡到多个 CPU 上。