平均负载

平均负载是指单位时间内,系统处于可运行状态和不可中断状态的平均进程数,也就是平均活跃进程数

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

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

比如,当一个进程向磁盘读写数据时,为了保证数据的一致性,在得到磁盘回复前,它是不能被其他进程或者中断打断的,这个时候的进程就处于不可中断状态。如果此时的进程被打断了,就容易出现磁盘数据与进程数据不一致的问题。

所以,不可中断状态实际上是系统对进程和硬件设备的一种保护机制。

既然平均的是活跃进程数,那么最理想的,就是每个 CPU 上都刚好运行着一个进程,这样每个 CPU 都得到了充分利用。比如当平均负载为 2 时,意味着什么呢?

在只有 2 个 CPU 的系统上,意味着所有的 CPU 都刚好被完全占用。

在 4 个 CPU 的系统上,意味着 CPU 有 50% 的空闲。

而在只有 1 个 CPU 的系统中,则意味着有一半的进程竞争不到 CPU。

平均负载是指单位时间内,处于可运行状态和不可中断状态的进程数。所以,它不仅包括了正在使用 CPU 的进程,还包括等待 CPU 和等待 I/O 的进程。

而 CPU 使用率,是单位时间内 CPU 繁忙情况的统计,跟平均负载并不一定完全对应。比如:

CPU 密集型进程,使用大量 CPU 会导致平均负载升高,此时这两者是一致的;

I/O 密集型进程,等待 I/O 也会导致平均负载升高,但 CPU 使用率不一定很高;

大量等待 CPU 的进程调度也会导致平均负载升高,此时的 CPU 使用率也会比较高。

平均负载最理想的情况是等于 CPU 个数。所以在评判平均负载时,首先你要知道系统有几个 CPU,这可以通过 top 命令或者从文件 /proc/cpuinfo 中读取,比如:

1# 关于 grep 和 wc 的用法请查询它们的手册或者网络搜索
2$ grep 'model name' /proc/cpuinfo | wc -l
32

平均负载案例分析

stress 是一个 Linux 系统压力测试工具,这里我们用作异常进程模拟平均负载升高的场景。

1apt install stress sysstat

而 sysstat 包含了常用的 Linux 性能工具,用来监控和分析系统的性能。我们的案例会用到这个包的两个命令 mpstat 和 pidstat。

场景一:CPU 密集型进程

模拟一个 CPU 使用率 100% 的场景

1stress --cpu 1 --timeout 600

在第二个终端运行 uptime 查看平均负载的变化情况

1# -d 参数表示高亮显示变化的区域
2$ watch -d uptime
3...,  load average: 1.00, 0.75, 0.39
1# -P ALL 表示监控所有 CPU,后面数字 5 表示间隔 5 秒后输出一组数据
2$ mpstat -P ALL 5
3Linux 4.15.0 (ubuntu) 09/22/18 _x86_64_ (2 CPU)
413:30:06     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
513:30:11     all   50.05    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00   49.95
613:30:11       0    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00
713:30:11       1  100.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00

到底是哪个进程导致了 CPU 使用率为 100% 呢?你可以使用 pidstat 来查询

1# 间隔 5 秒后输出一组数据
2$ pidstat -u 5 1
313:37:07      UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
413:37:12        0      2962  100.00    0.00    0.00    0.00  100.00     1  stress

场景二:I/O 密集型进程

1# 首先还是运行 stress 命令,但这次模拟 I/O 压力,即不停地执行 sync:
2stress -i 1 --timeout 600
1# 显示所有 CPU 的指标,并在间隔 5 秒输出一组数据
2$ mpstat -P ALL 5 1
3Linux 4.15.0 (ubuntu)     09/22/18     _x86_64_    (2 CPU)
413:41:28     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
513:41:33     all    0.21    0.00   12.07   32.67    0.00    0.21    0.00    0.00    0.00   54.84
613:41:33       0    0.43    0.00   23.87   67.53    0.00    0.43    0.00    0.00    0.00    7.74
713:41:33       1    0.00    0.00    0.81    0.20    0.00    0.00    0.00    0.00    0.00   98.99

