Kyle's Notebook

Linux CPU 原理与分析

Word count: 8.6kReading time: 35 min
2021/04/10

Linux CPU 原理与分析

  • CPU 性能指标
    • CPU 使用率
      • 用户 CPU
      • 内核 CPU
      • IOWAIT
      • 软中断
      • 硬中断
      • 窃取 CPU
      • 客户 CPU
    • 上下文切换
      • 自愿上下文切换
      • 非自愿上下文切换
    • 平均负载
    • CPU 缓存命中率

平均负载(Load Average)

使用 topuptime 命令,都会有以下输出(其中 load average 表示过去 1、5、15 分钟的平均负载):

1
02:34:03 up 2 days, 20:14,  1 user,  load average: 0.63, 0.83, 0.88

平均负载可理解为单位时间内,系统处于 可运行状态不可中断状态 的 平均进程数,即 平均活跃进程数,与 CPU 使用率没有直接关系。

  • 可运行状态:正在使用或等待 CPU 的进程,即使用 ps 命令可见状态为 R 的进程(Running/Runnable)。

  • 不可中断状态:处于内核态关键流程中、不可打断的进程,比如等待 I/O,使用 ps 命令可见状态为 D 的进程(Disk Sleep)。

如何监控?

  • 平均负载的值可表示系统的繁忙程度,最理想的情况是等于 CPU 核心数(逻辑 CPU,grep 'model name' /proc/cpuinfo | wc -l)。当平均负载值小于 CPU 核心数表示有空闲,大于 CPU 核心数表示有进程竞争不到 CPU。

  • 如果 1、5、15 分钟的值基本相同或相差不大,表明系统负载平稳。

  • 如果 1 分钟的值远小于 15 分钟的值,表明系统最近 1 分钟负载在减少,而过去 15 分钟内却有很大的负载。

  • 如果 1 分钟的值远大于 15 分钟的值,就说明最近 1 分钟的负载在增加,需要持续观察这种情况是临时性还是持续性。当 1 分钟的平均负载接近或超过了 CPU 核心数,意味着系统正在过载,应该分析调查原因并做出优化。

经验上平均负载超过 CPU 核心数的 70%~80% 需要分析排查负载高的原因,根据更多历史数据判断负载的变化趋势。

压力测试

安装监控及测试工具:

1
2
3
4
apt install -y stress sysstat
# 分析工具由 sysstat 提供:
# mpstat:多核 CPU 性能分析工具,实时查看每个 CPU 的性能指标,以及所有 CPU 的平均指标。
# pidstat:进程性能分析工具,实时查看进程的 CPU、内存、I/O 以及上下文切换等性能指标。

模拟 CPU 使用率 100% 场景:

1
stress --cpu 1 --timeout 600

观察平均负载变化:

1
2
watch -d uptime    # -d 参数表示高亮显示变化的区域
# ..., load average: 1.00, 0.75, 0.39

观察 CPU 使用率变化:

1
2
3
4
5
6
7
8
mpstat -P ALL 5    # -P ALL 表示监控所有 CPU,其中主要参考 CPU、iowait 等。
# 可见 %user 100%。

# Linux 4.15.0 (ubuntu) 09/22/18 _x86_64_ (2 CPU)
# 13:30:06 CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
# 13:30:11 all 50.05 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 49.95
# 13:30:11 0 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 100.00
# 13:30:11 1 100.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00

找出导致 CPU 使用率 100% 的进程:

1
2
3
pidstat -u 5 1
# 13:37:07 UID PID %usr %system %guest %wait %CPU CPU Command
# 13:37:12 0 2962 100.00 0.00 0.00 0.00 100.00 1 stress

除此之外,还可以模拟 I/O 压力场景:

1
2
stress -i 1 --timeout 600
# 此时 mpstat 应参考 iowait、%sys

或模拟大量进程的场景:

1
stress -c 8 --timeout 600

使用率(Usage)

