
Tengo un sistema de envío de clústeres basado en Docker y estoy intentando que también admita la ejecución local. Cuando se ejecuta localmente, el comando que inicia el trabajo es básicamente
docker run /results/src/launcher/local.sh
Para la ejecución del clúster, se ejecuta otro script. La dificultad a la que me enfrento es cómo ejecutar el código como usuario local y al mismo tiempo admitir CTRL-C correctamente. Dado que Docker Run inicia el punto de entrada como uid 0, necesito ejecutar el punto de entrada del usuario con su -c
. Básicamente, el script necesita ejecutar dos cosas:
- Un script previo a la ejecución (llamado root)
- Un programa Python (llamado usuario llamante)
El meollo del guión es actualmente el siguiente:
# 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
La propagación de la señal se maneja en el mismo script mediante:
_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
Tengo un controlador de señales /results/src/launcher/entrypoint.py
, que es el código ejecutado por su -c
. Sin embargo, nunca parece obtener el SIGINT. Supongo que el problema está en el su -c
. Como era de esperar, PYTHON_PID
en el script bash no se asigna el PID del intérprete de Python, sino del su
programa. Si hago un os.system("ps xa")
en mi punto de entrada de Python, veo lo siguiente:
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
se le asigna el PID 61. Sin embargo, me gustaría poder apagar correctamente el intérprete de Python, por lo que debería poder captar alguna señal allí. ¿Alguien sabe cómo reenviar un SIGINT al intérprete de Python en una situación como esta? ¿Habría una manera más inteligente de hacer lo que estoy tratando de lograr? Tengo control total sobre el código que crea el docker run
comando cuando el código está programado para ejecución local.
Respuesta1
Están sucediendo algunas cosas aquí. Primero, está ejecutando un script de shell como pid 1 dentro del contenedor. Ese proceso en varios escenarios es lo que ve el cont+c, o docker stop
el envío de la señal, y depende de bash atraparlo y manejarlo. De forma predeterminada, cuando se ejecuta como pid 1, bash ignorará la señal (creo que maneja el modo de usuario único en un servidor Linux). Necesitarías capturar y manejar explícitamente esa señal con algo como:
trap 'pkill -P $$; exit 1;' TERM INT
en la parte superior del guión. Eso detectaría SIGTERM y SIGINT (generados por cont+c), eliminaría los procesos secundarios y saldría inmediatamente.
A continuación, está el su
comando, que a su vez bifurca un proceso que puede interrumpir el manejo de la señal. Prefiero gosu
que ejecute un ejecutivo en lugar de una llamada al sistema fork, eliminándose de la lista de procesos. Puede instalar gosu
con lo siguiente en un 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, hay mucha lógica en el punto de entrada para bifurcarse y luego esperar a que finalice un proceso en segundo plano. Esto podría simplificarse ejecutando los procesos en primer plano. El último comando que ejecute se puede iniciar con un exec
para evitar dejar el shell ejecutándose. Puede detectar errores con set -e
o expandirlo para mostrar la depuración de qué comandos se ejecutan con una -x
bandera. El resultado final se parece 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)
Si puede deshacerse de los /results
registros, debería poder cambiar desde /bin/bash
la /bin/sh
parte superior del script y simplemente confiar en docker logs
ver los resultados del contenedor.