案例:系统中出现大量不可中断进程(D)和僵尸进程(Z)怎么办?

记录《Linux性能优化》课程实践过程。(可点击右方查看目录)

前言

等待 I/O 的 CPU 使用率(以下简称为 iowait)升高,也是最常见的一个服务器性能问题。当 iowait 升高时,进程很可能因为得不到硬件的响应,而长时间处于不可中断状态。从 ps 或者 top 命令的输出中,你可以发现它们都处于 D 状态,也就是不可中断状态(Uninterruptible Sleep)。

进程状态

top 和 ps 是最常用的查看进程状态的工具。如下:
【笔记】系统中出现大量不可中断进程和僵尸进程怎么办?(上)

常见的进程状态有哪些?

  • R 是 Running 或 Runnable 的缩写,表示进程在 CPU 的就绪队列中,正在运行或者正在等待运行。
  • D 是 Disk Sleep 的缩写,也就是不可中断状态睡眠(Uninterruptible Sleep),一般表示进程正在跟硬件交互,并且交互过程不允许被其他进程或中断打断。
  • Z 是 Zombie 的缩写,它表示僵尸进程,也就是进程实际上已经结束了,但是父进程还没有回收它的资源(比如进程的描述符、PID 等)。
  • S 是 Interruptible Sleep 的缩写,也就是可中断状态睡眠,表示进程因为等待某个事件而被系统挂起。当进程等待的事件发生时,它会被唤醒并进入 R 状态。
  • I 是 Idle 的缩写,也就是空闲状态,用在不可中断睡眠的内核线程上。前面说了,硬件交互导致的不可中断进程用 D 表示,但对某些内核线程来说,它们有可能实际上并没有任何负载,用 Idle 正是为了区分这种情况。要注意,D 状态的进程会导致平均负载升高, I 状态的进程却不会。

除了以上 5 个状态,进程还包括下面这 2 个状态。

  • 第一个是 T 或者 t,也就是 Stopped 或 Traced 的缩写,表示进程处于暂停或者跟踪状态。
  • 另一个是 X,也就是 Dead 的缩写,表示进程已经消亡,所以你不会在 top 或者 ps 命令中看到它。

可以通过 man ps 查看详细解释,如图:
【笔记】系统中出现大量不可中断进程和僵尸进程怎么办?

案例一

本案例是一个基于C开发的多进程应用,用来分析大量不可中断状态和僵尸状态进程的问题。由于它的编译和运行步骤比较麻烦,课程提供了 Docker 镜像,只需要运行容器,就可以得到模拟环境。

环境准备

  • 虚拟机(Ubuntu 18.04)
  • 机器配置:2 CPU,8GB 内存
  • 预先安装 docker、sysstat、dstat 等工具,如 apt install docker.io dstat sysstat

打开终端,SSH 登录到机器上,并安装上面提到的工具。
默认以 root 用户运行所有命令,先运行 sudo su root 命令切换到 root 用户。

运行docker环境

docker run --privileged --name=app -itd feisky/app:iowait

下载过程可能比较缓慢… 下载完成后,执行 docker ps -a ,如图:
【笔记】系统中出现大量不可中断进程和僵尸进程怎么办?

输入 ps 命令,确认案例应用是否已正常启动。如下:

$ ps aux | grep /app

【笔记】系统中出现大量不可中断进程和僵尸进程怎么办?

图中,S 表示可中断睡眠状态,D 表示不可中断睡眠状态。关于 s 和 +,s 表示该进程是一个会话的领导进程,而 + 表示前台进程组,可以通过 man ps 查看详细解释。
【笔记】系统中出现大量不可中断进程和僵尸进程怎么办?

关于进程组和会话,它们是用来管理一组相互关联的进程。

  • 进程组表示一组相互关联的进程,比如每个子进程都是父进程所在组的成员;
  • 而会话是指共享同一个控制终端的一个或多个进程组。

应用启动前后的CPU状态

可以执行命令 docker stop app,停止应用。执行命令 top(按下数字 1 切换到所有 CPU 的使用情况),如图:
【笔记】系统中出现大量不可中断进程和僵尸进程怎么办?

执行命令 docker start app,然后 top,最好是观察5分钟以上,如图:
【笔记】系统中出现大量不可中断进程和僵尸进程怎么办?