CPU 使用率即除了空闲时间外的其他时间占总 CPU 时间的百分比(== 1 - (空闲时间/总 CPU 时间))。

是单位时间内对 CPU 繁忙程度的统计,与平均负载不一定完全对应:

  • 对于 CPU 密集型进程,使用大量 CPU 会导致平均负载升高,此时两者一致。

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

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

节拍率

CPU 划分时间片,通过调度器轮流分配给多个进程使用,在宏观上可见进程同时运行。

其通过中断(Interrupt)维护时间片,按预定义的节拍率(内核选项 HZ,每秒触发中断数)触发中断,并使用全局变量 Jiffies 记录自启动起的节拍数。

1
2
grep 'CONFIG_HZ=' /boot/config-$(uname -r)
# CONFIG_HZ=250

对于用户态进程,则参考用户态节拍率 USER_HZ(一般固定为 100)。

统计信息

通过 /proc 虚拟文件系统可以查看用户空间系统内部状态的信息,比如 CPU:

1
2
3
4
5
6
cat /proc/stat | grep ^cpu
# cat /proc/[pid]/stat 则取特定进程的数据。

# cpu 280580 7407 286084 172900810 83602 0 583 0 0 0
# cpu0 144745 4181 176701 86423902 52076 0 301 0 0 0
# cpu1 135834 3226 109383 86476907 31525 0 282 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 取进程整个生命周期)。使用 ps 命令查看整体情况(关注 %CPU 找到 CPU 占用高的进程):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
top
# top - 11:58:59 up 9 days, 22:47, 1 user, load average: 0.03, 0.02, 0.00
# Tasks: 123 total, 1 running, 72 sleeping, 0 stopped, 0 zombie
# %Cpu(s): 0.3 us, 0.3 sy, 0.0 ni, 99.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
# KiB Mem : 8169348 total, 5606884 free, 334640 used, 2227824 buff/cache
# KiB Swap: 0 total, 0 free, 0 used. 7497908 avail Mem
#
# PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
# 1 root 20 0 78088 9288 6696 S 0.0 0.1 0:16.83 systemd
# 2 root 20 0 0 0 0 S 0.0 0.0 0:00.05 kthreadd
# 4 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker/0:0H
# ...

# 在 Task 一行可见处于各种状态的进程数量统计。
# 在 %Cpu 一行可见总体情况,内核态、用户态、空闲百分比。
# 默认为所有 CPU 平均值,输入 1 可切换为每个 CPU 使用率。
# 在每个进程的 %CPU 列,包括该用户态和内核态 CPU 使用率总和。

使用 pidstat 命令查看进程的具体情况:

1
2
3
4
5
6
7
8
9
10
pidstat 1 5
# 15:56:02 UID PID %usr %system %guest %wait %CPU CPU Command
# 15:56:03 0 15006 0.00 0.99 0.00 0.00 0.99 1 dockerd
#
# ...
# 平均值:
# Average: UID PID %usr %system %guest %wait %CPU CPU Command
# Average: 0 15006 0.00 0.99 0.00 0.00 0.99 - dockerd

pidstat -p 24344 # 查看某个进程的具体情况。

其每列的含义:

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

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

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

  • 等待 CPU 使用率(%wait)

  • 总 CPU 使用率(%CPU)。

问题排查

当 CPU 使用率过高,可以使用 perf 分析问题成因。

找出占用 CPU 时钟最多的函数或指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
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
# ...

perf top -g -p 21515
# 开启调用关系的采样,方便根据调用链来分析性能问题,再查看具体的进程。

其每列的含义:

  • Overhead 是该符号的性能事件在所有采样中的比例,百分比表示。

  • Shared 是该函数或指令所在的动态共享对象(DSO),如内核、进程名、动态链接库名、内核模块名等。

  • Object 是动态共享对象的类型。比如 [.] 表示用户空间的可执行程序、或者动态链接库,而 [k] 则表示内核空间。

  • Symbol 是符号名(函数名),当函数名未知时,用十六进制的地址来表示。

