ROS 2 相比 ROS 1 的一个重大改进是直接复用了现成的 DDS 中间件,而不再自己定义中间件。默认的情况下,ROS 2 会使用 Fast DDS 作为中间件,提供 DDS 协议,即话题发布和接收的功能。

DDS 协议据 ROS 2 设计博客说已经身经百战,质量稳定,但实际上由于 DDS 协议是去中心化的协议,不容易直接地诊断网络问题,并且 DDS 协议栈也比较复杂,调试起来很困难。

Fast DDS 协议栈

对于 ROS 1 而言,进行基本的通信只需要指定一个ROS_MATER_URI即可,所有的数据通信都由中心化的节点管理。不过,在运行rosnode info时,还是会需要直接访问单个节点自身的端口,但无伤大雅。

ROS 2 发现节点

而 ROS 2 没有中心的节点,默认使用的是直接 UDP 多播的方式发现一个网络内的节点,而不同的网络通过ROS_DOMAIN_ID进行区分。另外,也可以用一个中心化的发现服务器组织节点,但实际的数据传输不会走这个发现服务器,意义不大。

UDP 多播

如果一开始的ros2 node list无法找到所有节点,ROS 2 提供了一个默认的工具ros multicast检查能否正常发现节点:

# 在一个网络中
ros2 multicast receive
# 在另一个网络中
ros2 multicast send

如果两个网络支持 UDP 多播,则这两个设备中的节点可以互相发现。在 Docker 中,默认会为容器建立独立的网络,这些网络除了 IP 不同以及具有防火墙相关设置外,和直接回环区别不大。如果希望直接用回环网络,也可以指定--network host。但使用回环网络时,会遇到后面的问题。

ROS 2 数据传输

但是,ROS 2 的一个非常麻烦的地方在于,数据传输和发现节点是独立的。ROS 2 默认的中间件 Fast DDS 默认的传输层配置 DEFAULT 是:

  1. 使用 IPv4 UDP 发现节点。
  2. 随后,如果两个节点的 IP 相同,则使用共享内存传输,否则使用 IPv4 UDP 传输。

Fast DDS 的判断非常地简单,不会具体地在共享内存里进行一次传输作为实验。这导致的结果是,如果两个节点间存在可以发现但不可以传输的状态,即ros2 topic list或者ros2 node list可以看到节点,ros2 topic echo没有输出

共享内存

共享内存的性能显然要高于回环 TCP,而 UDP 由于需要大量地拆包解包,性能更比 TCP 差,我的设备上可能只能跑到 TCP 1/4 以下的性能,总的带宽只略高于 1Gb/s,对于大数据量的任务显然无法胜任。因此,默认使用共享内存是非常合理的。

共享内存是 Linux 中一个非常独特的文件目录,位于/dev/shm下,也可以通过ipcs指令查看,可以参考Shared Memory & Docker。而作为文件,共享内存文件也同样要受到文件权限的限制。通过ls -l /dev/shm可以看到。

文件权限

Linux 中,文件的权限分为几部分:

  • 创建者自己的权限
  • 对应用户组的权限
  • 其他人的权限

其中,创建者和用户组的区分是根据一个整数 UID/GID 进行的。一般直接读取整数比较困难,因此 /etc/passwd 中需要存放每个用户的用户名和对应 UID,ls -l在显示时会查找对应用户名并显示。

Docker 用户权限

Docker 中,用户权限的管理非常复杂,默认情况下直接使用 root 用户,这也对应 dockerd 需要用 root 运行。但也可以用-u指定特定用户,或是指定特定命名空间。 这里需要注意的是,在不使用命名空间时,Docker 内外的 UID 是通用的,即类似于端口号。因此,指定的用户实际上就是在 Docker 外的实际用户。

Docker 配置

那么,在 Docker 内外实现共享内存通信的要素都拼凑齐了。

  • 使用--network host直接使用宿主网络,让 Fast DDS 知道这是同一台电脑。一般使用 Docker 时都使用宿主网络,毕竟隔离也没什么意义。
  • 使用--ipc host共享宿主和容器内的 IPC 资源,包括上述的共享内存。
  • 使用和宿主同样的 UID,保证文件权限不出错。

ROS 的 Docker 镜像默认使用 root 运行,对应容器外的 root,很可能会产生权限问题导致共享内存传输失败,必须修改 UID。修改 UID 有很多方法(Running Docker Containers as Current Host User),但我觉得直接docker build最简单。

这里还参考了 rosblox/ros-template,不过他的方法似乎有点问题。

首先,切换用户一步使用 ros2.dockerfile 实现

FROM ros:humble

ARG ROS_UID
RUN adduser --disabled-password --uid $ROS_UID ros
USER ros

其中的 ROS_UID 需要指定,可以通过id -u查看。

使用 docker-compose.yml 管理上述选项

services:
  ros2:
    build:
      context: ./
      dockerfile: ros2.dockerfile
      args:
        - ROS_UID=${ROS_UID}
    network_mode: host
    ipc: host

其中 ROS_UID 可以是环境变量,也可以是一个 .env 文件(一般第一个用户就是1000)

ROS_UID=1000

测试

在当前目录下创建上述几个文件后,分别在 Docker 内外以任意组合均可发布和接受均可正常收到,如

ros2 topic pub /test std_msgs/msg/String '{data: "hello"}'
docker compose run --rm -it ros2 ros2 topic echo /test

或是

docker compose run --rm -it ros2 ros2 topic pub /test std_msgs/msg/String '{data: "hello"}'
ros2 topic echo /test

其他方法

基于命名空间似乎也可以考虑:

https://gist.github.com/heyfluke/b8372df866ec2584f9a51ca7d7fe9ebb https://www.jujens.eu/posts/en/2017/Jul/02/docker-userns-remap/