コンテナ内で大量のゾンビプロセスが発生していた話

# ゾンビプロセスの大量発生

シェルを使ってとある処理を実行している最中に大量のゾンビプロセスが発生していることに気づきました。
以下はその時のtopコマンドの出力で、S列が「Z」と表示されているものが全てゾンビプロセスです。

これまでゾンビプロセスの発生を全く考えたことが無かったのですが、コンテナ環境の場合だといわゆる「PID 1 問題」の1つとして認識されています。

```
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
    1 root      20   0 2288244 310936  19076 S   0.0  15.3   1:24.64 java
   23 root      20   0   18480   2272   1688 S   0.0   0.1   0:00.32 bash
14538 root      20   0       0      0      0 Z   0.0   0.0   0:00.00 bash
14542 root      20   0       0      0      0 Z   0.0   0.0   0:00.00 bash
14544 root      20   0       0      0      0 Z   0.0   0.0   0:00.00 bash
14549 root      20   0       0      0      0 Z   0.0   0.0   0:00.00 bash
14554 root      20   0       0      0      0 Z   0.0   0.0   0:00.00 bash
14557 root      20   0       0      0      0 Z   0.0   0.0   0:00.00 bash
14561 root      20   0       0      0      0 Z   0.0   0.0   0:00.00 bash
14565 root      20   0       0      0      0 Z   0.0   0.0   0:00.00 bash
14568 root      20   0       0      0      0 Z   0.0   0.0   0:00.00 bash
14582 root      20   0       0      0      0 Z   0.0   0.0   0:00.00 bash
14587 root      20   0       0      0      0 Z   0.0   0.0   0:00.00 bash
14595 root      20   0       0      0      0 Z   0.0   0.0   0:00.00 bash
14596 root      20   0       0      0      0 Z   0.0   0.0   0:00.00 bash
14599 root      20   0       0      0      0 Z   0.0   0.0   0:00.00 bash
14629 root      20   0       0      0      0 Z   0.0   0.0   0:00.00 bash
14630 root      20   0       0      0      0 Z   0.0   0.0   0:00.00 bash
14636 root      20   0       0      0      0 Z   0.0   0.0   0:00.00 bash
14637 root      20   0       0      0      0 Z   0.0   0.0   0:00.00 bash
~~以下略~~
```

# PID 1 問題とは

Linuxで使用されているプロセスを一意に識別するためのプロセスID（PID）ですが、PID 0 と 1 は特別扱いされます。
コンテナではENTRYPOINTやCMDで指定したプロセスがPID=1に割り当てられ、それらのプロセスがLinuxのシグナルを正しくトラップしない問題が「PID 1 問題」とされます。
ゾンビプロセスの大量発生もこの問題に関連しています。

Wikipediaの「プロセス識別子」のページではPIDについて下記のように説明がされています。

> Unix系OSでは、プロセス識別子 0 と 1 は特別なタスクを指している。プロセス識別子 0 は swapper または sched と呼ばれ、ページングを担当している。これは実はカーネルの一部であり、ユーザーモードのプロセスではない。プロセス識別子 1 は init プロセスで、主にシステムの立ち上げとシャットダウンを担当している。
>
> https://ja.wikipedia.org/wiki/プロセス識別子

Linuxのシグナルについてはmanページをご参照ください。

https://man7.org/linux/man-pages/man7/signal.7.html

# ゾンビプロセスについて知る

## ゾンビプロセスとは

ゾンビプロセスとは
<font color="OrangeRed">**実行が終了したのにも関わらず、プロセステーブル上にエントリだけが残っているゴミプロセス**</font>
のことです。
プロセステーブル上に情報として残っているだけのため、実態として何らかの処理が存在しているわけではありません。

ゾンビプロセスは下記のような特徴を持ちます。

- CPUリソースは使用しない
- メモリリソースは消費しない
- PIDを消費する（PIDの数はOSで上限がある）
- killコマンドで消せない（親プロセスをkillする必要がある）

従って、PIDを消費する以外では基本的に害はありません。

## ゾンビプロセスと孤児プロセス

「ゾンビプロセス」と似た意味で使われる用語として「孤児プロセス」があります。
これらは区別されないことも多いですが、下記のような違いがあります。

|  | ゾンビプロセス | 孤児プロセス |
|:-:|:-:|:-:|
| 実行状態 | 終了済 | 実行中 |
| プロセスの実体 | なし | あり |
| 発生は意図的か | 多くの場合意図的でない | 意図的であることも意図的でないこともある |

## ゾンビプロセス発生の仕組み