或实时展示系统性能信息:

1
2
3
4
5
perf record -g
# [ perf record: Woken up 1 times to write data ]
# [ perf record: Captured and wrote 0.452 MB perf.data (6093 samples) ]

perf report -g # 输出报告

在实践中:

  • 用户 CPU(%user)和 Nice CPU(%nice)高,说明用户态进程占用了较多 CPU,着重排查进程性能问题。

  • 系统 CPU(%system)高,说明内核态占用了较多 CPU,着重排查内核线程或系统调用的性能问题。

  • I/O 等待 CPU(%iowait)高,说明等待 I/O 的时间比较长,着排查系统存储是否出现 I/O 问题。

  • 软中断(%softirq)和硬中断(%irq)高,说明软中断或硬中断处理程序占用较多 CPU,着重排查内核中断服务程序。

  • 当应用直接调用外部的二进制程序,或其本身在不断重启(可能是同名进程 pid 一直在改变),不会表现为某个进程 CPU 占用过高。

    • 此时也许可以从其父进程中找到一些线索:pstree | grep xxx

    • 结合 perf record -g 采集一段时间的数据,可见某同名进程占用的 CPU 时钟,确认是否导致 CPU 使用率高。

    • 也可以使用 execsnoop 监控短时进程,通过 ftrace 实时监控进程的 exec() 行为输出进程 PID、父进程 PID、命令行参数以及执行的结果。

上下文(Context)

上下文切换发生在进程竞争 CPU 时,即使此时进程没有运行,也会占用 CPU 负载。

CPU 时间片轮转分配给多任务,每个任务从何处加载、运行,依赖于事先保存在系统内核的上下文。在切换时新的任务上下文被加载到这些寄存器和程序计数器,再跳转到程序计数器所指的新位置来运行,因此上下文切换主要是更新 CPU 寄存器的值。

根据不同的场景,CPU 上下文切换分为进程上下文切换、线程上下文切换和中断上下文切换。

进程上下文切换

进程是资源的基本单位。按照特权等级,进程的运行空间分为 内核空间用户空间,对应着图中 Ring 0 和 Ring 3。进程在内核空间运行时称为 内核态,在用户空间运行时称为 用户态

1
2
3
4
5
+----------Ring3----- 应用
| +--------Ring2----- 设备驱动
| | +------Ring1----- 设备驱动
| | | +----Ring0----- 内核
| | | |

从用户态到内核态的转变,需要通过 系统调用 完成。系统调用是为 特权模式切换 而不是上下文切换,在一次系统调用中会发生两次 CPU 上下文切换:先保存起来 CPU 寄存器里原来用户态的指令位置。CPU 寄存器更新为内核态指令的新位置,最后跳转到内核态运行内核任务。而系统调用结束后,CPU 寄存器恢复原来保存的用户态,然后再切换到用户空间继续运行进程。

需要注意的是系统调用过程中一直是同一个进程在运行,不会涉及到虚拟内存等进程用户态的资源,也不会切换进程(区别于进程的上下文切换)。

进程上下文切换与系统调用的区别

进程由内核来管理调度,进程切换只能发生在内核态。所以进程的上下文不仅包括虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。

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

切换成本

每次上下文切换都需要几十纳秒到数微秒的 CPU 时间(Tsuna’s blog: How long does it take to make a context switch?)。特别是在进程上下文切换次数较多的情况下,容易导致 CPU 将大量时间耗费在寄存器、内核栈以及虚拟内存等资源的保存和恢复上,大大缩短了真正运行进程的时间,导致平均负载升高。

Linux 通过 TLB(Translation Lookaside Buffer)管理虚拟内存到物理内存的映射关系。当虚拟内存更新后 TLB 也需要刷新,进程的内存访问随之变慢。在多处理器系统上缓存被多个处理器共享,刷新缓存还会影响共享缓存的其他处理器的进程。

