Skip to main content
 首页 » 编程设计

python之使用 docker-compose up 运行时如何优雅地停止 Dockerized Python ROS2 节点

2024年11月24日36bonelee

我有一个基于 Python 的 ROS2 节点在 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",我收到“Stopping node..”消息。但是当我执行 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 inside docker container, gracefully stop问题,但那里没有明确的解决方案,我不确定使用 ROS/rclpy 是否会使我的设置有所不同(另外,我的主机是 Ubuntu 18.04,而该用户使用的是 Windows)。

是否可以在我的 stop_node 方法中捕获容器的停止?

请您参考如下方法:

当你的 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 包装器可能会或可能不会继续运行。)

这里的重要部分是这个包装器 shell,而不是你的脚本,是接收信号的主要容器进程,并且(事实证明)won't receive SIGTERM unless it's explicitly coded to .

这里最重要的重组是让你的包装脚本exec Python 脚本。这导致它替换包装器,因此它成为主进程并接收信号。如果不出意外,将最后一行更改为

exec python3 /nodes/mynode.py 

可能会有帮助。


我会在这里更进一步,确保将尽可能多的代码内置到您的 Docker 镜像中,并尽量减少显式 shell 包装器的数量。 “做一些初始化,然后 exec something”是一个非常常见的 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 添加额外的 shell 包装器。由于您的入口点脚本现在将运行任何命令,因此很容易单独更改它。例如,如果您想要一个已完成环境变量设置的交互式 shell 来尝试调试您的容器启动,您可以只覆盖命令部分

docker run --rm -it mynode sh