
У меня есть система отправки кластера на основе docker, и я пытаюсь заставить ее также поддерживать локальное выполнение. При локальном выполнении команда, которая запускает задание, в основном
docker run /results/src/launcher/local.sh
Для выполнения кластера вместо этого запускается другой скрипт. Трудность, с которой я столкнулся, заключается в том, как запустить код от имени локального пользователя, при этом правильно поддерживая CTRL-C. Поскольку docker run запускает точку входа как uid 0, мне нужно запустить точку входа пользователя с su -c
. По сути, скрипт должен запустить две вещи:
- Предварительно запущенный скрипт (называемый root)
- Программа 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_PID
в скрипте bash назначается не PID интерпретатора python, а PID программы su
. Если я делаю a os.system("ps xa")
в своей точке входа Python, я вижу следующее:
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 внутри контейнера. Этот процесс в различных сценариях видит cont+c или docker stop
отправляет сигнал, и bash должен перехватить и обработать его. По умолчанию, при запуске как pid 1, bash проигнорирует сигнал (я полагаю, для обработки однопользовательского режима на сервере Linux). Вам нужно будет явно перехватить и обработать этот сигнал с помощью чего-то вроде:
trap 'pkill -P $$; exit 1;' TERM INT
в верхней части скрипта. Это перехватит SIGTERM и SIGINT (сгенерированные cont+c), остановит дочерние процессы и немедленно завершит работу.
Далее идет su
команда, которая сама по себе разветвляет процесс, который может нарушить обработку сигнала. Я предпочитаю, gosu
которая запускает exec вместо fork syscall, удаляя себя из списка процессов. Вы можете установить 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
Наконец, в точке входа есть много логики для разветвления и ожидания завершения фонового процесса. Это можно упростить, запустив процессы на переднем плане. Последняя команда, которую вы запускаете, может быть запущена с , 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/sh
в верхней части скрипта и просто docker logs
посмотреть результаты из контейнера.