切换时机

在进程调度时才需要切换上下文。Linux 为每个 CPU 都维护一个就绪队列,将活跃进程(Running or Runnable)按照优先级和等待 CPU 的时间排序,再选择优先级最高和等待 CPU 时间最长的进程来运行,比如:

  • 进程执行完终止了,使用的 CPU 释放,此时再从就绪队列取出新进程运行。

  • 为了保证进程都被公平调度,CPU 划分一段段时间片,轮流分配给各个进程。某个进程的时间片耗尽就会被系统挂起,切换到其它进程运行。

  • 进程在系统资源不足(比如内存)时,要等到资源满足后才可以运行,此时进程会被挂起,由系统调度其他进程运行。

  • 进程通过 sleep 等函数将自己主动挂起,触发重新调度。

  • 有时为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行。

线程上下文切换

线程是调度的基本单位:

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

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

  • 线程有自己的私有数据,比如栈和寄存器等(在上下文切换时需要保存)。

线程上下文切换时,如果两个线程属于不同进程,切换过程就与进程上下文切换一样;如果属于同一进程,虚拟内存等资源保持不动,只切换线程的私有数据(寄存器等)。因此多线程比多进程开销更小。

中断上下文切换

中断用于快速响应硬件事件,会打断进程的正常调度和执行,转而调用中断处理程序响应设备事件。而在打断其他进程时需要将进程状态保存,在中断结束后进程仍然可以从原状态恢复运行。

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

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

中断上下文切换需要消耗 CPU,切换次数过多会严重降低系统的整体性能。

压力测试

安装 sysbenchsysstat

1
apt install -y sysbench sysstat

模拟多线程调度的场景:

1
sysbench --threads=10 --max-time=300 threads run    

如何监控?

使用 vmstat 命令查看总体情况:

1
2
3
4
5
6
7
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 6487428 118240 1292772 0 0 0 0 9019 1398830 16 84 0 0 0
# 8 0 0 6487428 118240 1292772 0 0 0 0 10191 1392312 16 84 0 0 0

# 可见上下文切换次数 cs 139 万次,系统中断次数 in 1 万次,绪队列长度 r 为 8。

其每列的含义:

  • cs(context switch)是每秒上下文切换的次数。

  • in(interrupt)则是每秒中断的次数。

  • r(Running or Runnable)是就绪队列的长度,即正在运行和等待 CPU 的进程数,如果超出 CPU 核心数,表示竞争激烈。

  • b(Blocked)是处于不可中断睡眠状态的进程数。

  • us(user)和 sy(system)是用户态和内核态 CPU 使用率,表示 CPU 被内核或应用程序占用的情况。

使用 pidstat 具体到每个进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pidstat -w -u -t 1

# 08:06:33 UID PID %usr %system %guest %wait %CPU CPU Command
# 08:06:34 0 10488 30.00 100.00 0.00 0.00 100.00 0 sysbench
# 08:06:34 0 26326 0.00 1.00 0.00 0.00 1.00 0 kworker/u4:2

# 08:06:33 UID PID cswch/s nvcswch/s Command
# ...
# 08:14:05 0 10551 - 6.00 0.00 sysbench
# 08:14:05 0 - 10551 6.00 0.00 |__sysbench
# 08:14:05 0 - 10552 18911.00 103740.00 |__sysbench
# 08:14:05 0 - 10553 18915.00 100955.00 |__sysbench
# 08:14:05 0 - 10554 18827.00 103954.00 |__sysbench
# ...
  • cswch 表示每秒自愿上下文切换(voluntary context switches)次数,指进程无法获取所需资源,导致的上下文切换。
  • nvcswch 表示每秒非自愿上下文切换(non voluntary context switches)的次数,指进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。

可以在 /proc/interrupts 查看中断的详细信息:

1
2
3
4
5
watch -d cat /proc/interrupts
# CPU0 CPU1
# ...
# RES: 2450431 5279697 Rescheduling interrupts
# ...