可以观察到,我的机器出现了一些变化:

  • Load Average:过去 1 分钟、5 分钟和 15 分钟内的平均负载在依次减小,说明平均负载正在升高;而 1 分钟内的平均负载(2.18)已经达到系统的 CPU 个数,说明系统很可能已经有了性能瓶颈。
  • Tasks:有 1 个正在运行的进程,但僵尸进程比较多(114),而且还在不停增加,说明有子进程在退出时没被清理。
  • 接下来看两个 CPU 的使用率情况,用户 CPU 使用率(us)不高,但系统CPU使用率(sy)和 iowait 有明显的升高。
  • 最后再看每个进程的情况, CPU 使用率最高的进程是两个(Z)进程,高达30%多;观察过程中发现,在演变成 Z 状态之前,app 是处于 D 状态的,它们可能在等待 I/O,但光凭这里并不能确定是它们导致了 iowait 升高。

汇总上述情况:

  • 第一点,启动 app 应用程序后,iowait 确实升高了,系统的平均负载升高,甚至达到了系统 CPU 的个数。
  • 第二点,僵尸进程在不断增多,说明有程序没能正确清理子进程的资源。

注意,一开始我的虚拟机只有 1 CPU,发现观察不到任何情况。于是,我重新安装虚拟机,并为其分配了 2 CPU。

案例二

案例一发现了一些负载问题,但具体根源是什么?还得继续分析。

环境准备

与案例一保持一致。

运行docker环境

如下:

# 先删除上次启动的案例
$ docker rm -f app
# 重新运行案例
$ docker run --privileged --name=app -itd feisky/app:iowait

Ⅰ. iowait 分析

先来看一下 iowait 升高的问题。

1. dstat 查看系统I/O

那么什么工具可以查询系统的 I/O 情况呢?这里推荐使用 dstat ,它的好处是,可以同时查看 CPU 和 I/O 这两种资源的使用情况,便于对比分析。

# 间隔1秒输出10组数据
$ dstat 1 10

可以使用 docker stop/start app,观察应用启动前后的变化,如图:
系统中出现大量不可中断进程(D)和僵尸进程(Z)怎么办?

可以看到,每当 iowait 升高(wai)时,对应的磁盘的读请求(read)都会很大。这说明 iowait 的升高跟磁盘的读请求有关,很可能就是磁盘读导致的。

那到底是哪个进程在读磁盘呢?再次关闭并重启应用,top 观察下输出情况。
系统中出现大量不可中断进程(D)和僵尸进程(Z)怎么办?

重点关注下 D 状态的进程,因为它们可能在等待I/O。

2. pidstat 查看进程磁盘读写

使用 pidstat 查看这些进程的磁盘读写情况,如下:

# -d 展示 I/O 统计数据,-p 指定进程号,间隔 1 秒输出 3 组数据
$ pidstat -d -p XXX 1 3

系统中出现大量不可中断进程(D)和僵尸进程(Z)怎么办?

可以看到,这两个进程目前并没有任何磁盘读写。那要怎么知道,到底是哪个进程在进行磁盘读写呢?

继续使用 pidstat,这次观察所有进程的 I/O 使用情况,如下:

# 间隔 1 秒输出多组数据 (这里是 20 组)
$ pidstat -d 1 20

如图:

系统中出现大量不可中断进程(D)和僵尸进程(Z)怎么办?

可以发现,的确是 app 进程在进行磁盘读,并且每秒读的数据有 320 MB,看来就是 app 的问题。

不过,app 进程到底在执行啥 I/O 操作呢?这里,注意进程用户态和内核态的区别,进程想要访问磁盘,就必须使用系统调用。

接着,尝试找出 app 进程的系统调用。

3. strace 跟踪进程系统调用

strace 是常用的跟踪进程系统调用的工具。可以用 -p 参数指定 PID 号:

$ strace -p 6680

系统中出现大量不可中断进程(D)和僵尸进程(Z)怎么办?

图中,提示 strace 命令执行失败,没有权限。其实本身就处于 root 用户,不应该没有权限的。

一般遇到这种问题时,先检查一下进程的状态是否正常。

$ ps aux | grep 6680

如图:
系统中出现大量不可中断进程(D)和僵尸进程(Z)怎么办?

可以看到,进程 6680 已经变成了 Z 状态,也就是僵尸进程。僵尸进程都是已经退出的进程,所以就没法继续分析它的系统调用。

至此,发现 top、pidstat 工具无法给出更多信息。这时,可以求助那些基于事件记录的动态追踪工具,比如 perf。

4. perf 分析系统 CPU 时钟事件

如下:

# 记录性能事件,等待大约15秒后按 Ctrl+C 退出
$ perf record -g

