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

当iowait 升高时,进程可能因为得不到硬件响应,而长时间处于不可中断状态,从ps或top命查看进程状态

进程状态

  • R是Running或Runnable的缩写,表示进程在CPU的就绪队列中,正在运行或者正在等待运行。

  • D是Disk Sleep 的缩写,也就是不可中断状态睡眠(Uninterruptible Sleep), 一般表示进程正在跟硬件交互,并且交互过程不允许被其他进程或中断打断。

  • Z是Zombie的缩写,、示僵尸进程,也就是进程实际.上已经结束了,但是父进程还没有回收它的资源(比如进程的描述符、PID等)。

  • S是Interruptible Sleep的缩写,也就是可中断状态睡眠,表示进程因为等待某个事件而被系统挂起。当进程等待的事件发生时,它会被唤醒并进入R状态。

  • I是Idle的缩写,也就是空闲状态,用在不可中断睡眠的内核线程上。前面说了,硬件交互导致的不可中断进程用D表示,但对某些内核线程来说,它们有可能实际上并没有任何负载,用Idle正是为了区分这种情况。要注意,D状态的进程会导致平均负载升高,1状态的进程却不会。

  • T 或 t 也就是 Stopped 或 Traced 的缩写,表示进程处于暂停或者跟踪状态。向一个进程发送 SIGSTOP 信号,它就会因响应这个信号变成暂停状态(Stopped);再向它发送 SIGCONT 信号,进程又会恢复运行(如果进程是终端里直接启动的,则需要你用 fg 命令,恢复到前台运行)。

  • X 表示 Dead,不会再top或ps中看到

不可中断状态为了保证进程数据与硬件状态一致,正常情况下,不可中断状态在很短时间内就会结束。所以,短时的不可中断状态进程,我们一般可以忽略。

但如果系统或硬件发生了故障,进程可能会在不可中断状态保持很久,甚至导致系统中出现大量不可中断进程。这时,你就得注意下,系统是不是出现了I/O 等性能问题。

僵尸进程,是多进程应用很容易碰到的问题。正常情况下,当-一个进程创建了子进程后,它应该通过系统调用wait()或者waitpid()等待子进程结束,回收子进程的资源;而子进程在结束时,会向它的父进程发送SIGCHLD信号,所以,父进程还可以注册SIGCHL .D信号的处理函数,异步回收资源。

如果父进程没这么做,或是子进程执行太快,父进程还没来得及处理子进程状态,子进程就已经,提前退出,那这时的子进程就会变成僵尸进程。换句话说,父亲应该一直对儿子负责,善始善终,如果不作为或者跟不上,都会导致“问题少年”的出现。

通常,僵尸进程持续的时间都比较短,在父进程回收它的资源后就会消亡;或者在父进程退出后,由init进程回收后也会消亡。

一旦父进程没有处理子进程的终止,还一直保持运行状态,那么子进程就会一直处于僵尸状态。大量的僵尸进程会用尽 PID 进程号,导致新进程不能创建,所以这种情况一定要避免。

案例分析

1.下载镜像

1
docker run --privileged --name=app -itd feisky/app:iowait /app -d /dev/vdc -s 67108864 -c 20

2.查看ps

1
2
3
4
5
6
7
root@linux:~# ps aux | grep /app
root 9321 0.1 0.0 4512 760 pts/0 Ss+ 10:16 0:00 /app -d /dev/vdc -s 67108864 -c 20
root 9361 0.5 0.8 70052 65832 pts/0 D+ 10:16 0:00 /app -d /dev/vdc -s 67108864 -c 20
root 9362 0.5 0.8 70052 65832 pts/0 D+ 10:16 0:00 /app -d /dev/vdc -s 67108864 -c 20
root 9379 0.7 0.8 70052 65832 pts/0 D+ 10:16 0:00 /app -d /dev/vdc -s 67108864 -c 20
root 9380 0.7 0.8 70052 65832 pts/0 D+ 10:16 0:00 /app -d /dev/vdc -s 67108864 -c 20
root 9382 0.0 0.0 15996 1052 pts/0 S+ 10:16 0:00 grep --color=auto /app

从这个界面,我们可以发现多个app进程已经启动,并且它们的状态分别是Ss+和D+。其中,S表示可中断睡眠状态,D表示不可中断睡眠状态,我们在前面刚学过,那后面的s和+是什么意思呢?不知道也没关系,查一下man ps就可以。现在记住,s表示这个进程是一个会话的领导进程,而+表示前台进程组。

这里又出现了两个新概念,进程组和会话。它们用来管理-组相互关联的进程,意思其实很好理解。

  • 进程组表示一组相互关联的进程,比如每个子进程都是父进程所在组的成员;

  • 而会话是指共享同一个控制终端的一个或多个进程组。

比如,我们通过 SSH 登录服务器,就会打开一个控制终端(TTY),这个控制终端就对应一个会话。而我们在终端中运行的命令以及它们的子进程,就构成了一个个的进程组,其中,在后台运行的命令,构成后台进程组;在前台运行的命令,构成前台进程组。

