Dockerでsupervisorを使う時によくハマる点まとめ

morimorihoge です。最近格ゲー部に入部してGGXrd REV 2を始めました。高校時代にGGXをやっていたときと感覚が違いすぎてまだ全然ですが、チマチマ練習中です。

Dockerにはinitプロセスがないよ問題とsupervisor

さて、Dockerコンテナはそのままではsystemdやsysvstat経由で起動を管理するサービス(nginxとかpostgresqlとかの永続系サービス)がうまく動きません。
これはDockerの設計思想によりinitプロセスがいないことが原因で、この辺りは Docker and the PID 1 zombie reaping problem にいい感じでまとまっています。

もしsystemdが使いたければDockerではなくsystemd-nspawnを使うのが楽でしょう、という話になるのですが、Dockerでinitプロセスを扱う試みもあるので、Dockerをできるかぎり軽量VMとして透過的に利用したいということなら以下のサイトなどが参考になりそうです。

では世の中のDockerユーザはこのinitプロセスがない問題をどう解決しているかというと、 supervisor というスーパーバイザ機能のみを提供する軽量プログラムを利用するのが一般的に思えます。Ubuntu系containerであれば apt-get install supervisor でインストールできます。
この辺りは以前社内勉強会で話したので別記事で解説しようと思います。

なお、本記事のDocker環境は普通のUbuntu 16.04LTSコンテナで、aptでパッケージインストールしたものです。

Dockerにおけるよくあるsupervisor設定のミス

supervisor は非常に簡易な設定でプロセス起動管理ができるのでありがたいのですが、注意しないとハマります。

[supervisord] セクションで nodaemon=true にする

これをtrueにしないとdaemonモードで起動します(こちらがデフォルト)。
daemonモードで起動してしまうと、Container立ち上げ時にすぐプロセスを手放してしまいContainerが終了します。
supervisorを設定したけどいざdocker runしたら一瞬でcontainerが止まってしまったという場合は大体これが原因です

[supervisord]
nodaemon = true

参考: supervisordセクションの公式ドキュメント

[program:x] セクションで autorestart を使う場合には注意する

[program:#{PROGRAM_NAME}] セクションには command で実行したいコマンドを書ける上に autorestart という便利そうな設定があります。
これは当該プロセスが落ちていた場合に自動的に再起動してくれるオプションなのですが、 気軽に使うと死にます

もし commandservice コマンド経由や /etc/init.d 以下のスクリプト経由でサービス起動するような設定を書いていて autorestart を有効にした場合、 延々とプログラムが再起動し続けます
外部から見ると繋がったり繋がらなかったりしてチャタリングしているように見えるので、まともなサービスはできません。気をつけましょう。

とりあえず、あまり深い理解をせずに気軽に使いたければ、 autorestartは使わず、autostartだけ設定し、再起動が必要になったらserviceコマンドやinitスクリプトで手動立ち上げするか、またはcontainerごとrestartする(supervisordが立ち上がり直すのでautostartが起動する) ことをオススメします。詳細は後述。

supervisorの設定ファイルを弄ったら reread update restart

supervisorの設定は通常 /etc/supervisor にあり、こちらの設定を弄ることになりますが、この設定はsupervisor実行中には動的に読み込まれません。
設定更新時にsupervisorプロセス自体を再起動しても良いのですが、Docker Containerの起動時にsupervisordをrunプロセスに指定していた場合、supervisorが死ぬのは困ります。

というわけで、supervisordを落とさずに設定を読み込むのは以下の手順です。

supervisorctl reread
supervisorctl update
supervisorctl restart #{新しく追加したサービス}

reread すると設定に変更があれば標準出力に表示されますが、この状態ではまだ反映されていないので、 update で反映します。その後、動作中のサービスであれば restart することで新しい設定で動作するようになります。

※ぐぐった中では reread restart するだけで良いという記事が多々ありましたが、手元の環境(supervisor 3.2.0) では update の必要がありました。

autorestartチャタリング問題についてもう少し詳しく

autostartは使うな、だけだとあんまりなので、もう少し詳しく解説します。

autorestartが正しく動く条件として、supervisorが正しく監視対象のプロセスを捕捉できている必要があります。今現在supervisorが監視対象のプロセスを捕捉できているかは、 supervisorctl status で調べることができます。

# supervisorctl status
nginx                            EXITED    May 29 07:11 AM
postgresql                       RUNNING   pid 13398, uptime 0:25:43
sshd                             RUNNING   pid 8, uptime 3 days, 14:37:22

この結果を見ると、Nginxは落ちていてPostgreSQLとSSHdが起動しているように見えます。
ps コマンドでも確認してみましょう。

# ps ax
    8 ?        S      0:00 /usr/sbin/sshd -D
 7382 ?        Ss     0:00 nginx: master process /usr/sbin/nginx
 7383 ?        S      0:10 nginx: worker process
13398 ?        S      0:00 /usr/lib/postgresql/9.6/bin/postgres -D /var/lib/postgresql/9.6/main -c config_file=/etc
※(抜粋)

あれ? Nginxは起動しています ね。これはどういうことでしょうか?

supervisorの設定を見てみると、

[program:nginx]
command=service nginx start

となっています。 service コマンド経由、つまり/etc/init.d以下のスクリプトを経由してサービス起動をしていますね。
この設定は正しく動きますし、初回起動させるだけであれば問題ありません
supervisorのデフォルトでは autostart パラメータはONになっているため、これだけ設定してあればsupervisor起動時にnginxが起動します。やったね!

supervisorctlコマンドでNginxがEXITEDした理由

ではなぜ supervisorctl コマンドではNginxが EXITED ステータスになってしまったのでしょうか?

これは command 指定が service nginx start になってしまっているためです。
supervisorの command 設定には監視対象のプログラムそのものの実行パスを書く必要があり、serviceや/etc/init.d以下の起動スクリプトはそれ自身はサービス起動後に終了してしまうため、supervisorがサービス自身を捕捉できないのだと思います。supervisorからしてみれば、 service コマンドのプロセス自体は実行後即終了するので、 EXITED なわけですね。

対応方法

ではどうすればいいかというと、nginxであればnginxを起動する /usr/sbin/nginx を直接記述すれば良いわけです。

[program:nginx]
command=/usr/sbin/nginx

なお、systemdやsysvinit向けのnginx設定の場合、 これでもまだちゃんと動きません
nginxはデフォルトではdaemonとして起動するため、supervisorがnginxプロセスを立ち上げてもsupervisorの管理下から外れてしまいます。
この辺りについては supervisorのドキュメント にも書かれており、
supervisorで管理したいプロセスはdaemon化せず、フォアグラウンドプロセスとして起動する様に設定する必要がありますyamasita コメントありがとう :))。

