ページ

2016年3月19日土曜日

Dockerと付き合う為の虎の巻 その03

 「Dockerと付き合う為の虎の巻 その03」です。今回は前回のDockerと起動するプログラムとPID1のお話しついてもうすこし掘り下げた回となります。内容的には主に前回の以下の項目についてのお話の続きです。


〇 Dockerコンテナは1つのコンテナに1つのプログラム(プロセス)を起動する事を基本として作成し、使用するものである

〇 Dockerコンテナで起動したプロセスはPID1(プロセスIDが1)のプロセスとして起動される

〇 Dockerコンテナの問題の無い通常の停止(docker stop 等)はPID1に対するSIGTERMシグナルによって行われる。

〇 Dockerコンテナでは基本的にログ出力は使用せずに標準出力にそのまま内容を出力する



 前回のお話しにもありましたが、Dockerにおいてといいますか、LinuxにおいてPID1のプロセスは特別な意味を持っています。一見では単純に Dockerコンテナとして起動したいプロセスをENTRYPOINTとして指定するだけでよさそうに見えるのに実はそうではないという所は大きな落とし穴と言いますか、初見殺しの罠となっているような気がしますので色々捕捉をしていこうかと思います。


 とりあえずはexampleといたしまして、redis君に登場していただきつつ、以下のようなDockerfileにてバッドケースを実演してみる事にします。(最近この手のexampleでやたらとコキ使われている気がしますねredis君)

FROM centos:6

RUN yum -y install epel-release
RUN yum -y install redis cronie

ENTRYPOINT ["/usr/sbin/redis-server"]
CMD []

 内容的には単純にCentOSの6をベースにyumでredisとcronie(crond)をインストールした後に、ENTRYPOINTとしてredisを指定して起動しているものです。cronは後で実験をする為の素材として入れてありますので似たようなものであれば別になんでも良いです。設定ファイルの指定もしていないのでredisとしてまともに機能する訳ではないのですが、今回はそちらは主目的ではないのでとりあえず気にしない事としておきます。


 とりあえず前述のDockerfileをビルドしつつ。
# docker build -t example01 .

 起動を行ってみます。
# docker run -itd --name=example01 example01
# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
1d7a425f2aca        example01           "/usr/sbin/redis-serv"   6 seconds ago       Up 5 seconds                            example01

 無事起動いたしました。とりあえずこのままこのコンテナの中に入ります。

# docker exec -it example01 bash

 さて、この状態でpsコマンドでプロセスの状態を確認してみます。

# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 15:52 ?        00:00:00 /usr/sbin/redis-server
root        11     0  0 15:54 ?        00:00:00 bash
root        26    11  0 15:56 ?        00:00:00 ps -ef

 ENTRYTPOINTで指定したredis-serverがPID1として実行されているのが分かります。bashというのはexecで実行した今この動作しているbashの事です。色々不自然ですが取り敢えずは置いておきましょう。


 さて、ここまでは普通ぽい動作といいますか、想定通りとなる訳ですが、ここで一つバッドな事を実行してみます。最初のDockerfileにて事前にcronieをインストールしていますので、cronと関連するinitscripts等が事前に入っている状態となります。さっそくそいつを起動してみましょう。

# service crond start
 Starting crond:                                            [  OK  ]
# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 15:52 ?        00:00:00 /usr/sbin/redis-server
root        11     0  0 15:54 ?        00:00:00 bash
root        47     1  0 16:01 ?        00:00:00 crond
root        49    11  0 16:02 ?        00:00:00 ps -ef

 どうやらうまくcrondが起動したようです。しかし、PIDがちょっと不自然な状況にも見えます。とりあえず分かりやすくする為に今起動したcrondを停止してみます。

# service crond stop
Stopping crond:                                            [FAILED]

 画面出力をみるとどうやら停止に失敗したような出力がなされました。とりあえずpsコマンドで状況を確認してみます。
# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 15:52 ?        00:00:00 /usr/sbin/redis-server
root        11     0  0 15:54 ?        00:00:00 bash
root        47     1  0 16:01 ?        00:00:00 [crond] <defunct>
root        82    11  0 16:09 ?        00:00:00 ps -ef

(=゚ω゚) !?

 なんと、crondプロセスが<defunct>という表示で残ってしまっています。これはいわゆるゾンビプロセスとよばれる状態になってしまっている事を表しています。色々ダメな状態ですね。


 なぜこのような状態になってしまったのでしょうか。色々と細かい事情が複合してはいますが、簡単に一言で言えば「ENTRYPOINTでPID1として起動させたredis-serverがLinux的なPID1プロセスとして必要な役割を果たしていないから」という事になります。

 今回のケースはちょっとしたバッドケースなのですが、「聞いていた話だけだと一見問題なさそうなはずなのに結構簡単に問題が出てしまったよ」という身近な例として少し長めに説明を割いてみましたがいかがでしょうか?


 さて、それではPID1の役割とは一体何なのかという話になります。自分の場合も完全には調べ切れていないのですが、少なくとも今回のようなケースでは以下の役割が足りていない為に起きているようです。(※下記の文章は公式文書の引用等ではありませんので、もし間違っている部分がありましたら申し訳ございません)