3.观察top

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
top - 10:22:38 up 19 min,  1 user,  load average: 126.13, 66.90, 27.96
Tasks: 243 total, 1 running, 186 sleeping, 0 stopped, 19 zombie
%Cpu0 : 0.7 us, 27.5 sy, 0.0 ni, 0.0 id, 71.6 wa, 0.0 hi, 0.0 si, 0.3 st
%Cpu1 : 0.3 us, 18.4 sy, 0.0 ni, 0.0 id, 80.6 wa, 0.0 hi, 0.3 si, 0.3 st
KiB Mem : 8156288 total, 120128 free, 7989652 used, 46508 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 3892 avail Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
41 root 20 0 0 0 0 S 20.6 0.0 0:04.66 kswapd0
5561 root 20 0 808712 17500 0 S 6.2 0.2 0:02.53 docker-containe
5538 root 20 0 1118256 35996 0 S 5.9 0.4 0:04.71 dockerd
9531 root 20 0 70052 55208 0 D 2.3 0.7 0:00.10 app
9477 root 20 0 70052 65504 0 D 1.6 0.8 0:00.10 app
9518 root 20 0 70052 63392 0 D 1.6 0.8 0:00.08 app
9290 root 20 0 9396 668 0 S 1.3 0.0 0:00.13 docker-containe
9400 root 20 0 70052 65504 0 D 1.3 0.8 0:00.12 app
9525 root 20 0 70052 57320 0 D 1.3 0.7 0:00.07 app
9542 root 20 0 70052 34876 0 D 1.3 0.4 0:00.06 app
256 root 19 -1 86664 4764 4076 S 0.7 0.1 0:00.55 systemd-journal
8 root 20 0 0 0 0 I 0.3 0.0 0:00.08 rcu_sched
25 root 20 0 0 0 0 S 0.3 0.0 0:00.01 oom_reaper
663 syslog 20 0 267268 1312 0 S 0.3 0.0 0:00.15 rsyslogd
1333 root 20 0 0 0 0 I 0.3 0.0 0:00.05 kworker/1:5
9187 root 20 0 0 0 0 I 0.3 0.0 0:00.02 kworker/0:0
9429 root 20 0 43440 796 0 R 0.3 0.0 0:01.44 top
9553 root 20 0 70052 32768 0 D 0.3 0.4 0:00.04 app
9560 root 20 0 70052 16400 0 D 0.3 0.2 0:00.01 app
9561 root 20 0 70052 18248 0 D 0.3 0.2 0:00.18 app
9564 root 20 0 70052 18248 0 D 0.3 0.2 0:00.01 app
1 root 20 0 159864 2468 4 S 0.0 0.0 0:02.70 systemd

发现1分钟负载已经到达126.13,而5分钟15分钟负载相对1分钟较低,但是也很高了,说明系统已经有了性能瓶颈。

tasks 这一栏 1个进程正在运行,186个睡眠,19个僵尸进程,且不断增多。 说明有子进程退出没有被清理。

在看cpu 用户cpu和系统cpu都不高,但是iowait 高达80.6 。

由此,可以很明确:

  • iowait 太高,导致负载升高。
  • 僵尸进程多,说明程序没有正确清理子进程资源。

如何解决

1.dstat 观察CPU 和 I/O 使用情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@linux:~# dstat 1 10 # 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
2 2 55 41 0| 48M 1252k| 0 0 | 0 0 | 491 922
0 2 0 98 0| 117M 0 | 132B 724B| 0 0 | 744 1349
0 1 0 98 0| 111M 0 | 66B 326B| 0 0 | 293 627
0 4 0 96 0| 134M 0 | 66B 342B| 0 0 |1246 1865
0 0 0 100 0| 109M 0 | 66B 342B| 0 0 | 360 746
0 2 0 98 0| 118M 200k| 66B 342B| 0 0 | 846 1471
0 1 0 99 0| 110M 0 | 132B 444B| 0 0 | 285 630
0 0 0 100 0| 108M 0 | 66B 350B| 0 0 | 258 616
0 1 0 99 0| 110M 0 | 66B 342B| 0 0 | 287 636
0 1 0 99 0| 110M 0 | 66B 342B| 0 0 | 283 675

发现每当iowait 升高 磁盘的读很大,那么到底是什么进程在读磁盘呢

我们从top 发现 僵尸进程很可疑,

1
2
3
4
5
6
9462 root      20   0   70052  65504      0 D   0.3  0.8   0:00.07 app
9490 root 20 0 70052 65504 0 D 0.3 0.8 0:00.07 app
9497 root 20 0 70052 65504 0 D 0.3 0.8 0:00.07 app
9503 root 20 0 70052 65504 0 D 0.3 0.8 0:00.07 app
9555 root 20 0 70052 65504 0 D 0.3 0.8 0:00.06 app
9557 root 20 0 70052 65504 0 D 0.3 0.8 0:00.18 app

使用pidstat 分析僵尸进程

1
2
3
4
5
6
7
8
root@linux:~# pidstat -d -p 9431 1 3 # -d 展示I/O 统计数据,-p 指定进程号 间隔1秒输出3组数据
Linux 4.15.0-46-generic (linux) 03/27/2019 _x86_64_ (2 CPU)

