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
...

这里发现四个可疑的地方:

  1. 第一行的平均负载(Load Average)

过去1分钟、5分钟和15分钟内的平均负载在依次减小,说明平均负载正在升高
而1分钟内的平均负载已经达到系统的CPU个数,说明系统很可能已经有了性能瓶颈

  1. 第二行的 Tasks

有1个正在运行的进程,但僵尸进程比较多,而且还在不停增加,说明有子进程在退出时没被清理

  1. CPU使用率

查看两个CPU 的使用率情况,用户CPU和系统CPU 都不高,但iowait分别是60.5%和94.6%,有点儿不正常

  1. 进程情况

每个进程的情况,CPU使用率最高的进程只有0.3%,看起来并不高
但有两个进程处于D状态,它们可能在等待I/O,但光凭这里并不能确定是它们导致了iowait升高

把这四个问题再汇总一下,就可以得到很明确的两点:

  1. iowait 太高,导致系统的平均负载升高,甚至达到了系统 CPU 的个数
  2. 僵尸进程在不断增多,说明有程序没能正确清理子进程的资源

接下来,顺着这两个问题继续分析,找出根源

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_readblkdev_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应用程序的代码,看看子进程结束的处理是否正确,比如:

  1. 有没有调用wait()waitpid()
  2. 有没有注册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升高时,需要先用dstatpidstat等工具,确认是不是磁盘I/O的问题,然后再找是哪些进程导致了I/O

等待I/O的进程一般是不可中断状态,所以用ps命令找到的D状态(即不可中断状态)的进程,多为可疑进程
但这个案例中,在I/O操作后,进程又变成了僵尸进程,所以不能用strace直接分析这个进程的系统调用

这种情况下,用perf工具来分析系统的CPU时钟事件,最终发现是直接I/O导致的问题,再检查源码中对应位置的问题

而僵尸进程的问题相对容易排查,使用pstree找出父进程后,去查看父进程的代码,检查wait()/waitpid()的调用,或是SIGCHLD信号处理函数的注册

以上是关于linux性能优化不可中断进程和僵尸进程的问题的主要内容,如果未能解决你的问题,请参考以下文章

linux性能优化软中断的理解及问题定位

Linux性能优化实战:负载均衡与CPU使用率(01)

linux性能优化动态追踪

linux 性能优化之路: 什么是平均负载, 如何判断是哪种负载过高(cpu密集, io密集, 大量进程)

linux 性能优化之路: 什么是平均负载, 如何判断是哪种负载过高(cpu密集, io密集, 大量进程)

这些Linux 系统监控命令你都用过吗?