ブログに書くつもりじゃなかった

フリーのプログラマーが綴る、裏チラ系の備忘録や雑記帳。

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 stopSIGTERMを送信する対象はメインプロセスに対して、と記載されている。メインプロセスとは何か?それはPIDが1で実行されるプロセスとのことだ。 PID 1で実行しているのは?それはDockerfileCMDに記載しているbin/bash startup.shだ。その中でjava Sampleを実行したら?それは別プロセスになるだろう!そりゃSIGTERMが受信できる訳ないよ。何とも間抜けだな。

execを付けて同じプロセスでjavaを実行するようstartup.shを修正する。

#!/bin/bash
exec java Sample

これでイメージを作り直して再実行。今度は期待通りの動作になった。一件落着。