RES 表示重调度中断,即唤醒空闲状态的 CPU 来调度新的任务运行。多处理器系统(SMP)中,调度器用于分散任务到不同 CPU 的机制,也被称为 处理器间中断(Inter-Processor Interrupts,IPI)

问题排查

上下文切换频率取决于系统 CPU 性能。

  • 如果系统上下文切换次数稳定,从数百到一万以内都正常。但当超过一万次或者切换次数出现数量级的增长,就可能已经出现性能问题。

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

  • 非自愿上下文切换变多,说明进程都在被强制调度、争抢 CPU,此时 CPU 称为瓶颈;

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

异常进程

通常指 不可中断进程僵尸进程

使用 top 命令可见 S 列(Status)表示进程的状态:

  • R(Running 或 Runnable)表示进程在 CPU 的就绪队列中,正在运行或者正在等待运行。

  • D(Disk Sleep)表示不可中断状态睡眠(Uninterruptible Sleep),表示进程正在跟硬件交互,不允许被其他进程或中断打断。

  • Z(Zombie)表示僵尸进程,即实际上已经结束了,但是父进程还没有回收其资源(比如进程的描述符、PID 等)的进程。

  • S(Interruptible Sleep)是可中断状态睡眠,表示进程因为等待某个事件而被系统挂起。当进程等待的事件发生时,它会被唤醒并进入 R 状态。

  • I(Idle)是空闲状态,用在不可中断睡眠的内核线程上。硬件交互导致不可中断进程用 D 表示,但某些内核线程可能并没有任何负载,则处于 I 状态。D 状态进程会导致平均负载升高, I 状态的进程不会。

  • T 或者 t(Stopped 或 Traced)表示进程处于暂停或者跟踪状态。向一个进程发送 SIGSTOP 信号,就会因响应信号变成暂停状态(Stopped);再向它发送 SIGCONT 信号,则又会恢复运行。或使用调试器(GDB 等)时,进程就会变成跟踪状态。

  • X(Dead)表示进程已经消亡,不会在 top 或者 ps 命令中看到它。

1
2
3
4
5
6
7
8
9
10
11
12
top
# PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
# 28961 root 20 0 43816 3148 4040 R 3.2 0.0 0:00.01 top
# 620 root 20 0 37280 33676 908 D 0.3 0.4 0:00.01 app
# 1 root 20 0 160072 9416 6752 S 0.0 0.1 0:37.64 systemd
# 1896 root 20 0 0 0 0 Z 0.0 0.0 0:00.00 devapp
# 2 root 20 0 0 0 0 S 0.0 0.0 0:00.10 kthreadd
# 4 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 kworker/0:0H
# 6 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 mm_percpu_wq
# 7 root 20 0 0 0 0 S 0.0 0.0 0:06.37 ksoftirqd/0

# Ss+ 中的 s 表示该进程是一个会话的领导进程,+ 表示前台进程组。

进程组:表示一组相互关联的进程,比如每个子进程都是父进程所在组的成员。比如以 SSH 登录客户端,就会打开一个控制中断(TTY),控制终端对应一个会话(指共享同一个控制终端的一个或多个进程组)。在终端中运行命令以及其子进程,就构成了进程组,其中在后台运行的命令,构成后台进程组;在前台运行的命令,构成前台进程组。

不可中断进程(D):当 iowait 升高时,进程很可能因为得不到硬件响应,长时间处于不可中断状态。短时间的不可中断状态一般可以忽略,但如果系统或硬件发生故障,进程可能在不可中断状态保持很久,导致在系统中大量出现。

iowait 分析

