本文记录了一次完整的 ROS2 多机器人通信实操。从零开始搭建 3 台虚拟机械臂的集群仿真,到用 QoS Profile 差异化配置关节指令与相机图像,再到模拟消息洪流拥塞并用 Deadline QoS 策略修复时序错乱,完整还原了多机 ROS2 系统中 DDS 通信的核心痛点与解决思路。所有代码使用 C++ 实现,采用增量开发模式,每个阶段都可独立验证。

  代码地址:GitHub - HulinCal/multi-robots_DDS_cluster_communication_simulation · GitHub

 1. 前言

        在单机器人 ROS2 项目中,开发者很少需要关心 DDS 的细节:话题能通就行,QoS 用默认配置就好。但当系统扩展到多机器人集群时,一系列问题会接踵而至:

        1. 话题冲突:3 台机械臂都发布 `/joint_states`,订阅者会同时收到 3 份混叠的数据,无法区分来源。

        2. QoS 错配:关节指令需要可靠传输(RELIABLE),相机图像允许丢帧但要求低延迟(BEST_EFFORT)。用错 QoS 会导致控制延迟或图像卡顿。

        3. DDS 域串扰:不同实验环境的机器人如果共用同一 `ROS_DOMAIN_ID`,消息会互相干扰。

        4. 网络拥塞时序错乱:高负载下关节状态消息延迟到达,控制器基于过时状态计算指令,引发机械臂抖动甚至失控。

        5. QoS 不兼容:发布端和订阅端 QoS 不匹配时,DDS 会直接拒绝通信,且错误信息晦涩难懂。

        这次实操的目标,就是在一个可控的仿真环境里,把这些坑一个个踩出来,并给出工程化的解决方案。技术栈选择 ROS2 Jazzy + Cyclone DDS,机械臂模型用 3-DOF 简化模型,3 台机械臂分别命名为 `arm1`、`arm2`、`arm3`。

2. 多进程启动 3 台虚拟机械臂 

2.1 功能包创建与机械臂建模

        第一步是创建 `multi_arm_sim` 功能包。依赖项包括 `rclcpp`、`sensor_msgs`、`std_msgs`、`robot_state_publisher` 和 `urdf`。这里有一个小坑:`package.xml` 中重复声明 `sensor_msgs` 依赖会导致构建错误,需要仔细检查。

        机械臂模型采用 3-DOF 简化 URDF,包含 `base_link`、`link1`、`link2`、`link3` 和 3 个 `joint`。虽然 KDL 解析器会警告 "root link has an inertia specified, but KDL does not support a root link with an inertia",但这不影响 `robot_state_publisher` 正常工作,可以忽略。如果后续要用 MoveIt2 规划,可以添加一个 dummy link 作为根链接的 workaround。

2.2 仿真节点设计

        核心仿真节点有两个,都用 C++ 实现:

        arm_sim_node.cpp- 关节状态仿真节点:

                - 订阅 `joint_commands` 接收目标关节位置

                - 按 50Hz 发布 `joint_states`,包含位置和速度

                - 内部用一阶逼近模拟关节运动(限制最大速度)

                - 每个机械臂实例通过命名空间隔离(如 `/arm1/joint_states`)

        camera_sim_node.cpp- 相机仿真节点:

                - 按 30Hz 发布 `sensor_msgs/Image`

                - 图像分辨率 640x480,内容为简单的渐变图案

                - 话题命名为 `camera/image_raw`

        这两个节点都通过 `declare_parameter` 暴露关键参数(发布率、关节名等),方便 launch 文件统一配置。

2.3 多进程 Launch 脚本

        `multi_arm.launch.py` 用来启动所有节点。

        关键设计:

arm_names = ['arm1', 'arm2', 'arm3']

for idx, arm_name in enumerate(arm_names):

# 每个 arm_sim_node 注入命名空间

arm_sim_node = Node(

package='multi_arm_sim',

executable='arm_sim_node',

namespace=arm_name, # 关键:命名空间隔离

parameters=[{

'publish_rate': 50.0,

'joint_names': ['joint1', 'joint2', 'joint3'],

}],

)