那么到底是哪个进程,导致 iowait 这么高呢?我们还是用 pidstat 来查询

1# 间隔 5 秒后输出一组数据,-u 表示 CPU 指标
2$ pidstat -u 5 1
3Linux 4.15.0 (ubuntu)     09/22/18     _x86_64_    (2 CPU)
413:42:08      UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
513:42:13        0       104    0.00    3.39    0.00    0.00    3.39     1  kworker/1:1H
613:42:13        0       109    0.00    0.40    0.00    0.00    0.40     0  kworker/0:1H
713:42:13        0      2997    2.00   35.53    0.00    3.99   37.52     1  stress
813:42:13        0      3057    0.00    0.40    0.00    0.00    0.40     0  pidstat

可以发现,还是 stress 进程导致的。

场景三:大量进程的场景

我们还是使用 stress,但这次模拟的是 8 个进程:

1stress -c 8 --timeout 600

由于系统只有 2 个 CPU,明显比 8 个进程要少得多,因而,系统的 CPU 处于严重过载状态,平均负载高达 7.97:

1$ uptime
2...,  load average: 7.97, 5.93, 3.02
 1# 间隔 5 秒后输出一组数据
 2$ pidstat -u 5 1
 314:23:25      UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
 414:23:30        0      3190   25.00    0.00    0.00   74.80   25.00     0  stress
 514:23:30        0      3191   25.00    0.00    0.00   75.20   25.00     0  stress
 614:23:30        0      3192   25.00    0.00    0.00   74.80   25.00     1  stress
 714:23:30        0      3193   25.00    0.00    0.00   75.00   25.00     1  stress
 814:23:30        0      3194   24.80    0.00    0.00   74.60   24.80     0  stress
 914:23:30        0      3195   24.80    0.00    0.00   75.00   24.80     0  stress
1014:23:30        0      3196   24.80    0.00    0.00   74.60   24.80     1  stress
1114:23:30        0      3197   24.80    0.00    0.00   74.80   24.80     1  stress
1214:23:30        0      3200    0.00    0.20    0.00    0.20    0.20     0  pidstat

可以看出,8 个进程在争抢2个CPU,每个进程等待CPU的时间(也就是代码块中的 %wait 列)高达 75%。这些超出 CPU 计算能力的进程,最终导致 CPU 过载。

小结

平均负载高有可能是 CPU 密集型进程导致的;

平均负载高并不一定代表 CPU 使用率高,还有可能是 I/O 更繁忙了;

当发现负载高的时候,你可以使用 mpstat、pidstat 等工具,辅助分析负载的来源。

cpu上下文

CPU 寄存器,是 CPU 内置的容量小、但速度极快的内存。

而程序计数器,则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。

它们都是 CPU 在运行任何任务前,必须的依赖环境,因此也被叫做 CPU 上下文

CPU 上下文切换,就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。

硬件通过触发信号,会导致中断处理程序的调用,也是一种常见的任务

根据任务的不同,CPU 的上下文切换就可以分为几个不同的场景,也就是进程上下文切换、线程上下文切换以及中断上下文切换。

进程上下文切换

Linux 按照特权等级,把进程的运行空间分为内核空间和用户空间,分别对应着下图中, CPU 特权等级的 Ring 0 和 Ring 3。

  • 内核空间(Ring 0)具有最高权限,可以直接访问所有资源;

  • 用户空间(Ring 3)只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问这些特权资源。

进程既可以在用户空间运行,又可以在内核空间中运行。进程在用户空间运行时,被称为进程的用户态,而陷入内核空间的时候,被称为进程的内核态。

从用户态到内核态的转变,需要通过系统调用来完成。比如,当我们查看文件内容时,就需要多次系统调用来完成:首先调用 open() 打开文件,然后调用 read() 读取文件内容,并调用 write() 将内容写到标准输出,最后再调用 close() 关闭文件。

