コンテナ終了シグナルとSTOPSIGNAL

まず、コンテナはどのような状態になると停止するのかは、プロセスID1の終了でコンテナ停止へ。また、プロセスIDが1になるのはシェルかコマンドかも。

ここでのポイントは、docker stopでコンテナを停止すると、動いていたプログラムはどのように終了するのか。

  1. 実行中のプロセスに対してシグナル(信号)を送る
  2. シグナルを検知して何か処理
  3. コンテナでSIGTERMシグナルをtrapしてみる例
  4. docker stopでプロセスID1はSIGTERMシグナルを受け取る
  5. trapをしない場合のdocker stopにかかる時間
  6. SIGTERMシグナルをtrapするシェルスクリプトをコンテナで実行
  7. trapに失敗するケース

実行中のプロセスに対してシグナル(信号)を送る

シグナルとは、実行中のプロセスに対して、その処理の最中に割り込んで「終了」や「一時停止」などの信号を通知できる仕組み。kill -s シグナル名(番号) プロセスIDというコマンドで、シグナルを通知できる。

シグナルの種類はman 7 signalkill -lで。以下は代表的なもの。

コマンドでシグナル名の「SIG」は省略でき、以降のコマンドでそうする。例えば、バックグラウンドのsleepコマンドにTERMシグナルを送信すると、処理途中で(正常)終了にできる。

# バックグラウンドで365日sleep
sleep 365d &

# $!は最後に実行したバックグラウンドのプロセスID
kill -s TERM $!
[1]+  Terminated              sleep 365d

シグナルを検知して何か処理

シグナルを捉えたプロセスは、その種類ごとにデフォルトの処理をするか、プログラム内で決めた処理をする。

例えばシェルスクリプトの場合、trap "処理内容" シグナル名で独自の処理ができる。(SIGKILLSIGSTOPはtrapできない。)

signal.sh

#!/bin/bash

# バックグラウンドで365日sleep
sleep 365d &

# プロセスID確認
ps --format pid,user,args

# このプログラムがSIGTERMを捉えたら、sleepを終了させる
# $!は最後に実行したバックグラウンドのプロセスID
trap "echo 'sleep(pid$!)を途中終了'; kill -s TERM $!" TERM

echo "シェル(スクリプト)のプロセスID: $$"

# このプログラムはバックグラウンドを待つ
wait

実行

# シェルスクリプトをバックグラウンドで実行
bash signal.sh &
  PID USER     COMMAND
15633 hanako   bash
19225 hanako   bash signal.sh
19229 hanako   sleep 365d
19230 hanako   ps --format pid,user,args
シェル(スクリプト)のプロセスID: 19225

# ここでは上の19225をkill
target=$( ps --format pid,args | grep 'signal.sh' | head -1 | awk '{print $1}' )
echo $target
kill -s TERM $target
sleep(pid19229)を途中終了
[1]+  終了 143              bash signal.sh

コンテナでSIGTERMシグナルをtrapしてみる例

# プロセスID1にSIGTERMを送信
docker run --rm -it --name test centos:7 \
  bash -c "\
    echo コンテナ開始;\
    trap 'echo SIGTERMを検知' TERM;\
    kill -s TERM 1
  "
コンテナ開始
SIGTERMを検知

なお、上のはシグナルをキャッチしただけで、以降にあるような特別な終了処理をしていない(他に残るプロセスがないので終了)。


docker stopでプロセスID1はSIGTERMシグナルを受け取る

上の例ではコンテナ内からプロセスID1にシグナルを送ったが、以下ではdocker stopでコンテナを停止し、シグナルを捉えてみる。

# waitコマンドでバックグラウンドのsleepを待つ
# コンテナ起動はバックグラウンド・モードにしておく
docker run -dit --name test centos:7 \
  bash -c " \
    sleep 365d & \
    ps -A --format pid,ppid,user,args; \
    trap 'echo SIGTERMを検知; echo コンテナ停止' TERM; \
    wait
  "

# コンテナ停止: timeで処理時間を計測
# --time=10: 強制終了するまで10秒待つ(デフォルト)
time docker stop --time=10 test  
real 0m1.629s
# ↑10秒も経たずに、すぐに終了

# コンテナのログ
docker logs test
  PID  PPID USER     COMMAND
    1     0 root     bash -c      sleep 365d &     ps -A --format pid,ppid,user,
    7     1 root     sleep 365d
    8     1 root     ps -A --format pid,ppid,user,args
SIGTERMを検知
コンテナ停止

