Как изящно остановить узел Dockerized Python ROS2 при запуске с помощью docker-compose?

У меня есть узел ROS2 на основе Python, работающий внутри контейнера Docker, и я пытаюсь обработать корректное завершение работы узла, перехватывая сигналы SIGTERM/SIGINT и/или перехватывая исключение KeyboardInterrupt.

Проблема в том, что когда я запускаю узел в контейнере, используя docker-compose. Кажется, я не могу поймать «момент», когда контейнер останавливается/убивается. Я явно добавил STOPSIGNAL в Dockerfile и stop_signal в файле docker-compose.

Вот пример кода узла:

import signal
import sys
import rclpy

def stop_node(*args):
    print("Stopping node..")
    rclpy.shutdown()
    return True

def main():
    rclpy.init(args=sys.argv)
    print("Creating node..")
    node = rclpy.create_node("mynode")
    print("Running node..")
    while rclpy.ok():
        rclpy.spin_once(node)

if __name__ == '__main__':
    try:
        signal.signal(signal.SIGINT, stop_node)
        signal.signal(signal.SIGTERM, stop_node)
        main()
    except:
        stop_node()

Вот пример Dockerfile для повторного создания образа:

FROM osrf/ros2:nightly

ENV DEBIAN_FRONTEND=noninteractive
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654
RUN apt-get update && \
    apt-get install -y vim
WORKDIR /nodes
COPY mynode.py .
ADD run-node.sh /run-node.sh
RUN chmod +x /run-node.sh
STOPSIGNAL SIGTERM

Вот образец docker-compose.yml:

version: '3'

services:
  mynode:
    container_name: mynode-container
    image: mynode
    entrypoint: /bin/bash -c "/run-node.sh"
    privileged: true
    stdin_open: false
    tty: true
    stop_signal: SIGTERM

Вот скрипт run-node.sh:

source /opt/ros/$ROS_DISTRO/setup.bash
python3 /nodes/mynode.py

Когда я вручную запускаю узел внутри контейнера (используя python3 mynode.py или /run-node.sh) или когда я делаю docker run -it mynode /bin/bash -c "/run-node.sh", я получаю сообщение «Остановка узла..». Но когда я делаю docker-compose up, я никогда не вижу это сообщение при остановке контейнера с помощью Ctrl+C или docker-compose down.

$ docker-compose up
Creating network "ros-node_default" with the default driver
Creating mynode-container ... done
Attaching to mynode-container
mynode-container | Creating node..
mynode-container | Running node..

^CGracefully stopping... (press Ctrl+C again to force)
Stopping mynode-container ... done
$

Я пытался:

  • перенос звонков на signal.signal
  • используя atexit вместо signal
  • используя docker stop и docker kill --signal

Я также проверил этот вопрос Python внутри док-контейнера, изящно остановите, но там нет четкого решения, и я не уверен, что использование ROS/rclpy делает мою настройку другой (к тому же, мой хост-компьютер - Ubuntu 18.04, а этот пользователь был в Windows).

Можно ли отловить остановку контейнера в моем методе stop_node?


person Gino Mempin    schedule 15.06.2019    source источник


Ответы (1)


Когда в вашем docker-compose.yml файле написано:

entrypoint: /bin/bash -c "/run-node.sh"

Поскольку это пустая строка, Docker помещает ее в оболочку /bin/sh -c. Таким образом, основной процесс вашего контейнера выглядит примерно так:

/bin/sh -c '/bin/bash -c "/run-node.sh"'

В свою очередь, скрипт bash продолжает работать. Он запускает сценарий Python и продолжает работать в качестве своего родителя до тех пор, пока этот сценарий не завершится. (Два уровня обертки sh -c могут продолжать работать, а могут и не работать.)

Важной частью здесь является то, что эта оболочка-оболочка, а не ваш скрипт, является основным процессом-контейнером, который получает сигналы, и (оказывается) не будет получать SIGTERM, если он явно не закодирован для.

Самая важная реструктуризация, которую нужно сделать здесь, — это заставить ваш скрипт-оболочку exec скрипт Python. Это заставляет его заменить оболочку, поэтому он становится основным процессом и получает сигналы. Если ничего другого не изменить последнюю строку на

exec python3 /nodes/mynode.py

скорее всего поможет.


Я бы пошел немного дальше и убедился, что большая часть этого кода встроена в ваш образ Docker, и попытался свести к минимуму количество явных оболочек-оболочек. «Выполните некоторую инициализацию, затем exec что-то» — чрезвычайно распространенный шаблон Docker, и вы можете написать этот скрипт и сделать его точкой входа вашего образа:

#!/bin/sh
# Do the setup
# ("." is the same as "source", but standard)
. "/opt/ros/$ROS_DISTRO/setup.bash"
# Run the main CMD
exec "$@"

Точно так же ваш основной скрипт должен начинаться со строки «shebang», например

#!/usr/bin/env python3
import ...

Ваш Dockerfile уже содержит настройку для прямого запуска оболочки, вам может понадобиться аналогичная строка RUN chmod для основного скрипта. Но тогда вы можете добавить

ENTRYPOINT ["/run-node.sh"]
CMD ["/nodes/my-node.py"]

Поскольку оба сценария являются исполняемыми и имеют строки «shebang», вы можете запускать их напрямую. Использование синтаксиса JSON не позволяет Docker добавить дополнительную оболочку оболочки. Поскольку ваш сценарий точки входа теперь будет выполнять любую команду, ее легко изменить отдельно. Например, если вы хотите, чтобы интерактивная оболочка, которая выполнила настройку переменной среды, пыталась отладить запуск вашего контейнера, вы можете переопределить только командную часть.

docker run --rm -it mynode sh
person David Maze    schedule 15.06.2019