那么,系统调用的过程有没有发生 CPU 上下文的切换呢?答案自然是肯定的。

CPU 寄存器里原来用户态的指令位置,需要先保存起来。接着,为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。最后才是跳转到内核态运行内核任务。

而系统调用结束后,CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。所以,一次系统调用的过程,其实是发生了两次 CPU 上下文切换。

进程上下文切换,是指从一个进程切换到另一个进程运行。

而系统调用过程中一直是同一个进程在运行。

系统调用过程通常称为特权模式切换,而不是上下文切换。但实际上,系统调用过程中,CPU 的上下文切换还是无法避免的。

线程上下文切换

线程是调度的基本单位,而进程则是资源拥有的基本单位

当进程只有一个线程时,可以认为进程就等于线程。

当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。

另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。

第一种, 前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。

第二种,前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。

到这里你应该也发现了,虽然同为上下文切换,但同进程内的线程切换,要比多进程间的切换消耗更少的资源,而这,也正是多线程代替多进程的一个优势。

中断上下文切换

中断处理会打断进程的正常调度和执行

跟进程上下文不同,中断上下文切换并不涉及到进程的用户态。所以,即便中断过程打断了一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文,其实只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。

对同一个 CPU 来说,中断处理比进程拥有更高的优先级,所以中断上下文切换并不会与进程上下文切换同时发生。同样道理,由于中断会打断正常进程的调度和执行,所以大部分中断处理程序都短小精悍,以便尽可能快的执行结束。

CPU 上下文切换,是保证 Linux 系统正常工作的核心功能之一,一般情况下不需要我们特别关注。

但过多的上下文切换,会把 CPU 时间消耗在寄存器、内核栈以及虚拟内存等数据的保存和恢复上,从而缩短进程真正运行的时间,导致系统的整体性能大幅下降。

怎么查看系统的上下文切换情况

1# 每隔 5 秒输出 1 组数据
2$ vmstat 5
3procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
4 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
5 0  0      0 7005360  91564 818900    0    0     0     0   25   33  0  0 100  0  0
  • cs(context switch)是每秒上下文切换的次数。
  • in(interrupt)则是每秒中断的次数。
  • r(Running or Runnable)是就绪队列的长度,也就是正在运行和等待 CPU 的进程数。
  • b(Blocked)则是处于不可中断睡眠状态的进程数。

vmstat 只给出了系统总体的上下文切换情况,要想查看每个进程的详细情况,就需要使用我们前面提到过的 pidstat 了。给它加上 -w 选项,你就可以查看每个进程上下文切换的情况了。

1# 每隔 5 秒输出 1 组数据
2$ pidstat -w 5
3Linux 4.15.0 (ubuntu)  09/23/18  _x86_64_  (2 CPU)
4 
508:18:26      UID       PID   cswch/s nvcswch/s  Command
608:18:31        0         1      0.20      0.00  systemd
708:18:31        0         8      5.40      0.00  rcu_sched

一个是 cswch ,表示每秒自愿上下文切换(voluntary context switches)的次数,

另一个则是 nvcswch ,表示每秒非自愿上下文切换(non voluntary context switches)的次数。

  • 所谓自愿上下文切换,是指进程无法获取所需资源,导致的上下文切换。比如说, I/O、内存等系统资源不足时,就会发生自愿上下文切换。

  • 而非自愿上下文切换,则是指进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。比如说,大量进程都在争抢 CPU 时,就容易发生非自愿上下文切换。

自愿上下文切换变多了,说明进程都在等待资源,有可能发生了 I/O 等其他问题;

非自愿上下文切换变多了,说明进程都在被强制调度,也就是都在争抢 CPU,说明 CPU 的确成了瓶颈;

中断次数变多了,说明 CPU 被中断处理程序占用,还需要通过查看 /proc/interrupts 文件来分析具体的中断类型。

