Как изящно остановить узел Dockerized Python ROS2 при запуске с docker-compose? - PullRequest
1 голос
/ 15 июня 2019

У меня есть узел 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 методу?

1 Ответ

1 голос
/ 15 июня 2019

Когда ваш 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
...