◯ Linux起動時の初期プロセスは全てPID1から起動しており、その他の子プロセスや孫プロセス等も親プロセスをたどっていけばPID1に辿り着くツリー構造のような形態をとる。つまりPID1はプロセスツリー的に全てのプロセスの頂点となるプロセスである。 

◯ 通常linuxのプロセスは終了した後に親プロセスによって終了状態の回収を待つ状態になる。プロセスは親プロセスに終了状態を回収される事により真にプロセスのライフサイクルが終了する。この終了状態の回収が行われずに放置された状態となる事をいわゆるゾンビプロセスという。(※これはPID1でなくてもプロセス共通です)

◯ 親プロセスが居なくなったプロセスの親プロセスは親プロセスの親プロセスに繋ぎ変えられる事になる。最終的には必ず存在するプロセスツリーの頂点で あるPID1のプロセスが親プロセスとなる。例えば意図的に親プロセスを終了させて孤立状態となるデーモンプロセス等のケースが存在するが、このような機構によりデーモンプロセスの親プロセスはPID1のプロセスが担うこととなる。


 以上、簡潔に?ですがPID1の役割を説明してみました。


 つまり、今回のケースでは、crondが起動した際にその親プロセスが終了しているが、PID1が正しく終了状態を回収していない為、親プロセスの継ぎ換えが正しく行われずに切り離された状態となってしまった。そしてその状態でcrondを終了させた事により、実行もされておらず、終了状態の回収もされておらず、親プロセスも存在しないという完全なゾンビプロセス状態となってしまった。

 というのが現象発生の流れという事になるかと思います。



 なんか文章ばかりで疲れてしまいましたが、こうしてみますと、redisは別にdocker用途としてPID1で動くように作られている訳ではありませんので(そういったオプション設定が用意されている可能性もありますが)このような状態になってしまうのはむしろ当然の出来事であるというのが分かります。

 この問題は自作プログラムをdockerコンテナとしたい場合にも直面する問題です。その対象の自作プログラムは本来の動作以外にもPID1として動作を実装しなければならないという事になってしまいます。色々と面倒くさい事になって来てしまいました。(=゚ω゚) エー ミタイナ...

  このPID1としてのプロセス回収の役割ですが、自作のプログラムの場合、基本的にはLinuxのシグナルであるSIGCHLDに対して適切な処理を行う事が必要となって来ます。C言語等でサーバプログラムを作った事がある方ならご存知かと思いますが、親プロセスによる終了状態の回収というやつはSIGCHLDにて行うという手法が一般的に存在しています。PID1の場合はそれがLinuxシステム全体のプロセスの親としてSIGCHLDの処理が任されていると考えれば問題なさそうです。(状況次第ではSIGCHILDにSIG_IGNをセットするだけで事足りる場合もあるかもです)


 なお、今回のredisでは子プロセスの回収処理を行っていない事により問題が発生しておりましたが、昨今のdockerの流行等による影響もあり、最近のソフトウェアではdockerコンテナのPID1として動作させる為の対応を盛り込んできている物もみられるようになって来ております。

 具体的には本ブログの「「Consulを頑張って理解する」 を公開してみました(ノ=゚ω゚)ノ」
の回や「ConsulのDockerコンテナを作成&公開してみました」の回で紹介しました、HashiCorp社の「consul」等もその一つです。consulは元々はdockerコンテナ内部で動作させるものではなかったかと思いますが、dockerの隆盛の流れに乗って進化してきたという経緯がある為か、PID1で動作する場合には子プロセスの回収を行う仕組みが働くようになっています。また、同じくHashiCorp社の「nomad等のソフトウェアでも同様の仕組みが組み込まれています。

 また、今回のPID1問題の解決策として前回すでにsupervisordを使えば問題ありません」と書いてあったりするのですが、もちろんsupervisordにも同様の機構が組み込まれていますのでDockerコンテナ内部のプロセス管理にもってこいという訳であります。

 なお、このような子プロセスの回収の仕組みは主に「reap」という名前で表現されるようです。なにか既存のソフトウェアをdockerコンテナ上で動作させる際に、PID1の動作に対応しているかどうかを確認する際には、対象ソフトウェアのドキュメントや設定ファイル中に「reap」という文字が存在するかどうかを調べてみてください。メジャーなソフトウェアで公式のdockerコンテナを出しているようなソフトウェアに「reap」という記述がありましたら、この子プロセス回収の機構の事でまず間違いありません。(=゚ω゚) タブン


 さてはて、今回色々と説明が長かったのですが、結論としては「SIGCHLDを自分で色々なんとかする」か「supervisordを使う」のどちらかという事になりますでしょうか。

 実際の所、1つのコンテナで1プロセスが動くのみの単純な物であるのならば、そもそも管理する別のプロセスが存在しない事になりますので、PID1としての子プロセスの回収等は行う必要自体が無い事になるのですが、内部で子プロセスを作成したりするデーモン的なソフトウェア等を動作させようとしたりすると、結構この部分で引っかかってしまう事もあるようです。

 次回は一応この問題に対しての解決案の一つ提示してみようかなと考えています。次回でこの問題についての話題は最後にしたいですね!なんか字ばっかりだし!難しそうな説明になるし!(=゚ω゚) テガツカレマシタ


0 件のコメント:

コメントを投稿