这篇文章上次修改于 761 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

1 系统 CPU 利用高,但为啥找不到高 CPU 应用?

top 显示 CPU 利用率较高,但是每个进程的 CPU 利用率不高,且从 pidstat 也看出各 CPU 的利用率不高。

继续查看 top 输出,发现 running 为 6,大于 CPU 核数。

继续分析处于 R(Running)状态的进程,用 pidstat -p 指定进程 PID,发现没有任何输出,用 ps 查找,发现进程已经不存在了。

top 显示 CPU 利用率仍然较高,且之前的进程仍在,只是 PID 不一样了,说明那些进程在不停的重启,原因可能为:

  • 进程不断崩溃重启。
  • 这些进程都是短时进程,也就是在其应用内部通过 exec 调用的外部命令。短时进程较难用 top 这种间隔时间较长的工具发现。

PID 不断变化的进程看起来是被其他进程调用的短时进程,如果继续分析,需要找到它的父进程,可用 pstree 显示进程间的调用关系。

$ pstree | grep stress
        |-tmux-+-4*[bash---bash---docker---23*[{docker}]]

找到父进程后,可进入 docker 分析,通过 grep 找到调用 stress 的地方。

分析出问题来源后,需要确认有大量的 tmux 进程,可通过 perf 查看。

# 记录性能事件,大约 15s 后按下 Ctrl + C
$ perf record -g

# 查看报告
$ perf report

可以看到 stress 占用了所有 CPU 时钟事件的 70%,调用栈中比例最高的是随机生成函数 random()。

更好的分析工具:execsnoop,可监控短时进程。它通过 fstrace 实时监控进程的 exec() 行为,并输出短时进程的基本信息,包括进程 PID、父进程 PID、命令行参数以及执行结果。

2 系统出现大量不可中断进程和僵尸进程怎么办

当 iowait 升高时,进程可能因为得不到硬件的响应,而长时间处于不可中断状态。ps 或 top 的输出中处于 D 状态时,就是不可中断状态。

$ top
 PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 3229 walle     20   0 3263992 565016  94156 S   0.7  0.9 363:58.07 compiz
 1411 nx        20   0  895564 270148  11304 S   0.3  0.4 102:22.30 nxserver.bin
 1431 root      20   0 1523336  23392   3440 S   0.3  0.0  45:29.56 containerd
 1619 root     -51   0       0      0      0 S   0.3  0.0  51:29.60 irq/145-nvidia
19760 root      20   0   43840   4092   3384 R   0.3  0.0   0:00.01 top
30401 root      20   0  370840  27400  22936 S   0.3  0.0 102:20.71 nxclient.bin
    1 root      20   0  185456   5268   3248 S   0.0  0.0   0:16.81 systemd
    2 root      20   0       0      0      0 S   0.0  0.0   0:00.30 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

S 列表示进程的状态。

  • R Running 或 Runnable 的缩写,表示进程在 CPU 的就绪队列中,正在运行或等待运行。
  • D Disk Sleep 的缩写,是不可中断的睡眠状态,一般表示正在跟硬件交互,交互过程不可被其他进程或中断打断。该状态会在很短时间内结束,一般可以忽略。
  • Z Zombie 的缩写。僵尸进程,进程实际上结束了,但是父进程还没有回收它的进程描述符、PID 等资源。
  • S Interruptible Sleep 的缩写,可中断的睡眠状态,表示进程因为等待某个事件而被系统挂起。当等待的事件发生时,会被唤醒并进入 R 状态。
  • I Idle 的缩写,空闲状态,用在不可中断睡眠的内核线程上。
  • t Stopped 或 Traced 的缩写,表示进程处于暂停或跟踪状态。收到 SIGSTOP 后进入暂停状态,收到 SIGCONT 后恢复运行。
  • X Dead 的缩写,表示已经消亡,不会出现在 top 或 ps 输出中。
# 按下数字 1 切换到所有 CPU 的使用情况,观察一会儿按 Ctrl+C 结束
$ 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
...
  • 通过 top 命令后,看到 CPU 负载达到系统 CPU 个数,说明出现性能问题。
  • 第二行发现僵尸进程较多,且不停增加,说明子进程退出后没有被清理。
  • CPU 利用率不高,但 iowait 较高。
  • 有处于 D 状态的进程,可能在等待 I/O

可得到如下结论:

  • iowait 太高,导致 CPU 负载较高,甚至达到 CPU 个数。
  • 僵尸进程不断增多,说明程序没能正确清理子进程资源。

2.1 分析 iowait

接下来分析 iowait,一般需要查询系统的 I/O 情况。dstat 可同时观察系统的 CPU、磁盘 I/O、网络及内存使用情况。

# 间隔1秒输出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
  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

可见,当 iowait(wai)升高时,磁盘读请求增大。

需要找出哪个进程在读磁盘,可用 pidstat 查看进程的资源使用情况。

# -d 展示 I/O 统计数据
# 间隔 1 秒输出多组数据 (这里是 20 组)
$ 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

06:48:48      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command
06:48:49        0      6081      0.00      0.00      0.00     191  app

06:48:49      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command

06:48:50      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command
06:48:51        0      6082  32768.00      0.00      0.00       0  app
06:48:51        0      6083  32768.00      0.00      0.00       0  app

06:48:51      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command
06:48:52        0      6082  32768.00      0.00      0.00     184  app
06:48:52        0      6083  32768.00      0.00      0.00     175  app

06:48:52      UID       PID   kB_rd/s   kB_wr/s kB_ccwr/s iodelay  Command
06:48:53        0      6083      0.00      0.00      0.00     105  app
...

是 app 进程在读磁盘。app 在执行什么 I/O 操作呢,需要找出 app 的系统调用,可用如下命令查看:

$ perf record -g
$ perf report

可以看到,app 在调用 sys_read() 读取数据,通过 new_sync_read 和 blkdev_read_iter 可以看出,进程正在对磁盘进行直接读,绕过系统缓存,导致 I/O 升高。

查看源码,会看到它使用了 O_DIRECT 打开磁盘:

open(disk, O_RDONLY|O_DIRECT|O_LARGEFILE, 0755)

2.2 僵尸进程

需要找到父进程,然后在父进程中解决。可运行 pstree 命令。

# -a 表示输出命令行选项
# p表PID
# s表示指定进程的父进程
$ 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 app 进程。接下在 app 代码中查看子进程退出时父进程是否调用 wait() 或 waitpid(),或者注册 SIGCHILD 信号处理函数即可。

参考

倪朋飞. Linux 性能优化实战.