# supervisorctl status
nginx                            EXITED    Jun 01 11:00 PM

ではどうすればいいかというと、deamonモードで動かさなければ良いので、

[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"

とします。
これで、nginxプロセスがsupervisorの管理下に置かれるようになり、

# supervisorctl status
nginx                            RUNNING   pid 13816, uptime 0:04:37

とちゃんと見えるようになりました。

まとめ

supervisorは軽量スーパーバイザとして便利で、Dockerと合わせて使うテンプレートが世にたくさんあります。設定も簡単で、それほどLinuxサーバ管理に長けていなくても割とフィーリングで設定すればそれなりに動いてくれるという点でお手軽なのはありがたいツールです。
しかし、それでもちょいちょい落とし穴はあるので、軽量VMとしてDockerを使う場合にはある程度ノウハウを貯めていく必要があると思います。

ただ、僕のオススメはsupervisorで管理すると決めたら特定プロセスが止まった場合にはsshして手動でごそごそせずに、 container&supervisorごとrestartする運用 です。
最初に動かしたいプロセスをsupervisorの設定に書く手間がありますが、そもそもスーパーバイザを使うなら起動設定は全てそこに書くべきですし、 containerは起動が軽い というメリットを最大限活用するためにも気軽にrestartするのが良いと思います。

弊社でもDockerを本格運用し始めて2年くらい経ちましたが、そろそろ複数Docker Hostの管理が辛くなってきたので最近はKubernetesAmazon EC2 Container Service辺りを勉強中です。
アプリケーション側の総portable container化についてはまだネットワークとVolumeどうするの問題及びトラシューどうしようといった問題があり、なかなか本番系では提案するノウハウが足りてないですが、この辺もおさえていきたいところです。

関連記事(Docker)


Ruby on RailsによるWEBシステム開発、Android/iPhoneアプリ開発、電子書籍配信のことならお任せください この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

morimorihoge

高校卒業後,学生をやりながらずっとWebアプリ開発に携わってきました.2010くらいまではPHP/Symfonyプログラマでしたが,それ以降のWeb開発はRailsほぼ一本に宗旨替えしました.開発とは別にサーバ構築・運用も10年以上やってきているので,要件定義から設計・実装・環境構築・運用まで一通り何でもこなせます.開発以外では季節により大学でWebサービス開発やプログラミング関連の非常勤講師もしており,技術の啓蒙・教育にも積極的に関わっています.最近はPM的な仕事が増えていますが,現役開発者としていつでも動ける程度にはコードもサーバも弄る日々を送っています.AWS 認定ソリューションアーキテクト – アソシエイトレベル取りました

morimorihogeの書いた記事

週刊Railsウォッチ

インフラ

BigBinary記事より

ActiveSupport探訪シリーズ