本地docker执行和CTRL-C信号传播

本地docker执行和CTRL-C信号传播

我有一个基于docker的集群提交系统,我正在尝试让它也支持本地执行。在本地执行时,启动作业的命令基本上是

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

对于集群执行,正在运行另一个脚本。我面临的困难是如何以本地用户身份运行代码,同时仍然正确支持 CTRL-C。由于 docker run 以 uid 0 启动入口点,因此我需要使用 运行用户的入口点su -c。基本上,脚本需要运行两件事:

  1. 预运行脚本(以 root 身份调用)
  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_PID被分配了 PID 61。但是,我希望能够正常关闭 Python 解释器,这样我就应该能够在那里捕获一些信号。有人知道在这种情况下如何将 SIGINT 转发到 Python 解释器吗?有没有更聪明的方法来完成我想要完成的任务?docker run当代码被安排在本地执行时,我可以完全控制将命令组合在一起的代码。

答案1

这里发生了一些事情。首先,您正在容器内以 pid 1 运行 shell 脚本。在各种情况下,该进程都会看到 cont+c,或者docker stop发送信号,并由 bash 来捕获和处理它。默认情况下,当以 pid 1 运行时,bash 将忽略该信号(我相信是为了在 Linux 服务器上处理单用户模式)。您需要使用类似以下方式明确捕获和处理该信号:

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

在脚本顶部。这将捕获 SIGTERM 和 SIGINT(由 cont+c 生成),终止子进程并立即退出。

接下来是su命令,它本身会派生出一个可以中断信号处理的进程。我更喜欢gosu运行 exec 而不是 fork 系统调用,将其自身从进程列表中删除。您可以gosu在 Dockerfile 中使用以下命令进行安装:

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

最后,入口点中有很多逻辑需要 fork,然后等待后台进程完成。通过在前台运行进程可以简化这一过程。您运行的最后一个命令可以用 来启动,exec以避免让 shell 继续运行。您可以使用 来捕获错误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

相关内容