# 同样为 camera_sim_node 和 robot_state_publisher 注入命名空间

        这里有几个实战中踩过的坑:

        坑1:PythonExpression 类型错误。在 launch 文件中用 `idx + 1` 给参数赋值时,`idx` 是 int,而 `PythonExpression` 期望 string,会导致 substitution 失败。解决方法是显式转换 `str(idx + 1)`。

        坑2:URDF 路径含空格。如果工作目录含有空格,导致 `cat` 命令读取 URDF 时失败。解决方案是在 launch 文件中直接用 Python 的 `open(default_urdf, 'r').read()` 读取文件内容,而非通过 shell 命令。

        坑3:conda 环境冲突。系统装了 conda,默认 Python 路径会被 conda 劫持,导致 ROS2 构建报错。必须在每次执行命令前 `conda deactivate` 并 `unset CONDA_PREFIX`,强制使用系统 Python。

2.4 验证

        启动后用 `ros2 topic list` 验证:

        /arm1/joint_states

        /arm1/joint_commands

        /arm1/camera/image_raw

        /arm2/joint_states

        /arm2/joint_commands

        /arm2/camera/image_raw

        /arm3/joint_state

        台机械臂的话题完全隔离,互不干扰。这就是命名空间隔离的威力——同一份节点代码,通过 launch 注入不同命名空间,即可实例化多台机器人,无需修改任何源码。

3. Phase 2:DDS 分区隔离与 QoS Profile

3.1 为什么需要 QoS 差异化?

        ROS2 的 QoS(Quality of Service)Profile 是 DDS 的核心特性,它允许开发者针对不同话题定制通信行为。在多机器人系统中,不同数据类型对通信的要求截然不同:

        如果相机图像用 RELIABLE,订阅端处理慢时 DDS 会重传,导致队列堆积、延迟雪崩。如果关节指令用 BEST_EFFORT,丢一帧指令机械臂就会"抽搐"。所以 QoS 差异化不是优化,而是必需。

3.2 QoS Profile 代码实现

        在 `arm_sim_node.cpp` 中,关节话题使用 RELIABLE:

rclcpp::QoS joint_qos(rclcpp::KeepLast(10));

joint_qos.reliable(); // RELIABLE: 保证送达

joint_qos.durability_volatile(); // VOLATILE: 仅订阅后消息

joint_qos.avoid_ros_namespace_conventions(false);

在 `camera_sim_node.cpp` 中,相机话题使用 BEST_EFFORT:

rclcpp::QoS camera_qos(rclcpp::KeepLast(5));

camera_qos.best_effort(); // BEST_EFFORT: 允许丢帧

camera_qos.durability_volatile();

        关键点:`avoid_ros_namespace_conventions(false)` 让 QoS 遵循 ROS2 命名空间约定,确保参数和话题名正确映射。

3.3 DDS 配置文件

        需要创建 DDS 配置文件,用于网络隔离和性能调优。

        cyclonedds.xml - Cyclone DDS 配置:

<?xml version="1.0" encoding="UTF-8" ?>

<CycloneDDS xmlns="https://cdds.io/config">

<Domain Id="any">

<General>

<AllowMulticast>true</AllowMulticast>

</General>

<Interfaces>

<NetworkInterface name="lo" /> <!-- 本地回环, 适合仿真 -->

</Interfaces>

</Domain>

</CycloneDDS>

        这里有一个坑:CycloneDDS 的 XML schema 在不同版本间有差异。旧版用 `NetworkInterfaceAddress`,新版用 `Interfaces/NetworkInterface`。如果配置文件用了废弃元素,会报 schema 错误。解决方法是查看当前 CycloneDDS 版本的文档,使用正确的元素名。

        fastdds.xml- Fast DDS 配置(备选方案):

        Fast DDS 的配置语法不同,但功能类似,主要用于传输层调优。本项目默认用 Cyclone DDS,Fast DDS 配置作为备选。