比如使用 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 分钟、5 分钟和 15 分钟内的平均负载在依次减小,说明平均负载正在升高;而 1 分钟内的平均负载已经达到系统的 CPU 个数,说明系统很可能已经有了性能瓶颈。

  • Tasks 中可见有 1 个正在运行的进程,但僵尸进程比较多且还在不停增加,说明有子进程在退出时没被清理。

  • 两个 CPU 的使用率:用户 CPU 和系统 CPU 都不高,但 iowait 高达 60.5% 和 94.6%。

  • 每个进程的情况: CPU 使用率最高的进程只有 0.3%,但有两个进程处于 D 状态,其可能在等待 I/O。

可见 iowait 太高导致平均负载升高、达到系统 CPU 个数;僵尸进程在不断增多,有程序不能正确清理子进程的资源。使用 dstat 命令查看 CPU 和 I/O 使用情况:

1
2
3
4
5
6
7
8
9
10
dstat 1 10
# You did not select any stats, using -cdngy by default.
# --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

可见每当 iowait 升高(wai)时,磁盘的读请求(read)都很大,说明 iowait 的升高与读磁盘有关。

对于在 top 的输出中状态为 D 的进程,使用 pidstat 命令查看 I/O 具体情况:

1
2
3
4
5
6
7
8
9
10
pidstat -d 1 20
# ...
# 06:48:46 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
# 06:48:47 0 4615 0.00 0.00 0.00 1 kworker/u4:1
# 06:48:47 0 6080 32768.00 0.00 0.00 170 app
# 06:48:47 0 6081 32768.00 0.00 0.00 184 app
#
# 06:48:47 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
# 06:48:48 0 6080 0.00 0.00 0.00 110 app
# ...

其中 kB_rd 表示每秒读的 KB 数, kB_wr 表示每秒写的 KB 数,iodelay 表示 I/O 的延迟(时钟周期)。可见 app 进程在进行磁盘读,32 MB/s,重点是找到 app 进程的系统调用。

使用 strace -p 跟踪进程系统调用,前提是该进程未退出,不能是僵尸进程。因此更建议使用基于事件记录的动态追踪工具 perf

1
2
3
4
perf record -g
perf report

# 重点观察 do_syscall 的操作。

iowait 高不一定代表 I/O 有性能瓶颈。

当 iowait 升高时,进程很可能因为得不到硬件的响应,而长时间处于不可中断状态。

但当系统中只有 I/O 类型的进程在运行时 iowait 也会很高,实际上磁盘的读写远没有达到性能瓶颈的程度。

因此还需要结合 dstat、pidstat 等工具确认是否磁盘 I/O 的问题,再找出导致 I/O 的进程。

处理僵尸进程

僵尸进程(Zombie):正常情况下一个进程创建子进程后,应通过系统调用 wait() 或者 waitpid() 等待子进程结束并回收资源;子进程在结束时会向父进程发送 SIGCHLD 信号,父进程还可以注册 该信号的处理函数异步回收资源。如果父进程没回收资源或是子进程执行太快、父进程未来得及处理子进程状态就已经提前退出,子进程就会变成僵尸进程。此时 mm_struct、files 等都已释放,但需要保留 task_struct(即 PCB,程序控制块),通过父进程处理。通常僵尸进程持续的时间都比较短,在父进程回收其资源后就会消亡,或者在父进程退出后由 init 进程回收后也会消亡。

一旦父进程没有处理子进程的终止,还一直保持运行状态,子进程就会一直处于僵尸状态:

  • 大量的僵尸进程会用尽 PID(进程号),导致不能创建新进程。

  • 另一方面是 task_struct会占用大量内存资源。

处理僵尸进程需要先找出其父进程,使用 pstree 命令:

1
2
3
4
5
6
7
pstree -aps 3084
# systemd,1
# └─dockerd,15006 -H fd://
# └─docker-containe,15024 --config /var/run/docker/containerd/containerd.toml
# └─docker-containe,3991 -namespace moby -workdir...
# └─app,4009
# └─(app,3084)

可见 3084 进程的父进程是 4009,此时应查看 4009 进程的应用代码,子进程结束的处理是否正确:是否调用 wait()waitpid()、有没有注册 SIGCHID 信号处理函数。