ゾンビプロセスが生成される過程は以下のようになります。
LinuxのプロセスはPID=1のプロセスを起点としてツリー状になっています。
親であるプロセスが子プロセスの面倒を見なければなりません。

コンテナではないLinux環境であればPID=1のプロセスは`init`プロセスになっています。

下図の例では、bashを起動してそこからcurlプロセスを複数起動しています。
親であるbashプロセスがプロセステーブルにcurlプロセスの情報を登録します。
`PPID`は親プロセスのPIDを表しています。

![fig1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705474/7fef1cb3-5afb-6d09-9208-9a1b92a8f1b7.jpeg)

ここでcurlのプロセスが終了する前にbashプロセスが終了するとどうなるでしょうか。
この場合、子プロセスであったcurlは、PID=1であるinitプロセスの子になります。

![fig2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705474/54ab6870-78c3-cbd1-75b7-e04540e3fddb.jpeg)

curlプロセス終了後はinitプロセスが、プロセスの情報をプロセステーブルから削除します。
コンテナではないLinux環境では、PID=1のプロセスはinitであり子プロセスを適切に管理するため、通常ゾンビプロセスが発生することはありません。
（実際には終了直後のプロセスは全てゾンビ状態になるが、親プロセスにより直ちにプロセステーブルから削除されるためゾンビ状態を確認するのは難しい）

![fig3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705474/1658a258-835c-ea9b-8ebf-64480b9d774f.jpeg)

これがコンテナ環境では以下のようになります。
PID=1はinitではなく、コンテナで実行するメインのプロセス（例えばnginxやmysqlなど）になります。
これらのプロセスはほとんどの場合、ゾンビ状態となった子プロセスを掃除するという機能を持っていません。
そのためプロセステーブル上に実体のないゾンビプロセスが残り続けることになります。

![fig4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705474/e818d537-9a89-5659-71c7-e6bc3d7ee768.jpeg)

# コンテナ環境におけるゾンビプロセスの検証

ここでは実際にコンテナ環境でゾンビプロセスを発生させ、その対処方法についてみてみます。

## ゾンビプロセスが発生（PID=1がinitではない適当なコマンド）

まずコンテナとして一般的によく使われる状態で検証してみます。
PID=1はinitではない別のプロセスで起動します。

ここではCentOSのイメージを使用します。
まずはコンテナを起動します。

```:コンテナの起動
$  docker run -itd --name zombie-test centos:7 sleep 3600
```

コンテナへログインしてプロセスを確認します。
ここではPID=1はsleepコマンドとしていますが、nginxやmysqlなどでも同様です。

```:プロセスの確認
$ docker exec -it zombie-test bash
[root@ccdc87dd344c /]# ps -eo  pid,ppid,pgid,stat,command
    PID    PPID    PGID STAT COMMAND
      1       0       1 Ss+  sleep 3600
      6       0       6 Ss   bash
     19       6      19 R+   ps -eo pid,ppid,pgid,stat,command
```

ここで、下記コマンドによりゾンビプロセスを発生させます。
コマンド実行後2・3秒後にCtrl+Cでプロセスを中断させるとゾンビプロセスが生成されます。

```:ゾンビプロセスの生成
[root@ccdc87dd344c /]# seq 0 50 | xargs -n 1 -P 5 sleep 1
^C
[root@ccdc87dd344c /]#
```

再度プロセスを確認してみます。
STATの項目がZとなっているものがゾンビプロセスです。
親プロセスのPIDであるPPIDが1の状態で、ゾンビプロセスが残っていることを確認できます。

```
[root@ccdc87dd344c /]# ps -eo  pid,ppid,pgid,stat,command
    PID    PPID    PGID STAT COMMAND
      1       0       1 Ss+  sleep 3600
      6       0       6 Ss   bash
     25       1      20 Z    [sleep] <defunct>
     26       1      20 Z    [sleep] <defunct>
     27       1      20 Z    [sleep] <defunct>
     28       1      20 Z    [sleep] <defunct>
     29       1      20 Z    [sleep] <defunct>
     30       6      30 R+   ps -eo pid,ppid,pgid,stat,command
```

## ゾンビプロセスが発生（PID=1がinit）

つづいてPID=1にinitを指定して確認してみます。

```:コンテナの起動
$ docker run -itd --name zombie-test centos:7 init
```

PID=1はinitプロセスになっています。

```:プロセスの確認
$ docker exec -it zombie-test bash
[root@db36af7005bf /]# ps -eo  pid,ppid,pgid,stat,command
    PID    PPID    PGID STAT COMMAND
      1       0       1 Ss+  init
      6       0       6 Ss   bash
     19       6      19 R+   ps -eo pid,ppid,pgid,stat,command
```

