ローカル Docker 実行と CTRL-C シグナルの伝播

ローカル Docker 実行と CTRL-C シグナルの伝播

私はDockerベースのクラスタサブミッションシステムを持っており、ローカル実行もサポートできるようにしようとしています。ローカルで実行する場合、ジョブを開始するコマンドは基本的に

docker run /results/src/launcher/local.sh

クラスター実行では、代わりに別のスクリプトが実行されます。私が直面している難しさは、CTRL-C を正しくサポートしながら、ローカル ユーザーとしてコードを実行する方法です。docker run はエントリポイントを uid 0 として開始するため、ユーザーのエントリポイントを で実行する必要がありますsu -c。基本的に、スクリプトは次の 2 つを実行する必要があります。

  1. 事前実行スクリプト(ルートとして呼び出される)
  2. Python プログラム (呼び出しユーザーとして呼び出される)

スクリプトの要点は現在次のとおりです。

# Run prerun script
$PRERUN &
PRERUN_PID=$!
wait $PRERUN_PID
PRERUN_FINISHED=true
status=$?

if [ "$status" -eq "0" ]; then
    echo "Prerun finished successfully."
else
    echo "Prerun failed with code: $status"
    exit $status
fi

# Run main program dropping root privileges.
su -c '/opt/conda/bin/python /results/src/launcher/entrypoint.py \
      > >(tee -a /results/stdout.txt) 2> >(tee -a /results/stderr.txt >&2)' \
      $USER &
PYTHON_PID=$!
wait $PYTHON_PID
PYTHON_FINISHED=true
status=$?

if [ "$status" -eq "0" ]; then
    echo "Entrypoint finished successfully."
else
    echo "Entrypoint failed with code: $status"
    exit $status 
fi

シグナル伝播は同じスクリプト内で次のように処理されます。

_int() {
    echo "Caught SIGINT signal!"
    if [ "$PRERUN_PID" -ne "0" ] && [ "$PRERUN_FINISHED" = "false" ]; then
        echo "Sending SIGINT to prerun script!"
        kill -INT $PRERUN_PID
        PRERUN_PID=0
    fi
    if [ "$PYTHON_PID" -ne "0" ] && [ "$PYTHON_FINISHED" = "false" ]; then
        echo "Sending SIGINT to Python entrypoint!"
        kill -INT $PYTHON_PID
        PYTHON_PID=0
    fi
}

PRERUN_PID=0
PYTHON_PID=0
PRERUN_FINISHED=false
PYTHON_FINISHED=false
trap _int SIGINT

にシグナル ハンドラがあります/results/src/launcher/entrypoint.py。これは によって実行されるコードですsu -c。しかし、SIGINT を取得できないようです。問題は にあると思われますsu -c。 で予想されたとおり、PYTHON_PIDbash スクリプトには Python インタープリタの PID ではなく、suプログラムの PID が割り当てられます。Python エントリポイントで を実行するとos.system("ps xa")、次のようになります。

  PID TTY      STAT   TIME COMMAND
    1 ?        Ss     0:00 /bin/bash /results/src/launcher/local.sh user 1000 1000 /results/src/example/compile.sh
   61 ?        S      0:00 su -c /opt/conda/bin/python /results/src/launcher/entrypoint.py \       > >(tee -a /results/stdout.txt) 2> >(tee -a /results/stderr.txt >&2) user
   62 ?        Ss     0:00 bash -c /opt/conda/bin/python /results/src/launcher/entrypoint.py \       > >(tee -a /results/stdout.txt) 2> >(tee -a /results/stderr.txt >&2)
   66 ?        S      0:01 /opt/conda/bin/python /results/src/launcher/entrypoint.py
   67 ?        S      0:00 bash -c /opt/conda/bin/python /results/src/launcher/entrypoint.py \       > >(tee -a /results/stdout.txt) 2> >(tee -a /results/stderr.txt >&2)
   68 ?        S      0:00 bash -c /opt/conda/bin/python /results/src/launcher/entrypoint.py \       > >(tee -a /results/stdout.txt) 2> >(tee -a /results/stderr.txt >&2)
   69 ?        S      0:00 tee -a /results/stdout.txt
   70 ?        S      0:00 tee -a /results/stderr.txt
   82 ?        R      0:00 /opt/conda/bin/python /results/src/launcher/entrypoint.py
   83 ?        S      0:00 /bin/dash -c ps xa
   84 ?        R      0:00 ps xa

PYTHON_PIDPID 61 が割り当てられます。ただし、Python インタープリターを正常にシャットダウンできるようにしたいので、そこで何らかのシグナルをキャッチできる必要があります。このような状況で SIGINT を Python インタープリターに転送する方法を知っている人はいますか? 私が達成しようとしていることを行うよりスマートな方法はありますか? コードがdocker runローカル実行用にスケジュールされているときにコマンドをまとめるコードを完全に制御できます。

答え1

ここではいくつかのことが起こっています。まず、コンテナ内で pid 1 としてシェル スクリプトを実行しています。さまざまなシナリオでそのプロセスが cont+c を認識し、docker stopシグナルを送信し、それをトラップして処理するのは bash の役割です。デフォルトでは、pid 1 として実行している場合、bash はシグナルを無視します (Linux サーバーでシングル ユーザー モードを処理するためだと思います)。次のようにして、そのシグナルを明示的にトラップして処理する必要があります。

trap 'pkill -P $$; exit 1;' TERM INT

スクリプトの先頭に、SIGTERM と SIGINT (cont+c によって生成される) をキャッチし、子プロセスを強制終了して、すぐに終了します。

次に、コマンドがありますsu。このコマンドは、シグナル処理を中断する可能性のあるプロセスをフォークします。私は、このコマンドが fork システムコールの代わりに exec を実行し、プロセス リストから自分自身を削除することを好みます。Dockerfileで次のようにgosuインストールできます。gosu

ARG GOSU_VER=1.10
ARG GOSU_ARCH=amd64
RUN curl -sSL "https://github.com/tianon/gosu/releases/download/${GOSU_VER}/gosu-${GOSU_ARCH}" >/usr/bin/gosu \
 && chmod 755 /usr/bin/gosu \
 && gosu nobody true

最後に、エントリポイントには、フォークしてバックグラウンド プロセスが終了するのを待つロジックが多数あります。これは、プロセスをフォアグラウンドで実行することで簡素化できます。最後に実行するコマンドは、execシェルの実行を中断しないように で開始できます。 でエラーをキャッチするset -eか、 フラグを使用して実行中のコマンドのデバッグを表示するために を展開できます-x。最終結果は次のようになります。

#!/bin/bash

set -ex

# in case a signal is received during PRERUN
trap 'exit 1;' TERM INT

# Run prerun script
$PRERUN

# Run main program dropping root privileges.
exec gosu "$USER" /opt/conda/bin/python /results/src/launcher/entrypoint.py \
      > >(tee -a /results/stdout.txt) 2> >(tee -a /results/stderr.txt >&2)

ログを削除できれば、スクリプトの先頭で から に切り替えて、 だけを使用してコンテナ/resultsからの結果を確認できるようになります。/bin/bash/bin/shdocker logs

関連情報