CPU Diagnosis

平均负载

我们经常会用 uptime、top 等工具查询 cpu 的平均负载(Load Average),那到底什么是平均负载呢?

通过 man uptime 可以看到平均负载的定义:System load averages is the average number of processes that are either in a runnable or uninterruptable state. A process in a runnable state is either using the CPU or waiting to use the CPU. A process in uninterruptable state is wait- ing for some I/O access, eg waiting for disk.

简单来说,即单位时间内系统处于可运行状态或不可中断状态的平均进程数。如平均负载为 2 时,系统有 2 个 CPU,意味着 CPU 刚好被完全占用。三个时间间隔的平均负载反映了趋势。所以平均负载高并不能反映问题出在哪里,可能是 CPU 使用过高,也可能是 IO 等待过长

  • 可运行状态(ps 命令中看到的 R 状态):

    • 正在使用 CPU

    • 等待使用 CPU

  • 不可中断状态(ps 命令中看到的 D 状态):等待某些硬件设备的 IO 响应。

经验:当平均负载高于 CPU 个数的 70% 的时候,就需要关注性能问题了。

平均负载与 CPU 使用率 两者并不一定是完全对应的,从定义可以看出,平均负载是指活跃的进程个数,活跃的进程还包括了等待 CPU 和等待 IO 的进程。

  • CPU 密集型:两个是一致的

  • IO 密集型:等待 IO 会使平均负载升高,但 CPU 使用率不一定高

  • 大量的进程调用会使平均负载升高,CPU 使用率也会升高

压力测试

# CPU 密集型, 模拟一个 CPU 使用 100%
stress -c 1 -t 10

# IO 密集型, 不停地执行 sync
stress -i 1 -t 600

# 大量进程
stress -c 8 -t 600

# 查看 uptime 命令输出的变化
watch -d uptime

# 每隔 5 秒输出一组所有 CPU 的使用率
mpstat -P ALL 5

# 查看哪个进程占用的 CPU
pidstat -u 5 1

上下文切换

每个任务运行前,CPU 都需要知道从哪里加载、从哪个位置开始运行,所以系统需要先设置好 CPU 寄存器和程序计数器(Program Counter, PC)。

寄存器是 CPU 内置的容量小、速度极快的内存;PC 用于存储正在执行的指令位置、或下一条指令位置。这两者是 CPU 在运行任何任务前必须依赖的环境,所以也叫 CPU 上下文。

CPU 上下文切换就是把前一个任务的上下文(寄存器和 PC)保存到内核中,加载新任务的上下文到寄存器和 PC。

根据任务和不同,可以分为进程上下文切换、线程上下文切换、中断上下文切换

系统调用

Linux 把进程的运行空间分为内核空间(最高权限,可以访问所有资源)和用户空间(必须通过系统调用陷入到内核中才能访问内存等硬件设备),分别对应 Ring0 和 Ring3。

系统调用时,CPU 寄存器中用户态的指令位置需要先保存起来,更新为内核态指令的位置;系统调用结束后,又恢复为用户态。所以一次系统调用会发生两次 CPU 上下文切换

系统调用不需要涉及虚拟内存等资源,也不会切换进程,所以系统调用通常称为特权模式切换,而不是上下文切换。

进程上下文切换

进程是由内核来管理和调度的,进程切换只能发生在内核态。所以进程切换不仅包括用户态资源(虚拟内存、栈、全局变量),还包括内核态资源(内核堆栈、寄存器)。

Linux 为每个 CPU 维护一个队列,将活跃进程(正在运行或正在等待 CPU)按照优先级和等待 CPU 的时长排序。进程调度的时机发生在:

  • 一个进程执行结束。

  • 分时操作系统中进程的时间片用完了

  • 进程通过 sleep 将自己主动挂起

  • 有更高优先级的进程

  • 硬件中断

线程上下文切换

线程是调度的基本单位,而进程是资源拥有的基本单位。内核中的任务调度对象都是线程,而进程只是给线程提供了虚拟内存、全局变量等资源。所以:

  • 进程只有一个线程时,进程就等于线程。

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

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

由此可见,若两个线程不属于同一进程,则上下文切换与进程切换一样;若属于同一进程,切换时只需要切换线程的私有数据。这也是多线程替代多进程的一个优势

中断上下文切换

为了快速响应硬件事件,中断会打断进程的正常执行,转而调度中断处理程序。中断不涉及进程的用户态,所以中断上下文切换不需要保存和恢复用户态资源。对于同一 CPU, 中断处理比进程有更高的优先级。

案例

# 1. 查看空闲状态时的状态
vmstat 1 1

# 2. 模拟8个线程
sysbench --threads=10 --max-time=300 threads run

# 3. 查看此时的上下文切换
vmstat 1

# 4. 查看是哪个进程导致的
pidstat -w -u 1

# 5. 查看线程上下文切换
pidstat -wt 1

# 6. 查看中断类型
watch -d cat /proc/interrupts

总结

上下文切换 1 万以内都算正常,若超过 1 万次、或出现量级增长,就可能出现了性能问题。不过这是经验数据,具体还要看配置等实际场景。

  • 自愿上下文切换变多,说明进程都在等待资源,可能发生了 IO 等其它问题。

  • 非自愿上下文切换变多,说明都在争抢 CPU,确实 CPU 出现了瓶颈。

  • 中断次数变多,需要查看 /proc/interrupts 具体分析。

使用率

Linux 将 CPU 运行划分成很短的时间片,通过定义节拍率(HZ)来触发时间中断,Jiffies 记录了开机以来的节拍数。

# 查看内核配置的节拍率。
  ~ grep 'CONFIG_HZ=' /boot/config-4.19.0-11-amd64