软中断

中断(Interrupt)是系统响应硬件设备请求的异步事件处理机制,会打断进程的正常调度和执行,再调用内核中的中断处理程序来响应设备的请求。为了减少对正常进程运行调度的影响,中断处理程序就需要尽可能快地运行。其分为两个阶段:

  • 硬中断:处理硬件请求,特点是快速执行,它在中断禁止模式下运行,主要处理与硬件紧密相关或时间敏感的工作( /proc/interrupts)。

  • 软中断:由内核触发,特点是延迟执行。延迟处理上半部未完成的工作,通常以内核线程的方式运行(/proc/softirqs,包括 10 个类型 )。除此之外,软中断还包括一些内核自定义的事件,比如内核调度和 RCU 锁(Read-Copy Update)等。

比如网卡接收到数据包通过硬件中断的方式通知内核新数据就绪。

硬中断:把网卡的数据读到内存中,更新硬件寄存器的状态,发送软中断信号。

软中断:从内存中找到网络数据,再按照网络协议栈对数据进行逐层解析和处理,送给应用程序。

软中断以内核线程运行,每个 CPU 对应一个软中断内核线程,即为 ksoftirqd/CPU 编号,使用 ps 命令:

1
2
3
4
5
6
ps aux | grep softirq
# root 7 0.0 0.0 0 0 ? S Oct10 0:01 [ksoftirqd/0]
# root 16 0.0 0.0 0 0 ? S Oct10 0:01 [ksoftirqd/1]

# 线程名字外有中括号,表示 ps 无法获取命令行参数(cmline)。
# 一般来说在 ps 的输出中,名字括在中括号里的都是内核线程。

问题分析

软中断(softirq)CPU 使用率升高是最常见的一种性能问题,尤其是网络收发类型的软中断。

安装 hping3、tcpdump 网络协议包工具。

1
apt install -y hping3 tcpdump

模拟 SYN FLOOD 攻击:

1
hping3 -S -p 80 -i u100 192.168.0.30

此时系统响应变慢,使用 top 查看总体情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
top
# top - 10:50:58 up 1 days, 22:10, 1 user, load average: 0.00, 0.00, 0.00
# Tasks: 122 total, 1 running, 71 sleeping, 0 stopped, 0 zombie
# %Cpu0 : 0.0 us, 0.0 sy, 0.0 ni, 96.7 id, 0.0 wa, 0.0 hi, 3.3 si, 0.0 st
# %Cpu1 : 0.0 us, 0.0 sy, 0.0 ni, 95.6 id, 0.0 wa, 0.0 hi, 4.4 si, 0.0 st
# ...
#
# PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
# 7 root 20 0 0 0 0 S 0.3 0.0 0:01.64 ksoftirqd/0
# 16 root 20 0 0 0 0 S 0.3 0.0 0:01.97 ksoftirqd/1
# 2663 root 20 0 923480 28292 13996 S 0.3 0.3 4:58.66 docker-containe
# 3699 root 20 0 0 0 0 I 0.3 0.0 0:00.13 kworker/u4:0
# ...

top 的输出可见:

  • 平均负载为 0,就绪队列里面只有一个进程(1 running)。
  • 每个 CPU 的使用率、所有进程的 CPU 使用率都比较低。

其中 CPU 使用率最高的进程都是软中断线程,问题可能出现在软中断上。观察软中断次数的变化速率:

1
2
3
4
5
6
7
8
9
10
11
12
watch -d cat /proc/softirqs
# CPU0 CPU1
# HI: 0 0
# TIMER: 1083906 2368646 定时中断
# NET_TX: 53 9
# NET_RX: 1550643 1916776 网络接收
# BLOCK: 0 0
# IRQ_POLL: 0 0
# TASKLET: 333637 3930
# SCHED: 963675 2293171 内核调度
# HRTIMER: 0 0
# RCU: 1542111 1590625 RCU 锁