3.4 ROS_DOMAIN_ID 域隔离

        `ROS_DOMAIN_ID` 是 DDS 的"虚拟网络"机制。同一 `ROS_DOMAIN_ID` 的节点互相可见,不同 `ROS_DOMAIN_ID` 的节点完全隔离。这比物理网络隔离更轻量,适合多实验环境共用同一台机器。

        验证方法:

# 终端1: ROS_DOMAIN_ID=10 启动机械臂

export ROS_DOMAIN_ID=10

ros2 launch multi_arm_sim multi_arm.launch.py

# 终端2: ROS_DOMAIN_ID=20 查看话题

export ROS_DOMAIN_ID=20

ros2 topic list # 输出为空, 看不到 arm1/arm2/arm3

        这就是 DDS 域隔离的效果。在真实多机器人部署中,可以为每个机器人编队分配不同的 `ROS_DOMAIN_ID`,避免跨编队干扰。

3.5 QoS 验证

        用 `ros2 topic info --verbose` 验证 QoS 是否生效:

        ros2 topic info /arm1/joint_states --verbose

        # 输出包含:

        # Reliability: RELIABLE

        # History (Depth): KEEP_LAST (10)

        ros2 topic info /arm1/camera/image_raw --verbose

        # 输出包含:

        # Reliability: BEST_EFFORT

        # History (Depth): KEEP_LAST (5)

4. 消息洪流拥塞与 Deadline QoS 修复

4.1 洪流发生器设计

        `flood_node.cpp` 是一个专门制造拥塞的节点,设计要点:

// 参数化配置

rate_hz_ = declare_parameter<double>("rate_hz", 1000.0); // 发布频率

payload_kb_ = declare_parameter<int>("payload_kb", 64); // 负载大小

num_topics_ = declare_parameter<int>("num_topics", 4); // 话题数量

duration_sec_ = declare_parameter<double>("duration_sec", 30.0); // 持续时长

关键设计决策:

        决策1:QoS 选择。最初用 BEST_EFFORT,但发现无订阅者时 DDS 直接丢弃消息,根本不制造拥塞。改为 RELIABLE 后,DDS 被迫维护发送队列,真正占用资源。

        决策2:自订阅回环。即使 RELIABLE,如果没订阅者,DDS 仍可能优化掉实际传输。通过让 flood_node 自己订阅自己发布的话题,强制 DDS 完成完整的发布-传输-接收流程,真正占满内部线程和内存带宽。

4.2 Deadline QoS 策略

        Deadline QoS 是 DDS 的"心跳"机制:发布端承诺在 Deadline 期限内必发布,订阅端期望在 Deadline 期限内必收到。超时则触发 `on_deadline_missed` 回调。

        在 `arm_controller_node.cpp` 中,控制器对关节状态订阅设置 Deadline:

double deadline_sec_ = declare_parameter<double>("deadline_sec", 0.035); // 35ms

rclcpp::QoS state_qos(rclcpp::KeepLast(10));

state_qos.reliable();

state_qos.deadline(rclcpp::Duration::from_seconds(deadline_sec_));

// 订阅器事件回调

rclcpp::SubscriptionOptions sub_opts;

sub_opts.event_callbacks.deadline_callback =

[this](rclcpp::QOSDeadlineRequestedInfo &) {

sub_deadline_misses_++;

double gap_ms = (this->now() - last_state_recv_).seconds() * 1000.0;

RCLCPP_ERROR(this->get_logger(),

"[DEADLINE 超时-状态] 状态反馈超时! 累计次数=%lu, "

"距上次状态=%.1fms (期限=%.0fms) -> 时序错乱, 疑似洪流拥塞",

sub_deadline_misses_, gap_ms, deadline_sec_ * 1000.0);

};

发布端(指令)也设置 Deadline 自监控:

double cmd_deadline_sec = 1.2 * (1.0 / cmd_rate_); // 60ms @20Hz


rclcpp::PublisherOptions pub_opts;

pub_opts.event_callbacks.deadline_callback =