CONFIG_HZ=250 # 表示每秒触发 250 次时间中断

用户空间的节拍率 USER_HZ 固定为 100。

Linux 通过 /proc 虚拟文件系统,向用户提供了内核信息。/proc/stat 提供了 CPU 和任务统计信息,进程的的 CPU 使用情况可以查看 /proc/[pid]/stat。

# 后面的每一列表示不同场景下 CPU 的累积节拍数,单位为 USER_HZ(10ms)
  ~ cat /proc/stat | grep cpu
cpu  62730 26 81989 40792338 728 0 424 0 0 0
cpu0 31578 11 42637 20389529 667 0 237 0 0 0
cpu1 31152 14 39352 20402809 61 0 186 0 0 0

可通过 man proc 查看每一列的意义:

  • user:用户态。不包括 nice,包括 guest。

  • nice:低优先级用户态。

  • system:内核态。

  • idle:空闲。不包括 iowait。

  • iowait:等待 I/O。

  • irq:硬中断。

  • softirq:软中断。

  • steal:若当前系统运行在虚拟机中,CPU 被其它虚拟机占用的时间。

  • guest:虚拟化运行其它操作系统时,运行虚拟机的时间。

  • guest_nice:低优先级运行虚拟机的时间。

CPU 使用率为:

utility=1空间时间/CPU时间utility = 1 - 空间时间 / 总 CPU 时间

上面是开机以来的平均使用率,应该计算某段时间范围内的使用率,所以需要做个差值。需要注意:不同的性能分析工具做差值的时间范围不同,如 top 默认是 3s,ps 是进程的整个生命周期。

通常查看 CPU 使用率的工具有:ps、top、pidstat。可用 perf、GDB 来定位具体的函数。

系统的 CPU 使用率很高,不一定能找到对应高 CPU 使用率的进程。

  • 进程在不断的崩溃重启,如配置错误等等。

  • 应用调用其它二进制程序,这些程序运行时间较短。

若要检测上述问题,可通过:

  • perf record -g,perf report

  • execsnoop,专为短时进程设计的工具

不可中断状态

不可中断状态是一种保护机制,可以保证硬件的交互过程不被意外打断。所以,短时间的不可中断状态是很正常的,但一个进程长时间处于 D 状态,通常表示系统有 I/O 性能问题。若系统中出现大量 D 状态的进程,则要考虑下是不是出现了 I/O 性能问题。

但是 iowait 高不一定就是 I/O 有性能瓶颈,当系统中只有 I/O 密集型的进程运行时,iowait 也会很高,但是磁盘还可能远没到读写瓶颈。

可以通过 pidstat、dstat 等工具确认是否有磁盘 I/O 问题。

僵尸进程

大量僵尸进程会占用进程号,导致新进程不能创建。

若系统中出现大量 Z 状态的进程,则要考虑父进程是否回收自己进程的资源。回收的方式:

  1. 父进程通过 wait(), waitpid() 等待子进程结束,并回收资源。

  2. 父进程可以注册 SIGCHLD 信号处理函数异步回收。

  3. 父进程退出,init 进程也会回收。

软中断

softirq 也是 CPU 使用率升高的常见原因。

中断是系统用来响应硬件设备请求的一种机制,会打断进程的正常调度和执行,然后调用内核中的中断处理程序来响应设备的请求。中断是一种异步的事件处理机制,可以提高系统的并发处理能力

中断处理程序在响应中断时,会临时关闭中断,所以本次中断处理完成之前,其它中断都不能响应,即中断可能会丢失。所以中断处理程序的特点是尽可能快地运行。为了防止中断处理程序执行过长,Linux 将中断分成两个阶段:

  • 上半部:快速处理中断,在中断禁止模式下运行。直接处理硬件请求,常说的硬中断,特点是快速。会打断 CPU 正在执行的任务,立即执行中断处理程序。

  • 下半部:延迟处理上半部未完成的工作,通常以内核线程的方式运行。由内核触发,常说的软中断,特点是延迟执行。每个 CPU 都对应一个软中断内核线程,名字为 ksoftirq/CPU编号。

比如网卡接收到数据,通过硬件中断,内核启动响应的中断处理程序。上半部,网卡把数据读到内存,更新寄存器状态(表示数据已经就绪),再发送一个软中断信号,通知下半部程序做进一步处理;下半部:从内存中读取数据,按照网络协议栈对数据逐层梳理。

软中断不仅包含硬件中断处理程序的下半部,一些内核自定义事件也属于软中断,比如内核调度和 RCU 锁。

# 查看软中断运行情况
cat /proc/softirqs

# 查看硬中断运行情况
cat /proc/interrupts

# 查看软中断内核线程
ps aux | grep softirq

总结

CPU 性能指标

指标 -> 工具

工具 -> 指标

指标的关联性

CPU 性能优化

方法论

  • 怎么判断性能优化是否有效?优化后能提升多少性能?

  • 多个性能问题同时发生,应该先优化哪个?80% 的问题都由 20% 的代码导致,并不是所有的问题都值得优化。

  • 有多种优化方法时,选用哪个?

评估优化效果

  1. 确定量化指标。不要局限于单一维度,至少从应用程序(如吞吐量、延迟)和系统资源(如 CPU 使用率)两个维度。

  2. 测试优化前指标。

  3. 测试优化后指标。

应用程序优化

  • 编译器优化

  • 算法优化

  • 异步

  • 多线程替代多进程

  • 缓存

系统优化

  • CPU 绑定

  • CPU 独占

  • 优先级调整

  • 为进程设置资源限制

  • NUMA

  • 中断负载均衡

避免过早优化

Last updated