Dockerコンテナ内のJavaプログラムのシャットダウン処理
やりたいこと
Dockerコンテナ上で動作するJavaプログラムをdocker stop
で停止した時に、リソースの解放やその他の後始末に関する処理が実行されるようにしたい。
Javaプログラムにシグナル受信時の処理を追加する
最初にdocker stop
を実行したときの動作を確認しておこう。Dockerのコマンドライン・リファレンスには、以下のように記載されている。
コンテナ内のメイン・プロセスが
SIGTERM
を受信し、一定期間の経過後、SIGKILL
を送信します。
ということは、Javaプログラム側はRuntime#addShutdownHook
を使ってシャットダウンフックを追加し、シグナルを受信した時に呼び出されるようにすれば良さそう。早速サンプルを作って試してみよう。
public class Sample { public static void main(String[] args) throws Exception { Runtime.getRuntime().addShutdownHook(new Thread( () -> System.out.println("called ShutdownHook.") )); System.out.println("executing..."); try { Thread.sleep(60000); // 何もせず1分間待機 } catch (InterruptedException e) { e.printStackTrace(); } System.exit(0); } }
上記プログラムをコンパイルして単体で実行してみる。実行後、一定時間経過したらCtrl + c
を押す。
$ java Sample executing... ^Ccalled ShutdownHook.
今度はSIGTERM
を送ってみる。再度プログラムを実行する。
$ java Sample executing...
別ウィンドウからkill
コマンドを実行する。
$ kill -15 [PID]
シグナルを受信したJavaプログラムが、以下を出力して終了することを確認。
called ShutdownHook.
Dockerコンテナに入れて実行
Dockerfile
を準備する。
FROM openjdk:17 COPY ./Sample.class . CMD ["java", "Sample"]
イメージをビルド。
$ docker build -t shutdown-sample .
実行してみる。一定時間が経過したらCtrl + c
を押して停止。
$ docker run -it --rm --name shutdown-sample shutdown-sample executing... ^Ccalled ShutdownHook.
今度は-d
オプションを付けてデタッチドモードで実行する。(コンテナ停止後にdocker logs
を実行したいので、先ほど付けていた--rm
オプションは外しておく。)
$ docker run -it --name shutdown-sample -d shutdown-sample
docker stop
を実行する。
$ docker stop shutdown-sample shutdown-sample
docker logs
でログを確認。
$ docker logs shutdown-sample executing... called ShutdownHook.
ここまでは期待通り動いてくれた。
起動用スクリプトを追加してハマる
本番では起動用スクリプトからJavaプログラムを実行する。まあ結果は同じだろうけど、念のため確認しておこうか?と軽い気持ちで作業したら、これが結構ハマった。
startup.sh
という起動用スクリプトを用意する。
#!/bin/bash java Sample
Dockerfile
に追加。
FROM openjdk:17 COPY ./Sample.class . COPY ./startup.sh . RUN chmod +x startup.sh CMD ["./startup.sh"]
ビルドして実行。一定時間経過後にCtrl + c
を押す。
$ docker run -it --rm --name shutdown-sample shutdown-sample executing... ^Ccalled ShutdownHook.
ここまではOK。次は-d
を付けてデタッチドモードで実行する。
$ docker run -it --name shutdown-sample -d shutdown-sample
docker stop
を実行する。
$ docker stop shutdown-sample
10秒ほど時間がかかってコンテナが終了。これはなんだか怪しい挙動だ。docker logs
を覗いてみる。
$ docker logs shutdown-sample executing...
やはりSIGTERM
を受け付けていないようだ。終了するのに10秒ほどかかったのは、最終的にSIGKILL
が受信されて終了したいうことか。
原因と解決策
リファレンスをもう一度読むと、docker stop
がSIGTERM
を送信する対象はメインプロセスに対して、と記載されている。メインプロセスとは何か?それはPIDが1で実行されるプロセスとのことだ。 PID 1で実行しているのは?それはDockerfile
のCMD
に記載しているbin/bash startup.sh
だ。その中でjava Sample
を実行したら?それは別プロセスになるだろう!そりゃSIGTERM
が受信できる訳ないよ。何とも間抜けだな。
exec
を付けて同じプロセスでjava
を実行するようstartup.sh
を修正する。
#!/bin/bash exec java Sample
これでイメージを作り直して再実行。今度は期待通りの動作になった。一件落着。