其中 NET_RX 变化特别快,可以初步确定是网络接收软中断出现问题,使用 sar 查看网络收发情况:

1
2
3
4
5
6
7
8
9
sar -n DEV 1

# PPS BPS
# 报告时间 网卡 每秒接收、发送的网络帧数 每秒接收、发送的千字节数
# 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

可见 eth0 接收的 PPS 比较大(12607),而接收的 BPS 却很小(664KB/s),每个网络帧比较小(664*1024/12607 == 54bytes),因此是小包问题。使用 tcpdump 在 eth0 上抓包分析:

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
# ...

可见包的源、目的 IP 和端口,而 Flags [S] 表示这是 SYN 包。SYN 包的 PPS 高达 20000,可见是 SYN FLOOD 攻击。

缓存命中率

CPU 采用多级缓存用于匹配与内存速度的差距:

1
2
3
4
5
6
7
8
9
+---------------------+
| L3 Cache |
+----------+----------+
| L2 Cache | L2 Cache |
+----------+----------+
| L1 Cache | L1 Cache |
+----------+----------+
| Core 0 | Core 1 |
+----------+----------+

其中 L1 Cache、L2 Cache 在单核中,L3 则用在多核中。从 L1 到 L3,大小依次增大、性能依次降低(通过 cat /sys/devices/sytem/cpu/cpu0/cache/index0/size)。

其命中率可衡量 CPU 缓存复用情况,命中率越高性能越好。

优化总结

要对系统 CPU 进行优化,可从应用和系统两个层面考虑了。其中系统优化:

  • CPU 绑定:把进程绑定到一或多个 CPU 上,可提高 CPU 缓存的命中率,减少跨 CPU 调度带来的上下文切换问题。

  • CPU 独占:类似 CPU 绑定,进一步将 CPU 分组,通过 CPU 亲和性机制为其分配进程。这些 CPU 就由指定的进程独占,不允许其他进程使用。

  • 优先级调整:使用 nice 调整进程的优先级(正/负值调低/高)。适当降低非核心应用的优先级,增高核心应用的优先级,可确保核心应用得到优先处理。

  • 为进程设置资源限制:使用 Linux cgroups 设置进程 CPU 使用上限,可防止由于某个应用自身的问题而耗尽系统资源。

  • NUMA(Non-Uniform Memory Access)优化:支持 NUMA 的处理器会被划分为多个 node,每个 node 都有自己的本地内存空间。就是让 CPU 尽可能只访问本地内存。

  • 中断负载均衡:中断处理程序可能会耗费大量的 CPU,开启 irqbalance 服务或者配置 smp_affinity,可以把中断处理过程自动负载均衡到多个 CPU 上。

了解 CPU 相关概念与指标后,可通过下表找到合适的工具、分析问题。

img img

img

参考

分析案例可参考:

CATALOG
  1. 1. Linux CPU 原理与分析
    1. 1.1. 平均负载(Load Average)
      1. 1.1.1. 如何监控?
      2. 1.1.2. 压力测试
    2. 1.2. 使用率(Usage)
      1. 1.2.1. 节拍率
      2. 1.2.2. 统计信息
      3. 1.2.3. 如何监控?
      4. 1.2.4. 问题排查
    3. 1.3. 上下文(Context)
      1. 1.3.1. 进程上下文切换
        1. 1.3.1.1. 切换成本
        2. 1.3.1.2. 切换时机
      2. 1.3.2. 线程上下文切换
      3. 1.3.3. 中断上下文切换
      4. 1.3.4. 压力测试
      5. 1.3.5. 如何监控?
      6. 1.3.6. 问题排查
    4. 1.4. 异常进程
      1. 1.4.1. iowait 分析
      2. 1.4.2. 处理僵尸进程
    5. 1.5. 软中断
      1. 1.5.1. 问题分析
    6. 1.6. 缓存命中率
    7. 1.7. 优化总结
    8. 1.8. 参考