先ほどと同様にゾンビプロセスを発生させます。

```:ゾンビプロセスの生成
[root@db36af7005bf /]# seq 0 50 | xargs -n 1 -P 5 sleep 1
^C
[root@db36af7005bf /]#
```

ゾンビプロセス発生後に確認すると、先ほど同様ゾンビプロセスは残っています。
ゾンビプロセスはinitプロセスが掃除しますが、Linuxに含まれているinit(/sbin/init)を指定してもゾンビプロセスは掃除してくれません。
そのため別のinitプロセスを使用する必要があります。

```:ゾンビプロセスの確認
[root@db36af7005bf /]# ps -eo  pid,ppid,pgid,stat,command
    PID    PPID    PGID STAT COMMAND
      1       0       1 Ss+  init
      6       0       6 Ss   bash
     31       1      25 Z    [sleep] <defunct>
     32       1      25 Z    [sleep] <defunct>
     33       1      25 Z    [sleep] <defunct>
     34       1      25 Z    [sleep] <defunct>
     35       1      25 Z    [sleep] <defunct>
     36       6      36 R+   ps -eo pid,ppid,pgid,stat,command
```

# ゾンビプロセスを発生させない対策

## --initオプションを使ってコンテナを起動

Dockerではinitオプションを使用することでゾンビプロセスの発生を抑止できます。
--initオプションについて公式ドキュメントでは下記のように説明があります。

> init プロセスの指定
--init プラグを使うと、コンテナ内で PID 1 として使われるべき init プロセスを指定できます。init プロセスの指定とは、ゾンビプロセスの回収のような処理を作成したコンテナ内で行い、init システムにおける通常の処理を確実に担います。
>
>デフォルトの init プロセスには、Docker デーモンプロセスのシステムパス上で、最初に実行可能な docker-init を使います。この docker-init バイナリは、デフォルトのインストールに含まれており、 tiny の支援を受けています。
>
> https://docs.docker.jp/engine/reference/run.html#init

実際に`--init`オプションを使用してコンテナを起動してみます。

```:コンテナの起動
$ docker run -itd --init --name zombie-test centos:7
```

コンテナで実行されているプロセスを確認すると、PID=1のプロセスは`/sbin/docker-init`となっています。
こちらが`--init`オプションを追加した際にデフォルトで使用されるdocker-initバイナリです。

```:プロセスの確認
$  docker exec -it zombie-test bash
[root@a6810cb3b725 /]# ps -eo  pid,ppid,pgid,stat,command
    PID    PPID    PGID STAT COMMAND
      1       0       1 Ss   /sbin/docker-init -- /bin/bash
      6       1       6 S+   /bin/bash
     15       0      15 Ss   bash
     28      15      28 R+   ps -eo pid,ppid,pgid,stat,command
```

それでは実際にゾンビプロセスを発生させてみます。
下記コマンドを実行した2・3秒後にCtrl+Cでプロセスを中断させます。

```:ゾンビプロセスの生成
[root@a6810cb3b725 /]# seq 0 50 | xargs -n 1 -P 5 sleep 1
^C
[root@a6810cb3b725 /]#
```

プロセスを確認するとゾンビプロセスが発生していないことを確認できます。
これは、PID=1である`/sbin/docker-init`のプロセスがゾンビプロセスを掃除したからです。

```:ゾンビプロセスの確認
[root@a6810cb3b725 /]# ps -eo  pid,ppid,pgid,stat,command
    PID    PPID    PGID STAT COMMAND
      1       0       1 Ss   /sbin/docker-init -- /bin/bash
      6       1       6 S+   /bin/bash
     15       0      15 Ss   bash
     38      15      38 R+   ps -eo pid,ppid,pgid,stat,command
```

このように、PID=1で起動するプロセスを適切なものに設定することでゾンビプロセスの発生を防ぐことができます。

# 参考資料
- [docs.docker.jp](https://docs.docker.jp/engine/reference/run.html#init)
- [Docker and the PID 1 zombie reaping problem](https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/)
- [An Init System inside the Docker Container](https://medium.com/@BeNitinAgarwal/an-init-system-inside-the-docker-container-3821ee233f4b)
- [docker --init (Tini) が PID 1 問題を解決している様子を調べてみた](https://qiita.com/ko1nksm/items/e8c2fbf58687e6979448)
- [Dockerコンテナに使える「正しい」軽量init process(PID 1)まとめ](https://qiita.com/mumoshu/items/064cd93ce116d8e04169)