10:27:26 AM UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
10:27:27 AM 0 9431 0.00 0.00 0.00 121 app
10:27:28 AM 0 9431 0.00 0.00 0.00 89 app
10:27:29 AM 0 9431 0.00 0.00 0.00 99 app
Average: 0 9431 0.00 0.00 0.00 103 app

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

同样的方法分析其他僵尸进程,发现也都没有异常。

我们查看所有进程的 I/O 发现 果然是app 在捣鬼。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#pidstat -d 1 20 # 不指定进程号查看
10:30:13 AM UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
10:30:14 AM 0 759 552.58 0.00 0.00 1 sshd
10:30:14 AM 0 5538 263.92 0.00 0.00 0 dockerd
10:30:14 AM 0 5561 131.96 0.00 0.00 0 docker-containe
10:30:14 AM 0 9408 6334.02 0.00 0.00 96 app
10:30:14 AM 0 9412 4222.68 0.00 0.00 90 app
10:30:14 AM 0 9414 0.00 0.00 0.00 110 app
10:30:14 AM 0 9415 0.00 0.00 0.00 104 app
10:30:14 AM 0 9416 0.00 0.00 0.00 139 app
10:30:14 AM 0 9417 0.00 0.00 0.00 82 app
10:30:14 AM 0 9418 0.00 0.00 0.00 112 app
10:30:14 AM 0 9424 0.00 0.00 0.00 110 app
10:30:14 AM 0 9425 0.00 0.00 0.00 89 app
10:30:14 AM 0 9430 0.00 0.00 0.00 110 app
10:30:14 AM 0 9431 4222.68 0.00 0.00 102 app
10:30:14 AM 0 9432 0.00 0.00 0.00 103 app
10:30:14 AM 0 9433 0.00 0.00 0.00 112 app
10:30:14 AM 0 9434 0.00 0.00 0.00 110 app
10:30:14 AM 0 9435 4222.68 0.00 0.00 83 app
10:30:14 AM 0 9436 0.00 0.00 0.00 132 app
10:30:14 AM 0 9437 4222.68 0.00 0.00 89 app
10:30:14 AM 0 9439 0.00 0.00 0.00 110 app
10:30:14 AM 0 9440 0.00 0.00 0.00 118 app
10:30:14 AM 0 9441 0.00 0.00 0.00 112 app
10:30:14 AM 0 9442 0.00 0.00 0.00 103 app

不过,到底是 app 进程执行了什么导致 I/O 飙高呢?

strace

strace常用来跟踪进程执行时的系统调用和所接收的信号。在Linux世界,进程不能直接访问硬件设备,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等)时,必须由用户态模式切换至内核态模式,通过系统调用访问硬件设备。strace可以跟踪到一个进程产生的系统调用,包括参数,返回值,执行消耗的时间。

1
2
3
4
5
6
7
root@linux:~# strace -p 1675
strace: Process 1675 attached
read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 67108864) = 67108864
read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 67108864) = 67108864
read(3, "\0\0\n\0\0\0(\0\0\0\2\0?\373&\0\365\377\t\0\0\0\0\0\2\0\0\0\2\0\0\0"..., 67108864) = 67108864
read(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 67108864) = 67108864
read(3,

每一行都是一条系统调用,等号左边是系统调用的函数名及其参数,右边是该调用的返回值。我们可以看到该进程一直调用read函数。

但是如果是僵尸进程则此方法不管用。

所以可以通过perf 来查看进程的调用

1
2
root@linux:~# perf record -g  # 15秒后按 ctrl+c 结束
root@linux:~# perf report

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

观察该进程的源码,我们发现确实调用 O_DIRECT 对磁盘进行频繁读取

1
int fd = open(disk, O_RDONLY | O_DIRECT | O_LARGEFILE, 0755);

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

僵尸进程问题

对于僵尸进程,我们需要找到其父进程,然后从父进程哪里解决

1
2
3
4
5
6
7
root@linux:~# pstree -aps 1738
systemd,1
└─dockerd,1023 -H fd://
└─docker-containe,1112 --config /var/run/docker/containerd/containerd.toml --log-level info
└─docker-containe,1581 -namespace moby -workdir /var/lib/docker/containerd/daemon/io.containerd.runtime.v1.linux/moby/68e0799e0677a6bbe917bd8473b3d7a713d146836dffc905c12c00b30e729123 -address...
└─app,1608 -d /dev/vdc -s 67108864 -c 20
└─(app,1738)

运行完,你会发现 1738 号进程的父进程是 1608,也就是 app 应用。 所以,我们接着查看 app 应用程序的代码,看看子进程结束的处理是否正确,比如有没有调用 wait() 或 waitpid() ,抑或是,有没有注册 SIGCHLD 信号的处理函数。

1
2
3
4
5
6
7
8
9
10
11
int status = 0;
for (;;) {
for (int i = 0; i < 2; i++) {
if(fork()== 0) {
sub_process();
}
}
sleep(5);
}

while(wait(&status)>0);

发现

1
while(wait(&status)>0);

写在了for 循环外面,导致wait 函数实际上并没有被调用。