java中的接口是类吗
267
2022-11-04
系统中出现大量不可中断进程和僵尸进程怎么办(下)
一、问题分析(延续上一章案例)
1.1、打开一个终端,登录上次机器,执行如下命令,运行案例
# 先删除上次启动的案例$ docker rm -f app# 重新运行案例$ docker run --privileged --name=app -itd liushiju/app:iowait
1.2、iowait 分析
一般思路:查询系统的I/O情况使用工具:dstat,此工具的好处是可以同时查看CPU和I/O这两种资源的使用情况,便于分析。
1.2.1、终端运行dstat命令,观察CPU和I/O使用情况
# 间隔1秒输出10组数据$ dstat 1 10You 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
从 dstat 的输出,可以看到,每当 iowait 升高(wai)时,磁盘的读请求(read)都会很大。这说明 iowait 的升高跟磁盘的读请求有关,很可能就是磁盘读导致的。
那到底是哪个进程在读磁盘呢?不知道你还记不记得,上节在 top 里看到的不可中断状态进程,我觉得它就很可疑,我们试着来分析下。
1.2.2、终端运行top命令,观察D状态的进程
# 观察一会儿按 Ctrl+C 结束$ top... 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...
我们从 top 的输出找到 D 状态进程的 PID,你可以发现,这个界面里有两个 D 状态的进程,PID 分别是 4344 和 4345。
接着,我们查看这些进程的磁盘读写情况。对了,别忘了工具是什么。一般要查看某一个进程的资源使用情况,都可以用pidstat,加上 -d 参数,输出 I/O 使用情况。
1.2.3、以4344为例,终端运行pidstat命令,并用-p 4344参数指定进程号
# -d 展示 I/O 统计数据,-p 指定进程号,间隔 1 秒输出 3 组数据$ pidstat -d -p 4344 1 306:38:50 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command06:38:51 0 4344 0.00 0.00 0.00 0 app06:38:52 0 4344 0.00 0.00 0.00 0 app06:38:53 0 4344 0.00 0.00 0.00 0 app
在这个输出中, kB_rd 表示每秒读的 KB 数, kB_wr 表示每秒写的 KB 数,iodelay 表示 I/O 的延迟(单位是时钟周期)。它们都是 0,那就表示此时没有任何的读写,说明问题不是 4344 进程导致的。
可是,用同样的方法分析进程 4345,你会发现,它也没有任何磁盘读写。
那要怎么知道,到底是哪个进程在进行磁盘读写呢?继续使用 pidstat,但这次去掉进程号,干脆就来观察所有进程的 I/O 使用情况。
1.2.4、终端运行pidstat命令
# 间隔 1 秒输出多组数据 (这里是 20 组)$ pidstat -d 1 20...06:48:46 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command06:48:47 0 4615 0.00 0.00 0.00 1 kworker/u4:106:48:47 0 6080 32768.00 0.00 0.00 170 app06:48:47 0 6081 32768.00 0.00 0.00 184 app06:48:47 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command06:48:48 0 6080 0.00 0.00 0.00 110 app06:48:48 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command06:48:49 0 6081 0.00 0.00 0.00 191 app06:48:49 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command06:48:50 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command06:48:51 0 6082 32768.00 0.00 0.00 0 app06:48:51 0 6083 32768.00 0.00 0.00 0 app06:48:51 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command06:48:52 0 6082 32768.00 0.00 0.00 184 app06:48:52 0 6083 32768.00 0.00 0.00 175 app06:48:52 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command06:48:53 0 6083 0.00 0.00 0.00 105 app...
观察一会儿可以发现,的确是 app 进程在进行磁盘读,并且每秒读的数据有 32 MB,看来就是 app 的问题。不过,app 进程到底在执行啥 I/O 操作呢?
这里,我们需要回顾一下进程用户态和内核态的区别。进程想要访问磁盘,就必须使用系统调用,所以接下来,重点就是找出 app 进程的系统调用了。
strace 正是最常用的跟踪进程系统调用的工具。所以,我们从 pidstat 的输出中拿到进程的 PID 号,比如 6082,然后在终端中运行 strace 命令,并用 -p 参数指定 PID 号
1.2.5、strace 跟踪进程系统调用
$ strace -p 6082strace: attach: ptrace(PTRACE_SEIZE, 6082): Operation not permitted
这儿出现了一个奇怪的错误,strace 命令居然失败了,并且命令报出的错误是没有权限。按理来说,我们所有操作都已经是以 root 用户运行了,为什么还会没有权限呢?你也可以先想一下,碰到这种情况,你会怎么处理呢?
一般遇到这种问题时,我会先检查一下进程的状态是否正常。比如,继续在终端中运行 ps 命令,并使用 grep 找出刚才的 6082 号进程
1.2.6、检查进程状态是否正常
$ ps aux | grep 6082root 6082 0.0 0.0 0 0 pts/0 Z+ 13:43 0:00 [app]
果然,进程 6082 已经变成了 Z 状态,也就是僵尸进程。僵尸进程都是已经退出的进程,所以就没法儿继续分析它的系统调用。关于僵尸进程的处理方法,我们一会儿再说,现在还是继续分析 iowait 的问题。
到这一步,你应该注意到了,系统 iowait 的问题还在继续,但是 top、pidstat 这类工具已经不能给出更多的信息了。这时,我们就应该求助那些基于事件记录的动态追踪工具了。
你可以用 perf top 看看有没有新发现。再或者,可以像我一样,在终端中运行 perf record,持续一会儿(例如 15 秒),然后按 Ctrl+C 退出,再运行 perf report 查看报告
1.2.7、perf top 基于时间记录动态追踪工具
$ perf record -g$ perf report
接着,找到我们关注的 app 进程,按回车键展开调用栈,你就会得到下面这张调用关系图
这个图里的 swapper 是内核中的调度进程,你可以先忽略掉。
我们来看其他信息,你可以发现, app 的确在通过系统调用 sys_read() 读取数据。并且从 new_sync_read 和 blkdev_direct_IO 能看出,进程正在对磁盘进行直接读,也就是绕过了系统缓存,每个读请求都会从磁盘直接读,这就可以解释我们观察到的 iowait 升高了。
看来,罪魁祸首是 app 内部进行了磁盘的直接 I/O !
下面的问题就容易解决了。我们接下来应该从代码层面分析,究竟是哪里出现了直接读请求。查看源码文件 app.c,你会发现它果然使用了 O_DIRECT 选项打开磁盘,于是绕过了系统缓存,直接对磁盘进行读写。
1.2.8、查看源码文件app.c
open(disk, O_RDONLY|O_DIRECT|O_LARGEFILE, 0755)
直接读写磁盘,对 I/O 敏感型应用(比如数据库系统)是很友好的,因为你可以在应用中,直接控制磁盘的读写。但在大部分情况下,我们最好还是通过系统缓存来优化磁盘 I/O,换句话说,删除 O_DIRECT 这个选项就是了。
app-fix1.c 就是修改后的文件,我也打包成了一个镜像文件,运行下面的命令,你就可以启动它了
1.2.9、修改源码app-fix1.c
# 首先删除原来的应用$ docker rm -f app# 运行新的应用$ docker run --privileged --name=app -itd liushiju/app:iowait-fix1
1.2.10、最后再用 top 检查
$ toptop - 14:59:32 up 19 min, 1 user, load average: 0.15, 0.07, 0.05Tasks: 137 total, 1 running, 72 sleeping, 0 stopped, 12 zombie%Cpu0 : 0.0 us, 1.7 sy, 0.0 ni, 98.0 id, 0.3 wa, 0.0 hi, 0.0 si, 0.0 st%Cpu1 : 0.0 us, 1.3 sy, 0.0 ni, 98.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st... PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 3084 root 20 0 0 0 0 Z 1.3 0.0 0:00.04 app 3085 root 20 0 0 0 0 Z 1.3 0.0 0:00.04 app 1 root 20 0 159848 9120 6724 S 0.0 0.1 0:09.03 systemd 2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd 3 root 20 0 0 0 0 I 0.0 0.0 0:00.40 kworker/0:0...
你会发现, iowait 已经非常低了,只有 0.3%,说明刚才的改动已经成功修复了 iowait 高的问题,大功告成!不过,别忘了,僵尸进程还在等着你。仔细观察僵尸进程的数量,你会郁闷地发现,僵尸进程还在不断的增长中。
二、僵尸进程
接下来,就来处理僵尸进程的问题。既然僵尸进程是因为父进程没有回收子进程的资源而出现的,那么,要解决掉它们,就要找到它们的根儿,也就是找出父进程,然后在父进程里解决。
2.1、pstree 命令查找父进程
# -a 表示输出命令行选项# p表PID# s表示指定进程的父进程$ pstree -aps 3084systemd,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() ,亦或是,有没有注册 SIGCHLD 信号的处理函数。
2.2、查看修复 iowait 后的源码文件 app-fix1.c ,找到子进程的创建和清理的地方
int status = 0; for (;;) { for (int i = 0; i < 2; i++) { if(fork()== 0) { sub_process(); } } sleep(5); } while(wait(&status)>0);
循环语句本来就容易出错,你能找到这里的问题吗?这段代码虽然看起来调用了 wait() 函数等待子进程结束,但却错误地把 wait() 放到了 for 死循环的外面,也就是说,wait() 函数实际上并没被调用到,我们把它挪到 for 循环的里面就可以了。
2.3、修改源码文件 app-fix2.c ,也打包成了一个 Docker 镜像,运行下面的命令可以启动它
# 先停止产生僵尸进程的 app$ docker rm -f app# 然后启动新的 app$ docker run --privileged --name=app -itd feisky/app:iowait-fix2
2.4、启动后,再用top检查
$ toptop - 15:00:44 up 20 min, 1 user, load average: 0.05, 0.05, 0.04Tasks: 125 total, 1 running, 72 sleeping, 0 stopped, 0 zombie%Cpu0 : 0.0 us, 1.7 sy, 0.0 ni, 98.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st%Cpu1 : 0.0 us, 1.3 sy, 0.0 ni, 98.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st... PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 3198 root 20 0 4376 840 780 S 0.3 0.0 0:00.01 app 2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd 3 root 20 0 0 0 0 I 0.0 0.0 0:00.41 kworker/0:0...
僵尸进程(Z 状态)没有了, iowait 也是 0,问题终于全部解决了。
三、小结
多进程案例,分析系统等待I/O的CPU使用率(也就是iowait%)升高的情况。
虽然这个案例是磁盘 I/O 导致了 iowait 升高,不过,iowait 高不一定代表 I/O 有性能瓶颈。当系统中只有 I/O 类型的进程在运行时,iowait 也会很高,但实际上,磁盘的读写远没有达到性能瓶颈的程度。
到 iowait 升高时,需要先用 dstat、pidstat 等工具,确认是不是磁盘 I/O 的问题,然后再找是哪些进程导致了 I/O。
等待 I/O 的进程一般是不可中断状态,所以用 ps 命令找到的 D 状态(即不可中断状态)的进程,多为可疑进程。但这个案例中,在 I/O 操作后,进程又变成了僵尸进程,所以不能用 strace 直接分析这个进程的系统调用。
这种情况下,我们用了 perf 工具,来分析系统的 CPU 时钟事件,最终发现是直接 I/O 导致的问题。这时,再检查源码中对应位置的问题,就很轻松了。
而僵尸进程的问题相对容易排查,使用 pstree 找出父进程后,去查看父进程的代码,检查 wait() / waitpid() 的调用,或是 SIGCHLD 信号处理函数的注册就行了。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~