
Eu tenho um sistema de envio de cluster baseado em docker e estou tentando fazer com que ele também suporte a execução local. Ao executar localmente, o comando que inicia o trabalho é basicamente
docker run /results/src/launcher/local.sh
Para execução do cluster, outro script está sendo executado. A dificuldade que estou enfrentando é como executar o código como usuário local e ainda oferecer suporte a CTRL-C corretamente. Como docker run inicia o ponto de entrada como uid 0, preciso executar o ponto de entrada do usuário com su -c
. Basicamente, o script precisa executar duas coisas:
- Um script de pré-execução (chamado de root)
- Um programa Python (chamado de usuário chamador)
A essência do script é atualmente a seguinte:
# 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
A propagação do sinal é tratada no mesmo script por:
_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
Eu tenho um manipulador de sinal /results/src/launcher/entrypoint.py
, que é o código executado por su -c
. No entanto, parece nunca obter o SIGINT. Presumo que o problema esteja no su -c
. Como esperado PYTHON_PID
no script bash não é atribuído o PID do interpretador python, mas do su
programa. Se eu fizer um os.system("ps xa")
no meu ponto de entrada do Python, vejo o seguinte:
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
é atribuído o PID 61. No entanto, eu gostaria de poder desligar normalmente o interpretador python, para poder capturar algum sinal lá. Alguém sabe como encaminhar um SIGINT para o interpretador Python em uma situação como essa? Haveria uma maneira mais inteligente de fazer o que estou tentando realizar? Tenho controle total sobre o código que reúne o docker run
comando quando o código está agendado para execução local.
Responder1
Há algumas coisas acontecendo aqui. Primeiro, você está executando um script de shell como pid 1 dentro do contêiner. Esse processo em vários cenários é o que vê o cont+c, ou docker stop
enviando o sinal, e cabe ao bash capturá-lo e tratá-lo. Por padrão, ao executar como pid 1, o bash irá ignorar o sinal (acredito que ele lide com o modo de usuário único em um servidor Linux). Você precisaria capturar e manipular explicitamente esse sinal com algo como:
trap 'pkill -P $$; exit 1;' TERM INT
na parte superior do roteiro. Isso capturaria o SIGTERM e o SIGINT (gerados por cont+c), mataria os processos filhos e sairia imediatamente.
Em seguida, há o su
comando, que bifurca um processo que pode interromper o tratamento do sinal. Eu prefiro gosu
que execute um exec em vez de um syscall fork, removendo-se da lista de processos. Você pode instalar gosu
com o seguinte em um 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
Por último, há muita lógica no ponto de entrada para bifurcar e aguardar a conclusão de um processo em segundo plano. Isso poderia ser simplificado executando os processos em primeiro plano. O último comando executado pode ser iniciado com um exec
para evitar deixar o shell em execução. Você pode detectar erros set -e
ou expandi-los para mostrar a depuração de quais comandos estão sendo executados com um -x
sinalizador. O resultado final é semelhante a:
#!/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)
Se você conseguir se livrar dos /results
logs, poderá alternar de /bin/bash
para /bin/sh
na parte superior do script e apenas contar com docker logs
para ver os resultados do contêiner.