# 查看报告
$ perf report

保持 app 正常启动状态,执行以上命令。(如果提示命令 not found,按提示安装即可)

如图:

系统中出现大量不可中断进程(D)和僵尸进程(Z)怎么办?

图中的 swapper 是内核中的调度进程,可以先忽略掉。

可以发现, app 的确在通过系统调用 sys_read() 读取数据。并且从 new_sync_read 和 blkdev_direct_IO 能看出,进程正在对磁盘进行直接读,也就是绕过了系统缓存,每个读请求都会从磁盘直接读,这就可以解释我们观察到的 iowait 升高了。

关于 blkdev_direct_IO 可能会有点疑惑,可以查阅相关资料了解一下:

找到了问题的根源,那么可以从代码层面,看是否能找到 直接I/O 的操作?

由于 app 是经过编译后的 C 应用程序,所以还是直接看下课程提供的 C 源码 ,具体调用位置如下:

// O_DIRECT 选项打开磁盘,于是绕过了系统缓存,直接对磁盘进行读写。
int fd = open(disk, O_RDONLY | O_DIRECT | O_LARGEFILE, 0755);

5. 优化 iowait 问题

直接读写磁盘,对 I/O 敏感型应用(比如数据库系统)是很友好的,因为你可以在应用中,直接控制磁盘的读写。但在大部分情况下,我们最好还是通过系统缓存来优化磁盘 I/O,换句话说,删除 O_DIRECT 这个选项就是了。

重新下载并启动优化后的镜像,如下:

# 首先删除原来的应用
$ docker rm -f app
# 运行新的应用
$ docker run --privileged --name=app -itd feisky/app:iowait-fix1

重新 top 查看最新输出情况,如图:
系统中出现大量不可中断进程(D)和僵尸进程(Z)怎么办?

可以发现,sy、iowait 确实有明显的下降。不过,zombie 进程的数量还是在不断上升的。

Ⅱ. 僵尸进程

接下来,处理僵尸进程的问题。既然僵尸进程是因为父进程没有回收子进程的资源而出现的,那么,要解决掉它们,就要找到它们的根儿,也就是找出父进程,然后在父进程里解决。

1. pstree 分析进程调用关系

# -a 表示输出命令行选项 -p 表PID -s 表示指定进程的父进程
$ pstree -aps XXX

尝试找下 PID 5638 的父进程,如图:

系统中出现大量不可中断进程(D)和僵尸进程(Z)怎么办?

运行完,你会发现 5638 号进程的父进程是 5630,也就是 app 应用。

所以,我们接着查看 app 应用程序的代码,看看子进程结束的处理是否正确,比如有没有调用 wait() 或 waitpid() ,抑或是,有没有注册 SIGCHLD 信号的处理函数。

关于 父进程、子进程 可能会有点疑惑,可以查阅相关资料了解一下:

2. 查看源码

查看修复 iowait 后的源码文件 app-fix1.c ,找到子进程的创建和清理的地方:

int status = 0;
// 死循环
for (;;) {
    for (i = 0; i < 2; i++) {
        if (fork() == 0) {
            sub_process(disk, buffer_size, buffer_count);
        }
    }
    sleep(5);
}

while (wait(&status) > 0);

很明显,wait() 被放到了 for 死循环的外面,也就是说,wait() 函数实际上并没被调用到。

3. 优化源码

修复代码,如下:

int status = 0;
// 死循环
for (;;) {
    for (i = 0; i < 2; i++) {
        if (fork() == 0) {
            sub_process(disk, buffer_size, buffer_count);
        }
    }
    while (wait(&status) > 0);
    sleep(5);
}

4. 查看优化效果

# 先停止产生僵尸进程的 app
$ docker rm -f app
# 然后启动新的 app
$ docker run --privileged --name=app -itd feisky/app:iowait-fix2

重新 top 查看最新输出情况,如图:

系统中出现大量不可中断进程(D)和僵尸进程(Z)怎么办?

可以看到,已经不再有僵尸进程堆积了。

结语

虽然本文案例是基于 C 语言,但我认为,解决问题的思路都是通用的。如果其他语言的应用遇到了类似问题,比如 PHP应用、Java应用,是不是也可以类似地去解决呢?

理论结合实践,更能加深对知识的理解。如果有不明白的,也可以在评论区留言。

同时,我会把更多实践案例归纳在 Linux 性能优化笔记 文章底部,欢迎阅读。:smile:

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 2年前 自动加精
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 1
╰ゝSakura

好评 :+1:

4年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!