指定されたコマンドを子プロセスで実行し、特定のシグナルを受けたときは子プロセス終了時にリスタートするシェルスクリプト

はじめに

  • php で作ったキューワーカーのようなサービスをコンテナで実行させる
  • サービスはシグナルをハンドリングしていわゆるに Graceful に終了したい
  • サービスが終了したときは普通にコンテナも終了する
  • ただしローカルの開発環境では特定のシグナルを受けたときはコンテナを終了させずにサービスだけリスタートする
  • そのためにシェルスクリプトを書いてサービスはそのシェルスクリプトから実行する

実装例その1

コンテナの init プロセスとして dumb-init を使って「dumb-init -> sh -> サービス」のようなプロセスツリーになるなら、次のようにシンプルに実装できそうです。

#!/bin/sh

trap restart=1 HUP
trap restart=  INT
trap restart=  QUIT
trap restart=  TERM

while :; do
  restart=
  "$@"
  status=$?
  if [ -z "$restart" ]; then
    exit "$status"
  fi
done

dumb-init がシグナルを受けると(-c オプションが指定されていなければ)直接の子プロセスだけでなく孫プロセスにもまとめてシグナルが転送されます。ので、シェルスクリプトからサービスへシグナルを送る必要はなく、単にサービスが終了したときにリスタートするかどうかの条件として最後に受けたシグナルを使えば良いだけです。

この方法の問題点は↑で列挙しているシグナル(HUP/INT/QUIT/TERM)以外のシグナルを受けると、例えサービスでそれをハンドリングしていても sh のプロセスが落ちてしまい、直下のプロセスが落ちたことで dumb-init も終了してコンテナ自体が終了するため、サービスのプロセスも突然死します。ただ、サービスでハンドリングしているシグナルを列挙しておけば良いだけなので、問題ではありません。

他の問題点をあげるとすれば・・dumb-init からシグナルが複数のプロセスに送られた後、各プロセスでシグナルハンドラが実行されるタイミングは決まっていないだろうので、HUP シグナルの後に sh の trap で restart=1 が実行される前に、サービスのプロセスが終了して sh の while ループを抜けてしまう可能性がゼロでは無いかもしれません(未確認)。ただリスタートしたいのはローカル環境だけのことでプロダクション環境で使いたいわけではないので、この程度の問題は目を瞑ります。

実装例その2

別の実装案。dumb-init が無くても動きます。また、シェルスクリプトが予期しないシグナルなどで終了するときもサービスのプロセスの終了を一定時間待ちます。ただ予期しないシグナルでも trap EXIT を発動させるために bash が必要です()。

#!/bin/bash

on_exit() {
  if [ -n "$pid" ]; then
    kill -TERM "$pid"
    for x in 0 1 2 3 4 5 6 7 8 9; do
      if ! kill -0 "$pid" 2>/dev/null; then
        exit
      fi
      sleep 1
    done
    kill -KILL "$pid"
  fi
}

trap 'restart=1 ; [ -n "$pid" ] && kill -HUP  "$pid"' HUP
trap 'restart=  ; [ -n "$pid" ] && kill -INT  "$pid"' INT
trap 'restart=  ; [ -n "$pid" ] && kill -QUIT "$pid"' QUIT
trap 'restart=  ; [ -n "$pid" ] && kill -TERM "$pid"' TERM
trap 'on_exit' EXIT

while :; do
  restart=
  "$@" &
  pid=$!
  while :; do
    wait
    if kill -0 "$pid" 2>/dev/null; then
      continue
    fi
    pid=
    if [ -z "$restart" ]; then
      exit
    fi
    break
  done
done

この実装だとサービスの終了コードが何であってもシェルスクリプトの終了コードは 0 になってしまいます。

さいごに

特定のシグナルでリスタートしたいことがあるのはローカル環境だけで、プロダクション環境では素朴に dumb-init の直下で実行すればよかったので、それならローカル環境でのみ runit とか supervised とかでリスタートさせればよいか・・と思ったのですがそれだけのために runit やら supervised やらを出してくるのは仰々しい気がしたので、シェルスクリプトを書いてみました。