# コンテナ削除
docker rm test

上記のように、docker stopでプロセスID1にSIGTERMシグナルが送られることがわかる。

trapコマンドの処理でsleepを終了させる処理などはしていないが、コンテナは(10秒待たずに)すぐに終了している。


trapをしない場合のdocker stopにかかる時間

docker stopはプロセスID1にSIGTERMを送信する。その後デフォルトで10秒待って(--time=10)、停止しないならコンテナを強制終了するようだ。以下では--time=5で処理。

# バックグラウンド・モード
docker run --rm -dit --name test centos:7 tail -f /dev/null

# tailコマンドを終わらせるのに、
time docker stop --time=5 test
real 0m6.175s
# 5秒以上かかる

# sleepのバックグラウンドとtailにしてみた
docker run --rm -dit --name test centos:7 bash -c "sleep 365d & tail -f /dev/null"

time docker stop --time=5 test
real	0m5.901s
# 5秒以上かかる

上記のように、シグナルを捕捉しないと終了まで時間がかかる。--time=0にしてもいいが、何かまともな終了処理をしたいときは、以降のようにシグナルをtrapする。


SIGTERMシグナルをtrapするシェルスクリプトをコンテナで実行

以下のように、サーバとしてのシェルスクリプト「server.sh」とDockerfileを用意して、ビルドしてみる。

server.sh

#!/bin/bash

# バックグラウンドで365日sleep
sleep 365d &

# プロセスID確認
ps --format pid,ppid,user,args

終了処理(){
  # $!は最後に実行したバックグラウンドのプロセスID
  kill -s TERM $!
  echo "sleep(pid $!)を途中終了、コンテナ停止。"
}

# このプログラムがSIGTERMを捉えたら終了処理へ
trap "終了処理" TERM

# バックグラウンドを待つ
wait

Dockerfile

FROM centos:7

# コンテナにコピー
COPY server.sh /root/server.sh

# 実行権限付ける
RUN chmod 775 /root/server.sh

# 起動時に実行
CMD ["/root/server.sh"]

ビルド、起動

docker build --tag test .

# バックグラウンド・モード起動
docker run -dit --name test test

# 終了までの時間
time docker stop --time=10 test
real 0m0.807s
# すぐ終わる

# コンテナログ
docker logs test
  PID  PPID USER     COMMAND
    1     0 root     /bin/bash /root/server.sh
    7     1 root     sleep 365d
    8     1 root     ps --format pid,ppid,user,args
sleep(pid 7)を途中終了、コンテナ停止。

# コンテナ削除
docker rm test

上記のように、SIGTERMを受信して終了処理が成功し、docker stopもすぐ完了している。

コンテナ内からプロセスID1をkill

上記の構成なら、docker stopではなく「コンテナ内からプロセスID1をkill」しても同様に終了処理が成功する。

起動

# バックグラウンド・モード起動
docker run -dit --name test test

# コンテナ内からプロセスID1をkill
docker exec -it test kill -s TERM 1

# コンテナログ
docker logs test
  PID  PPID USER     COMMAND
    1     0 root     /bin/bash /root/server.sh
    7     1 root     sleep 365d
    8     1 root     ps --format pid,ppid,user,args
sleep(pid 7)を途中終了、コンテナ停止。

# コンテナ削除
docker rm test

ここで、シグナルをtrapしていないコマンドがプロセスID1の場合は、SIGKILLでさえもkillできない模様


trapに失敗するケース

上の例ではserver.shのプロセスIDが1だったからSIGTERMを受信したが、次のようにすると意図した終了処理ができなくなる。

プロセスIDが1でない場合

# コマンドを二つ付けて、コンテナ起動
# バックグラウンド・モード起動
docker run -dit --name test test bash -c "echo start; /root/server.sh"

# 終了までの時間は
time docker stop --time=5 test
real 0m5.872s
# 5秒以上経ってから終了

# コンテナログ
docker logs test
start
  PID  PPID USER     COMMAND
    1     0 root     bash -c echo start; /root/server.sh
    7     1 root     /bin/bash /root/server.sh
    8     7 root     sleep 365d
    9     7 root     ps --format pid,ppid,user,args

# コンテナ削除
docker rm test

上記では、server.shのプロセスIDが1でないので、シグナル受信に失敗し、意図した終了処理も行われていない。だからdocker stopも時間がかかっている。

なお、dashの場合、渡すコマンドが一つの場合でもプロセスIDが1なのはシェルで、コマンドはそうならない(プロセスIDが1になるのはシェルかコマンドか)。