[this, cmd_period_sec](rclcpp::QOSDeadlineOfferedInfo &) {

pub_deadline_misses_++;

RCLCPP_WARN(this->get_logger(),

"[DEADLINE 超时-发布] 指令发布节奏异常! 累计次数=%lu", ...);

};

4.3 DDS Deadline 兼容性陷阱

        设置完 Deadline 后启动,立刻报错:

[WARN] New subscription discovered on topic '/arm1/joint_states',

requesting incompatible QoS. No messages will be sent to it.

Last incompatible policy: DEADLINE_QOS_POLICY

        原因:DDS 对 Deadline 有严格的兼容性规则——发布端 offered Deadline ≤ 订阅端 requested Deadline。控制器订阅端 requested 100ms,但 arm_sim 发布端没有设置 Deadline(默认无限大),100ms < 无限大,不兼容,通信直接断开。

        解决方案:给 arm_sim_node 也设置 Deadline,且 offered ≤ requested

        这个规则的本质是:发布端承诺的频率不能低于订阅端期望的频率。如果发布端承诺 30ms 内必发布(即 ≥33Hz),订阅端期望 35ms 内收到(即 ≥28Hz),承诺高于期望,兼容。反之则不兼容。

4.4 多核 CPU 的拥塞难题

        设置好兼容的 Deadline 后,启动洪流节点,却发现控制器始终无超时。问题在于:现代多核 CPU 上,Linux CFS 调度器过于健壮,即使洪流节点占满一个核,arm_sim 仍能在其他核上按时发布。

尝试了多种方案:

方案1:taskset 绑核。用 `taskset -c 0` 把所有进程绑定到 CPU 核 0,强制 CPU 竞争。结果:洪流速率下降(2000Hz→800Hz),但 arm_sim 仍能维持 50Hz,CFS 调度器在多核机器上仍能公平分配时间片。

方案2:多实例洪流。同时启动 3 个洪流节点,累积 CPU 负载。结果:依然无超时,arm_sim 始终获得足够时间片。

方案3:chrt 实时调度。用 `chrt -f 80` 给洪流节点 SCHED_FIFO 实时优先级,强制抢占 arm_sim。结果:需要 root 权限,非 root 用户无法使用。

4.5 应用层拥塞模拟方案

        多核 CPU 上单纯靠 CPU 竞争无法稳定触发 Deadline 超时。最终采用的方案是在应用层模拟 DDS 传输层在拥塞下的消息延迟行为。

        在 `arm_sim_node.cpp` 的 `simStep()` 中添加概率性延迟:

// Phase 3: 拥塞模拟 - 概率性延迟发布, 模拟 DDS 传输层在洪流下的消息延迟

if (simulate_congestion_) {

std::uniform_real_distribution<double> dist(0.0, 1.0);

if (dist(rng_) < congestion_prob_) { // 15% 概率

auto delay_us = static_cast<int>(congestion_delay_ms_ * 1000.0); // 50ms

std::this_thread::sleep_for(std::chrono::microseconds(delay_us));

congestion_count_++;

}

}

        这个方案的本质是:在真实网络中,洪流拥塞导致的不是 CPU 饥饿,而是 DDS 传输层的消息延迟——消息在 DDS 内部队列中等待传输,到达订阅端时已经超过 Deadline。通过在发布前 `sleep`,模拟了这种传输延迟,让 Deadline QoS 回调真实触发。

在 launch 文件中,只对 arm1(控制器的目标)启用拥塞模拟:

'simulate_congestion': (arm_name == 'arm1'), # arm1 启用拥塞模拟

'congestion_prob': 0.15,

'congestion_delay_ms': 50.0,

4.6 最终联调验证

终端1 启动 Launch(arm1 自动启用拥塞模拟):

ros2 launch multi_arm_sim multi_arm.launch.py

终端2 启动洪流节点:

ros2 run multi_arm_sim flood_node --ros-args \

-p rate_hz:=1000.0 -p payload_kb:=128 -p num_topics:=4 -p duration_sec:=20.0

控制器终端日志:

[ERROR] [DEADLINE 超时-状态] 状态反馈超时! 累计次数=438, 距上次状态=35.1ms (期限=35ms) -> 时序错乱, 疑似洪流拥塞

