linux性能优化不可中断进程和僵尸进程的问题
Posted sysu_lluozh
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux性能优化不可中断进程和僵尸进程的问题相关的知识,希望对你有一定的参考价值。
CPU使用率的类型,除了用户CPU之外,还包括系统CPU(比如上下文切换)、等待I/O的CPU(比如等待磁盘的响应)以及中断 CPU(包括软中断和硬中断)等
系统CPU使用率高的问题中,等待I/O的CPU使用率(iowait)升高,是最常见的一个服务器性能问题,接下来看一个多进程I/O的案例,并分析这种情况
一、进程状态
当iowait升高时,进程很可能因为得不到硬件的响应,而长时间处于不可中断状态
从ps或者top命令的输出中,可以发现它们都处于D状态,也就是不可中断状态(Uninterruptible Sleep)
既然说到了进程的状态,进程有哪些状态呢?
top和ps是最常用的查看进程状态的工具,从top的输出开始
$ 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
上面是一个top命令输出的示例,S列(Status列)表示进程的状态。从这个示例里,可以看到R、D、Z、S、I等几个状态,它们分别是什么意思呢?
- 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的缩写,表示进程处于暂停或者跟踪状态
向一个进程发送SIGSTOP信号,它就会因响应这个信号变成暂停状态(Stopped)
再向它发送SIGCONT信号,进程又会恢复运行(如果进程是终端里直接启动的,则需要你用fg 命令,恢复到前台运行)
而当用调试器(如gdb)调试一个进程时,在使用断点中断进程后,进程就会变成跟踪状态,这其实也是一种特殊的暂停状态,只不过可以用调试器来跟踪并按需要控制进程的运行
- X
Dead的缩写,表示进程已经消亡,所以不会在top或者ps命令中看到它
了解了这些,再回到今天的主题
先看不可中断状态,这其实是为了保证进程数据与硬件状态一致,并且正常情况下,不可中断状态在很短时间内就会结束。所以,短时的不可中断状态进程一般可以忽略
但如果系统或硬件发生了故障,进程可能会在不可中断状态保持很久,甚至导致系统中出现大量不可中断进程。这时,就得注意下,系统是不是出现了I/O 等性能问题
再看僵尸进程,这是多进程应用很容易碰到的问题,正常情况下:
- 当一个进程创建子进程后,它应该通过系统调用
wait()
或者waitpid()
等待子进程结束,回收子进程的资源 - 当子进程在结束时,会向它的父进程发送SIGCHLD信号,所以,父进程还可以注册SIGCHLD信号的处理函数,异步回收资源
如果父进程没这么做,或是子进程执行太快,父进程还没来得及处理子进程状态,子进程就已经提前退出,那这时的子进程就会变成僵尸进程
换句话说,父亲应该一直对儿子负责,善始善终,如果不作为或者跟不上,都会导致"问题少年"的出现
通常,僵尸进程持续的时间都比较短,在父进程回收它的资源后就会消亡,或者在父进程退出后,由init
进程回收后也会消亡
一旦父进程没有处理子进程的终止,还一直保持运行状态,那么子进程就会一直处于僵尸状态
大量的僵尸进程会用尽 PID 进程号,导致新进程不能创建,所以这种情况一定要避免
二、案例分析
2.1 背景介绍
用一个多进程应用的案例,分析大量不可中断状态和僵尸状态进程的问题
这个应用基于C开发,由于它的编译和运行步骤比较麻烦,把它打包成了一个Docker镜像。这样,只需要运行一个Docker容器就可以得到模拟环境
2.2 操作
安装完成后,首先执行下面的命令运行案例应用:
$ docker run ‑‑privileged ‑‑name=app ‑itd feisky/app:iowait
然后,输入 ps 命令确认案例应用已正常启动
$ ps aux | grep /app
root 4009 0.0 0.0 4376 1008 pts/0 Ss+ 05:51 0:00 /app
root 4287 0.6 0.4 37280 33660 pts/0 D+ 05:54 0:00 /app
root 4288 0.6 0.4 37280 33668 pts/0 D+ 05:54 0:00 /app
从这个界面,可以发现多个app进程已经启动,并且它们的状态分别是Ss+和D+
其中,S 表示可中断睡眠状态,D表示不可中断睡眠状态
- s和+
后面的s和+是什么意思呢?不知道也没关系,查一下man ps就可以
s表示这个进程是一个会话的领导进程,而+表示前台进程组
- 进程组和会话
这里又出现了两个新概念,进程组和会话,它们用来管理一组相互关联的进程
- 进程组表示一组相互关联的进程,比如每个子进程都是父进程所在组的成员
- 会话是指共享同一个控制终端的一个或多个进程组
比如,通过SSH登录服务器,就会打开一个控制终端(TTY),这个控制终端就对应一个会话
而在终端中运行的命令以及它们的子进程,就构成了一个个的进程组,其中:
- 在后台运行的命令,构成后台进程组
- 在前台运行的命令,构成前台进程组
2.3 top分析
明白了这些,再用top看一下系统的资源使用情况:
# 按下数字 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
...
这里发现四个可疑的地方:
- 第一行的平均负载(Load Average)
过去1分钟、5分钟和15分钟内的平均负载在依次减小,说明平均负载正在升高
而1分钟内的平均负载已经达到系统的CPU个数,说明系统很可能已经有了性能瓶颈
- 第二行的 Tasks
有1个正在运行的进程,但僵尸进程比较多,而且还在不停增加,说明有子进程在退出时没被清理
- CPU使用率
查看两个CPU 的使用率情况,用户CPU和系统CPU 都不高,但iowait
分别是60.5%和94.6%,有点儿不正常
- 进程情况
每个进程的情况,CPU使用率最高的进程只有0.3%,看起来并不高
但有两个进程处于D状态,它们可能在等待I/O,但光凭这里并不能确定是它们导致了iowait升高
把这四个问题再汇总一下,就可以得到很明确的两点:
- iowait 太高,导致系统的平均负载升高,甚至达到了系统 CPU 的个数
- 僵尸进程在不断增多,说明有程序没能正确清理子进程的资源
接下来,顺着这两个问题继续分析,找出根源
2.4 iowait分析
先来看一下 iowait升高的问题
一提到iowait升高,首先会想要查询系统的I/O情况,那么什么工具可以查询系统的I/O情况呢?
2.4.1 dstat工具
可以使用dstat工具,它的好处是可以同时查看CPU和I/O这两种资源的使用情况,便于对比分析
2.4.2 dstat查看
在终端中运行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
从dstat的输出可以看到,每当iowait升高(wai)时,磁盘的读请求(read)都会很大
这说明iowait的升高跟磁盘的读请求有关,很可能就是磁盘读导致
那到底是哪个进程在读磁盘呢?在2.3中的top里就看到了不可中断状态进程,现在试着来分析下
2.4.3 top观察D状态进程
继续在刚才的终端中,运行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
2.4.4 pidstat查看具体进程I/O
接着,查看这些进程的磁盘读写情况
一般要查看某一个进程的资源使用情况,都可以用pidstat,不过需要记得加上-d
参数,以便输出I/O使用情况
比如,以4344为例,在终端里运行下面的pidstat命令,并用-p 4344参数指定进程号:
# ‑d 展示 I/O 统计数据,‑p 指定进程号,间隔 1 秒输出 3 组数据
$ pidstat ‑d ‑p 4344 1 3
06:38:50 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
06:38:51 0 4344 0.00 0.00 0.00 0 app
06:38:52 0 4344 0.00 0.00 0.00 0 app
06:38:53 0 4344 0.00 0.00 0.00 0 app
在这个输出中,kB_rd表示每秒读的KB数,kB_wr表示每秒写的KB数,iodelay表示I/O的延迟(单位是时钟周期)。它们都是 0,那就表示此时没有任何的读写,说明问题不是4344进程导致
可是,用同样的方法分析进程4345,发现也没有任何磁盘读写
2.4.5 pidstat查看所有进程I/O
那要怎么知道,到底是哪个进程在进行磁盘读写呢?继续使用pidstat,但这次去掉进程号,干脆观察所有进程的I/O使用情况
在终端中运行下面的pidstat命令:
# 间隔 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进程在进行磁盘读,并且每秒读的数据有32MB,看来就是app的问题
2.4.6 strace跟踪进程系统调用
不过,app进程到底在执行啥I/O操作呢?
回顾一下进程用户态和内核态的区别,进程想要访问磁盘就必须使用系统调用,所以接下来重点就是找出app进程的系统调用
strace正是最常用的跟踪进程系统调用的工具
从pidstat的输出中拿到进程的PID号,比如6082,然后在终端中运行strace命令,并用-p参数指定PID号:
$ strace ‑p 6082
strace: attach: ptrace(PTRACE_SEIZE, 6082): Operation not permitted
这儿出现一个奇怪的错误,strace命令居然失败,并且命令报出的错误是没有权限
按理来说,所有操作都已经是以root用户运行,那为什么还会没有权限呢?
2.4.7 检查进程状态
一般遇到这种问题时,可以先检查一下进程的状态是否正常。比如,继续在终端中运行ps命令,并使用grep找出刚才的6082号进程:
$ ps aux | grep 6082
root 6082 0.0 0.0 0 0 pts/0 Z+ 13:43 0:00 [app] <defunct>
果然,进程6082已经变成了Z状态,也就是僵尸进程
僵尸进程都是已经退出的进程,所以就没法儿继续分析它的系统调用
2.4.8 perf动态追踪
到这一步,系统iowait的问题还在继续,但是top、pidstat这类工具已经不能给出更多的信息
这时,应该求助那些基于事件记录的动态追踪工具
可以用perf top
看看有没有新发现。再或者在终端中运行perf record
,持续一会儿(例如15 秒),然后按Ctrl+C退出,再运行perf report查看报告:
$ perf record ‑g
$ perf report
接着,找到关注的app进程,按回车键展开调用栈,就会得到下面这张调用关系图:
这个图中的swapper是内核中的调度进程,可以先忽略
看其他信息可以发现,app的确在通过系统调用sys_read()
读取数据,并且从new_sync_read
和blkdev_direct_IO
能看出,进程正在对磁盘进行直接读,也就是绕过了系统缓存,每个读请求都会从磁盘直接读,这就可以解释观察到iowait升高的原因
看来,罪魁祸首是app内部进行了磁盘的直接I/O
2.4.9 问题解决
接下来从代码层面分析,究竟是哪里出现了直接读请求
查看源码文件app.c,发现果然使用了O_DIRECT
选项打开磁盘,于是绕过了系统缓存,直接对磁盘进行读写
open(disk, O_RDONLY|O_DIRECT|O_LARGEFILE, 0755)
直接读写磁盘,对I/O敏感型应用(比如数据库系统)是很友好的,因为可以在应用中直接控制磁盘的读写
但在大部分情况下,最好还是通过系统缓存来优化磁盘I/O,换句话说,删除O_DIRECT
这个选项就是了
app-fix1.c是修改后的文件,打包成了一个镜像文件,运行下面的命令可以启动:
# 首先删除原来的应用
$ docker rm ‑f app
# 运行新的应用
$ docker run ‑‑privileged ‑‑name=app ‑itd feisky/app:iowait‑fix1
最后,再用top检查一下:
$ top
top ‑ 14:59:32 up 19 min, 1 user, load average: 0.15, 0.07, 0.05
Tasks: 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.5 僵尸进程
仔细观察僵尸进程的数量,可以发现僵尸进程还在不断的增长中
2.5.1 pstree确定父进程
既然僵尸进程是因为父进程没有回收子进程的资源而出现的,那么要解决掉它们,就要找到它们的根儿,也就是找出父进程,然后在父进程里解决
父进程的找法最简单的就是运行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应用
2.5.2 查看子进程结束处理
所以,接着查看app应用程序的代码,看看子进程结束的处理是否正确,比如:
- 有没有调用
wait()
或waitpid()
- 有没有注册SIGCHLD信号的处理函数
查看修复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.5.3 问题修复
修改后的文件放到了app-fix2.c中,也打包成了一个Docker镜像,运行下面的命令启动:
# 先停止产生僵尸进程的 app
$ docker rm ‑f app
# 然后启动新的 app
$ docker run ‑‑privileged ‑‑name=app ‑itd feisky/app:iowait‑fix2
启动后,再用top最后来检查一遍:
$ top
top ‑ 15:00:44 up 20 min, 1 user, load average: 0.05, 0.05, 0.04
Tasks: 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,问题终于全部解决了
三、小结
主要通过简单的操作,熟悉几个必备的进程状态
用ps或者top可以查看进程的状态,这些状态包括运行®、空闲(I)、不可中断睡眠(D)、可中断
睡眠(S)、僵尸(Z)以及暂停(T)等
其中,不可中断状态和僵尸状态是此次的重点
- 不可中断状态
表示进程正在跟硬件交互,为了保护进程数据和硬件的一致性,系统不允许其他进程或中断打断这个进程
进程长时间处于不可中断状态,通常表示系统有I/O性能问题
- 僵尸进程
表示进程已经退出,但它的父进程还没有回收子进程占用的资源
短暂的僵尸状态通常不必理会,但进程长时间处于僵尸状态需要注意,可能有应用程序没有正常处理子进程的退出
用一个多进程的案例,分析系统等待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
信号处理函数的注册
以上是关于linux性能优化不可中断进程和僵尸进程的问题的主要内容,如果未能解决你的问题,请参考以下文章
linux 性能优化之路: 什么是平均负载, 如何判断是哪种负载过高(cpu密集, io密集, 大量进程)