docker产生大量僵尸进程_子进程


接上一篇文章。首先,打开一个终端,登陆到上次的机器中。然后执行下面的命令,重启这个案例:

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

iowait分析

先看一下iowait升高的问题。一般情况下,iowait升高首先会查询系统的I/O情况。这里我们使用dstat来查看CPU和I/O的情况:


docker产生大量僵尸进程_docker产生大量僵尸进程_02


从dstat输出我们看到,每当iowait升高时 ,磁盘的读请求(read)都会很大。说明iowait的升高和磁盘的读取有关。

接下来我们使用top命令来查看是哪个进程读取磁盘:


docker产生大量僵尸进程_子进程_03


我们从top的输出找到D状态进程的PID,你可以发现,这个界面里有两个D状态的进程PID分别是4344和4345。

接着,我们查看这些进程的磁盘读写情况。对了,别忘了工具是什么。一般要查看某一个进程的资源使用情况,都可以用我们的老朋友 pidstat,不过这次记得加上-d参数,以便输出I/O使用情况。

比如,以4344为例,我们在终端里运行下面的 pidstat命令,并用-p 4344参数指定进程号:


docker产生大量僵尸进程_docker产生大量僵尸进程_04


在这个输出中,kBrd表示每秒读的KB数,kBwr表示每秒写的KB数, odelay表示IO的延迟(单位是时钟周期)。它们都是0,那就表示此时没有任何的读写,说明问题不是4344进程导致的。

可是,用同样的方法分析进程4345,你会发现,它也没有任何磁盘读写。

那要怎么知道,到底是哪个进程在进行磁盘读写呢?我们继续使用 pidstat,但这次去掉进程号,干脆就来观察所有进程的IO使用情况。


docker产生大量僵尸进程_子进程_05


在终端中运行下面的pidstat命令:


docker产生大量僵尸进程_子进程_06


观察一会儿可以发现,的确是app进程在进行磁盘读,并且每秒读的数据有32MB,看来就是app的问题。不过,app进程到底在执行啥IO操作呢?

这里,我们需要回顾一下进程用户态和内核态的区别。进程想要访问磁盘,就必须使用系统调用,所以接下来,重点就是找出app进程的系统调用了。

strace正是最常用的跟踪进程系统调用的工具。所以,我们从pidstat的输出中拿到进程的PID号,比如6082,然后在终端中运行strace,并用-p参数指定PID号。

strace -p 6082

strace: attach: ptrace(PTRACE_SEIZE, 6082): Operation not permitted

出现了一个奇怪的错误, strace命令居然失败了,并且命令报出的错误是没有权限。按理来说,我们所有操作都已经是以root用户运行了,为什么还会没有权限呢?你也可以先想一下碰到这种情况,你会怎么处理呢?

遇到这种问题时,我会先检查一下进程的状态是否正常。比如,继续在终端中运行ps命令,并使用grep找出刚才的6082号进程

复制


docker产生大量僵尸进程_子进程_07


果然,进程6082已经变成了Z状态,也就是僵尸进程。僵尸进程都是已经退出的进程,所以就没法儿继续分析它的系统调用。关于僵尸进程的处理方法,我们一会儿再说,现在还是继续分析iowait的问题。


docker产生大量僵尸进程_子进程_08


应该注意到了,系统iowait的问题还在继续,但是top、 pidstat这类工具已经不

能给出更多的信息了。这时,我们就应该求助那些基于事件记录的动态追踪工具了。

你可以用 perf top看看有没有新发现。再或者,可以像我一样,在终端中运行perf record,持续一会儿(例如15秒),然后按Ctr+C退出,再运行 perf report查看报告:

perf record -g

perf report

之后,找到我们关注的app进程,按回车键展开调用栈,你就会得到下面这张调用关系图:


docker产生大量僵尸进程_docker产生大量僵尸进程_09


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

我们来看其他信息,你会发现,app的确在通过系统调用 sys _ read()读取数据。并且从new sync_read和blkdey direct io能看出,进程正在对磁盘进行直接读,也就是绕过了系统缓存,每个读请求都会从磁盘直接读,这就可以解释我们观察到的 iowait升高了。

看来,罪魁祸首是app内部进行了磁盘的直接I/O啊。

下面的问题就容易解决了。我们接下来应该从代码层面分析,究竟是哪里出现了直接读请求。查源码文件app.c,你会发现它果然使用了 O_DIRECT选项打开磁盘,于是绕过了系统缓存对磁盘进行读写。

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

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

app-fix1.c就是修改后的文件,我也打包成了一个镜像文件,运行下面的命令,你就可以启动它了:


docker产生大量僵尸进程_docker产生大量僵尸进程_10


最后,再用top检查一下:


docker产生大量僵尸进程_僵尸进程_11


你会发现, nowait已经非常低了,只有0.3%,说明刚才的改动已经成功修复iowait高的问题,大功告成!不过,别忘了,僵尸进程还在等着你。仔细观察僵尸进程的数量,你会发现,僵尸进程还在不断的增长中。

僵尸进程

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

进程的找法我们前面讲过,最简单的就是运行pstree命令:


docker产生大量僵尸进程_系统调用_12


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

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

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


docker产生大量僵尸进程_子进程_13


循环语句本来就容易出错,你能找到这里的问题吗?这段代码虽然看起来调用了Wait0函数等待子进程结束,但却错误地把wait()放到了for死循环的外面,也就是说,wait()函数实际上并没有被调用到,我们把它挪到for循环的里面就可以了

修改后的文件我放到了app-fx2c中,也打包成了一个 Docker镜像,运行下面的命令,你就可以启动它了:


docker产生大量僵尸进程_docker产生大量僵尸进程_14


启动后,再用top最后来检查一下:


docker产生大量僵尸进程_僵尸进程_15


好了,僵尸进程(Z状态)没有了, nowait也是0,问题终于全部解决了。