平均负载
平均负载是指单位时间内,系统处于可运行状态和不可中断状态的平均进程数,也就是平均活跃进程数
所谓可运行状态的进程,是指正在使用 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、命令行参数以及执行的结果。