[ERROR] [DEADLINE 超时-状态] 状态反馈超时! 累计次数=439, 距上次状态=70.1ms (期限=35ms) -> 时序错乱, 疑似洪流拥塞

[INFO] [回采] arm1: 接收 1900 帧 | Deadline超时(发布=2, 状态=437) | 位置[-0.350,0.720,-0.320]

成功!Deadline QoS 回调持续触发,ERROR 日志清晰报告:

- 超时次数:累计 438 次(15% 概率延迟 × 持续时长)

- 距上次状态:35.1ms / 70.1ms(超过 35ms 期限)

- 诊断结论:时序错乱,疑似洪流拥塞

5. 工程经验总结

5.1 QoS 配置不是优化,是必需

        很多 ROS2 教程把 QoS 当作"高级优化",用默认配置就行。这在单机 demo 里成立,但在多机生产系统中,QoS 错配会导致隐蔽且严重的故障:

- 关节指令用 BEST_EFFORT → 偶发丢指令 → 机械臂抽搐

- 相机图像用 RELIABLE → 订阅端慢 → 队列堆积 → 延迟雪崩

- Deadline 不匹配 → DDS 静默断开 → 通信中断但无明确错误

QoS 必须在设计阶段就明确,并为每类话题建立配置矩阵。

5.2 DDS Deadline 兼容规则必须牢记

        `offered ≤ requested` 这条规则是 DDS 通信建立的前提。调试时如果遇到 "incompatible QoS" 警告,第一步就是检查 Deadline 配置。建议在 launch 文件中用注释明确标注每对发布-订阅的 offered/requested 值,方便排查。

5.3 洪流测试需要贴近真实场景

        纯 CPU 压测无法模拟真实网络拥塞。真实拥塞的特征是:

- DDS 内部队列堆积

- 消息传输延迟增大

- 丢包率上升(但 RELIABLE 会重传,进一步加剧拥塞)

        应用层 `sleep` 模拟虽然"不真实",但能准确触发 Deadline QoS 的检测逻辑,适合功能验证。真实压测应在跨主机网络环境中进行,用 `tc`(traffic control)注入网络延迟和丢包。

5.4 conda 与 ROS2 的恩怨

        conda 和 ROS2 的 Python 环境冲突是老问题。根本原因是 conda 修改了 `PATH` 和 `PYTHONPATH`,导致 ROS2 找到的是 conda 的 Python 而非系统 Python。解决方案:

- 每次使用 ROS2 前 `conda deactivate`

- 或在 conda 环境中显式安装 ROS2 依赖(不推荐,容易版本冲突)

- 或用 `__conda_deactivate` 彻底关闭 conda

6. 扩展思考

6.1 真实多机器人部署的额外挑战

        这次实操在单机仿真环境完成,真实部署还需考虑:

- 跨主机网络:DDS 默认用多播发现,跨网段需要配置单播或 DDS Router

- 防火墙与端口:DDS 端口动态分配,防火墙策略需开放端口范围

- 实时性:Linux 非实时内核下,高负载时调度抖动仍会影响 Deadline,需用 PREEMPT_RT 或 Xenomai

- 安全:DDS Security 规范(SAML、权限控制)在多机部署中不可忽视

6.2 从 Deadline 到主动降级

        Deadline QoS 目前只能"报警",不能"修复"。更完善的方案是:

                - 检测到 Deadline 超时后,主动降低控制器频率(从 20Hz 降到 10Hz)

                - 通知洪流节点减速(通过单独的控制话题)

                - 触发安全模式:机械臂减速到安全速度或暂停

6.3 DDS 中间件选型

Cyclone DDS 和 Fast DDS 各有优劣:

- Cyclone DDS:ROS2 默认,配置简单,适合仿真和小规模部署

- Fast DDS:性能更优,支持更丰富的 QoS(如数据共享内存),适合大规模生产

        本项目用 Cyclone DDS 便于演示,生产环境建议评估 Fast DDS 的内存共享模式,可显著降低进程间通信延迟。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