查看线上上下文切换,加-t

 1# 每隔 1 秒输出一组数据(需要 Ctrl+C 才结束)
 2# -wt 参数表示输出线程的上下文切换指标
 3$ pidstat -wt 1
 408:14:05      UID      TGID       TID   cswch/s nvcswch/s  Command
 5...
 608:14:05        0     10551         -      6.00      0.00  sysbench
 708:14:05        0         -     10551      6.00      0.00  |__sysbench
 808:14:05        0         -     10552  18911.00 103740.00  |__sysbench
 908:14:05        0         -     10553  18915.00 100955.00  |__sysbench
1008:14:05        0         -     10554  18827.00 103954.00  |__sysbench
11...
1# -d 参数表示高亮显示变化的区域
2$ watch -d cat /proc/interrupts
3           CPU0       CPU1
4...
5RES:    2450431    5279697   Rescheduling interrupts
6...

观察一段时间,你可以发现,变化速度最快的是重调度中断(RES),这个中断类型表示,唤醒空闲状态的 CPU 来调度新的任务运行。这是多处理器系统(SMP)中,调度器用来分散任务到不同 CPU 的机制,通常也被称为处理器间中断(Inter-Processor Interrupts,IPI)。

所以,这里的中断升高还是因为过多任务的调度问题,跟前面上下文切换次数的分析结果是一致的。

CPU 使用率

man proc 文档里每一列的涵义,它们都是 CPU 使用率相关的重要指标

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

pidstat

用户态 CPU 使用率 (%usr);

内核态 CPU 使用率(%system);

运行虚拟机 CPU 使用率(%guest);

等待 CPU 使用率(%wait);

以及总的 CPU 使用率(%CPU)。

1# 每隔 1 秒输出一组数据,共输出 5 组
2$ pidstat 1 5
315:56:02      UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
415:56:03        0     15006    0.00    0.99    0.00    0.00    0.99     1  dockerd
5 
6...
7 
8Average:      UID       PID    %usr %system  %guest   %wait    %CPU   CPU  Command
9Average:        0     15006    0.00    0.99    0.00    0.00    0.99     -  dockerd

CPU 使用率过高怎么办?

推荐 perf

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

使用 perf 分析 CPU 性能问题

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

用户 CPU 和 Nice CPU 高,说明用户态进程占用了较多的 CPU,所以应该着重排查进程的性能问题。

系统 CPU 高,说明内核态占用了较多的 CPU,所以应该着重排查内核线程或者系统调用的性能问题。

I/O 等待 CPU 高,说明等待 I/O 的时间比较长,所以应该着重排查系统存储是不是出现了 I/O 问题。

软中断和硬中断高,说明软中断或硬中断的处理程序占用了较多的 CPU,所以应该着重排查内核中的中断服务程序。

碰到 CPU 使用率升高的问题,你可以借助 top、pidstat 等工具,确认引发 CPU 性能问题的来源;再使用 perf 等工具,排查出引起性能问题的具体函数。

1#并发请求数改成 5,同时把请求时长设置为 10 分钟(-t 600)
2ab -c 5 -t 600 http://192.168.0.10:10000/
3# 并发 100 个请求测试 Nginx 性能,总共测试 1000 个请求
4$ ab -c 100 -n 1000 http://192.168.0.10:10000/

碰到常规问题无法解释的 CPU 使用率情况时,首先要想到有可能是短时应用导致的问题,比如有可能是下面这两种情况。

第一,应用里直接调用了其他二进制程序,这些程序通常运行时间比较短,通过 top 等工具也不容易发现。

第二,应用本身在不停地崩溃重启,而启动过程的资源初始化,很可能会占用相当多的 CPU。

对于这类进程,我们可以用 pstree 或者 execsnoop 找到它们的父进程,再从父进程所在的应用入手,排查问题的根源。

execsnoop 就是一个专为短时进程设计的工具。它通过 ftrace 实时监控进程的 exec() 行为,并输出短时进程的基本信息,包括进程 PID、父进程 PID、命令行参数以及执行的结果。