ROS2 DDS选型与QoS配置实战:从原理到排坑
2026/9/17 4:00:17 网站建设 项目流程

ROS2 从 ROS1 走过来,很多人一开始最不适应、也最容易踩坑的,就是它底层的 DDS 换了,通信机制也跟着变了。ROS1 时代我们几乎不用关心消息怎么传输,topic 写好了、节点跑起来、rostopic echo 能看到数据,这事就算通了。到了 ROS2,DDS 作为中间件被引入之后,问题就多了起来:为什么两个节点明明在同一台机器上却互相发现不了?为什么发布端和订阅端 QoS 不一致就直接报错?为什么换了一个 DDS 实现之后,延迟和丢包表现差距这么大?这篇文章就围绕 ROS2 里的 DDS 协议选型和 QoS 策略做一次完整总结,把这些坑背后的原理讲清楚,同时给出可以直接参考的配置方法和排查思路。适合所有做 ROS2 开发、做机器人通信系统设计、或者正在被 multi-machine 通信和大数据量传输折腾的开发者参考。

1. 为什么 ROS2 要选 DDS:通信设计的一次大换血

1.1 ROS1 的通信短板

ROS1 的通信模型是中心化的。所有的节点要互相通信,必须依赖一个叫做 roscore 的 master 节点来做名字服务和连接建立。也就是说,节点启动之后先要去 master 那里注册自己的 topic、service 信息,再通过 master 的牵线搭桥,才能和真正的通信对端建立 TCP 连接。这种设计在单个机器人、单台电脑上跑 demo 没什么问题,但一旦进入真实场景,问题就暴露得很明显。

多机协同是 ROS1 最不好搞的事情。两个机器人之间要通信,你得先让它们都能访问到同一台 master,还要配置好网络,让各个节点的主机名能互相解析。更麻烦的是,单点故障问题,master 挂了,整个机器人系统里的所有 topic 通信都跟着断掉,这在机器人这种对稳定性要求极高的场景里几乎不可接受。

还有一个痛点是数据实时性。ROS1 的 TCP 通信在局域网里表现尚可,但到了需要实时控制、或者跨站点传输的时候,延迟、抖动、丢包重传这些因素都没有被上层抽象起来。开发者想对通信质量做精细控制,几乎无从下手。实时机器人系统里,一个传感器数据晚到 10ms,可能就意味着控制指令已经过时了。这就是 ROS2 下定决心要换通信底层的原因。

1.2 DDS 解决了什么

DDS,Data Distribution Service,是一套由 OMG 组织制定的分布式实时通信规范。它和 ROS1 的 TCP 直连模型有一个本质差别:DDS 是去中心化的,它采用发布订阅模型,节点之间直接发现、直接通信,不需要一个中心节点来管理全局的信息。

DDS 的发现机制是分两步走的。第一步是参与者发现阶段,每个 DDS DomainParticipant 启动后都会往 DDS 域里发送发现报文,宣告自己的存在;然后第二步是端点发现阶段,参与者之间交换各自的 publisher、subscriber 信息,如果发现彼此有匹配的 topic 和数据类型,就建立通信链路。这种机制的好处是天生支持动态拓扑,节点随时加入、随时退出,系统不需要重启,网络里的其他节点也能自动感知到变化。

对机器人这种分布式系统来说,DDS 带来的价值非常直接。首先是实时性,DDS 的实现通常支持 RTPS(Real-Time Publish-Subscribe)协议,在局域网内可以达到微秒到毫秒级的延迟,对于机器人控制、传感器数据流这种场景已经够用。其次是可靠性,DDS 的 QoS 策略允许你精确控制消息是只要尽力传输还是要可靠重传,是要只在内存里存一份还是要持久化到磁盘,这些策略组合起来,基本可以覆盖从遥感到指令下发到日志记录的所有通信需求。

1.3 RMW 抽象层的意义

ROS2 没有强制绑定某一个具体的 DDS 实现,而是在 DDS 之上加了一层 RMW(ROS Middleware Interface)抽象层。你的 ROS2 代码不直接面对某个 DDS 厂商的 API,而是通过 rclcpp、rclpy 这些 ROS2 客户端库去调用 RMW 接口,再由 RMW 翻译成具体 DDS 实现的调用。这就是我们平时说的 RMW 实现。

这意味着你可以像切换后端一样切换 DDS。ROS2 官方维护了好几种 RMW 实现,包括 Fast DDS、Cyclone DDS、RTI Connext DDS、GurumDDS 等。你在启动节点之前设置一个环境变量,就能让整个 ROS2 系统跑在另一种 DDS 实现上。这个抽象层还有一个好处,就是如果你对默认的 DDS 性能不满意,可以只改配置、不改业务代码,就完成整条通信链路的替换。我在实际项目里就频繁切换过 Fast DDS 和 Cyclone DDS,切换之后节点代码一行没动,但通信的实时性和吞吐量确实有明显变化。

RMW 层也带来了 ROS2 的一个常见问题:两个节点如果用了不同的 RMW 实现,它们之间是发现不了对方的。这一点我在后面的章节里会详细展开,这里先记住一个原则:同一条通信链路上,所有节点的 RMW 实现必须一致,QoS 策略必须互相匹配,否则通信就会静默失败或者直接报错。

2. DDS 协议选型:主流实现怎么选

2.1 Fast DDS:开箱即用的默认项

Fast DDS 是 eProsima 主导开发的开源 DDS 实现,也是 ROS2 从早期版本到现在的默认 RMW 实现。你安装的 ROS2 只要没特别配置过 RMW_IMPLEMENTATION,系统默认走的就是 Fast DDS。

Fast DDS 的优势首先体现在生态上。ROS2 官方测试得最多的是它,教程、文档里示例代码对应的也是它。你搜 ros2 qos 的相关问题,大概率会看到 Fast DDS 相关的报错和解决方案。这意味着什么问题都能找到参考,遇到 bug 不会孤立无援。

从性能上说,Fast DDS 在单机多节点、局域网通信这些常见场景下的表现中规中矩。吞吐量的上限能吃满千兆网卡,延迟通常在几毫秒量级。对于大多数 ROS2 应用,包括导航、机械臂控制、多传感器数据聚合,这个水平是完全够用的。

Fast DDS 最明显的一个弱点是发现阶段的广播风暴。当网络里节点数量比较多的时候,初始发现过程会产生大量 UDP 多播和单播报文,如果节点不断重启,这种发现流量还会反复出现。我在一个几十个节点的系统里实测过,发现过程慢的时候要等十几秒,而且会占用不少网络带宽。这个问题可以用 Fast DDS 的发现服务器(Discovery Server)机制来优化,把发现流量集中到一个专门的服务器上,可以显著减少网络负载和启动延迟。

另外,Fast DDS 对同一域内大量 topic 的处理效率会随着 topic 数量增加而下降。如果你构建的是一个超大型系统,有几十个节点,每个节点又发布十几个 topic,那 Fast DDS 在主题匹配时的时间复杂度会明显升高。这时候就要考虑换 Cyclone DDS,或者对 Fast DDS 做进一步的配置调优。

2.2 Cyclone DDS:性能敏感场景的备选方案

Cyclone DDS 是 Eclipse 基金会旗下的开源 DDS 实现,源自 OMG 的参考实现,底层是用 C 语言写的,核心代码抽取自 Real-Time Innovations(RTI)早期捐献给 OMG 的参考代码,后来逐渐发展成一套独立的、相当有性能竞争力的实现。

Cyclone DDS 最大的卖点是性能。在相同硬件条件下,Cyclone DDS 的消息发送延迟通常比默认情况下的 Fast DDS 更低、抖动也更小。这一点在做实时控制、高频传感器流(比如激光雷达点云)的时候特别明显。我做过一个点云传输对比测试,同样是 32 线激光雷达、10Hz 的频率,Fast DDS 在 QoS 设置为 Best Effort 时偶尔会有几百微秒到几毫秒的抖动,而 Cyclone DDS 的抖动区间明显收敛得多。

除了延迟,Cyclone DDS 在内存占用上也更有优势。它的核心实现很精简,不像 Fast DDS 那样有大量 C++ 模板和特性堆叠。对于树莓派、Jetson Nano 这类资源受限的嵌入式设备,Cyclone DDS 能省下不少内存和 CPU 占用。

切换到 Cyclone DDS 很简单,只需要设置环境变量:

export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp

然后正常启动你的节点即可。注意,切换之后不要忘记在所有节点上都设置相同的环境变量。

我在实际项目里把控制类通信(比如底盘速度指令、舵机位置指令)切换到 Cyclone DDS 上,因为这类通信对延迟和抖动最敏感,而且数据量小、频率高,正好发挥 Cyclone 的长处。而一些大数据量、对实时性要求不高的传感器数据,比如相机图像、点云,仍然留给 QoS 策略里的 Best Effort 模式去处理。

2.3 RTI Connext 与 GurumDDS:看看就好

RTI Connext DDS 是老牌的商业 DDS 实现,也是 DDS 领域的行业标杆之一。它的性能、稳定性、文档都是顶级的,很多工业级系统都在用。ROS2 官方也有对应的 RMW 实现,但是 Connext 是商业软件,普通开发者需要申请许可证才能使用完整功能,一般个人项目不会去选它。如果你是在做自动驾驶、医疗机器人这类有合规要求的商用产品,那么 Connext 的成熟度和技术支持可能值这个钱,但对 ROS2 教学、原型验证、实验室研发来说,开源实现已经够用了。

GurumDDS 是韩国一家公司开发的轻量级 DDS 实现,ROS2 同样有对应的 RMW 实现。它的特点是代码量小、易裁剪,适合做嵌入式移植。如果你在裸机或者 RTOS 上跑 ROS2(比如通过 micro-ROS),GurumDDS 可能是一个值得研究的选项。但对绝大多数跑在 Linux 上的机器人项目来说,它的生态和资料比 Fast DDS 和 Cyclone DDS 要薄弱很多,遇到问题排查起来比较费劲。

做选型的时候,我给一个比较务实的建议:先在默认的 Fast DDS 上把功能跑通,如果遇到性能瓶颈或者通信不稳定的问题,再花一个下午切换到 Cyclone DDS 对比测试。大多数项目的性能瓶颈其实不在 DDS 本身,而在 QoS 配置或者网络拓扑上。不要一开始就迷信某个 DDS 实现能解决所有问题。

2.4 RMW 实现对比速查表

实现协议开源/商业延迟表现内存占用生态完整度适用场景
Fast DDSRTPS开源中等中等最高默认选择,通用场景
Cyclone DDSRTPS开源较低较低较高实时控制、资源受限设备
RTI ConnextRTPS商业最低较高极高工业级、商业交付
GurumDDSRTPS开源中等最低一般嵌入式、micro-ROS

这张表不是严格意义上的性能横评,同一实现不同配置差别也很大,所以只能作为初选时的参考。实际选型的时候,最好在自己的目标硬件上跑一轮基准测试,用你真实的数据类型和频率来测,这样得到的结果最有参考意义。

3. QoS 策略:通信稳定性的核心参数

3.1 一对最容易踩坑的组合:Reliability

QoS 策略里面对通信行为影响最大的就是 Reliability。ROS2 里它有两个取值:Reliable 和 Best Effort。

Reliable 意味着每条消息都必须被订阅端确认收到,如果发送过程中有消息丢失,发送端会重传,直到订阅端确认收到为止。这个语义和 TCP 类似,适合指令下发、状态切换这种不能丢数据的通信。

Best Effort 则相反,数据发送之后就不管了,丢了就丢了,不会重传。它适合传感器数据流,比如相机图像、激光雷达点云,因为这类数据本身是以一定频率连续产生的,偶尔丢一帧下一帧马上就跟上来,重传反而会造成数据延迟越来越大。

问题是,在 ROS2 里发布端和订阅端的 Reliability 策略必须匹配,否则通信建立不起来。匹配规则是发布端提供的 QoS 必须满足订阅端请求的 QoS:如果发布端设置的是 Best Effort,订阅端非要 Reliable,那订阅端的请求永远无法被满足,两端就会互相报 QoS incompatible 错误。

举个例子,你用 Realsense 相机驱动发布图像数据,驱动默认把图像 topic 的 Reliability 设置成了 Best Effort,然后你写了一个订阅节点,没有显式设置 QoS,用默认的 Reliable 去订阅图像。启动之后你会发现订阅端收不到任何数据,终端里到处是 QoS 匹配失败的日志。我第一次遇到这个问题的时候排查了好久,最后发现就是一行 QoS 设置的问题。

正确的做法是在订阅端把 QoS 设置为 Best Effort:

from rclpy.qos import QoSProfile, ReliabilityPolicy qos = QoSProfile( depth=1, reliability=ReliabilityPolicy.BEST_EFFORT ) subscription = self.create_subscription( Image, '/camera/image_raw', self.callback, qos )

在 C++ 里对应的写法:

rclcpp::QoS qos(1); qos.best_effort(); subscription = node->create_subscription<Image>( "/camera/image_raw", qos, callback);

那个默认的 QoS 是 Reliable,放在这里就是坑。所以在写订阅节点之前,先ros2 topic info /topic --verbose看一下发布端的 QoS 设置,再决定自己怎么配,能省掉很多排查时间。

3.2 Durability:迟到的订阅者怎么办

Durability 策略规定了当一个新的订阅者加入时,发布端之前已经发送过的历史数据要不要补送给它。这里有两个主要取值:Volatile 和 Transient Local。

Volatile 是最常见的默认策略,意思是发布端不保留历史数据,新订阅者加入后只能收到它加入之后发布的新消息。这种情况适合大多数实时数据处理场景,迟到的数据没有意义,比如实时位姿、当前状态、传感器最新数据。

Transient Local 则是发布端会在本地保留一部分历史数据,有新的订阅者加入时,会先把历史数据重放给它,然后再继续接收新数据。这个策略非常适合那些本身没有常驻发布者、只有订阅者的场景,尤其适合一些配置下发类的 topic。

典型例子是地图服务。建图节点发布一张全局栅格地图,地图生成后建图节点并不会一直以固定频率持续发布地图消息,它可能只发一次就处于静默状态。如果你使用默认的 Volatile 策略,导航模块启动时订阅地图 topic,它可能什么数据都收不到,因为发布端只在短暂的时间内发了一次地图消息,订阅端启动的时候已经过了那一阵。这种情况下,把发布端的 Durability 设置为 Transient Local,地图消息就会被保留下来,导航模块任何时候启动订阅,都能立刻收到当前全局地图。

ROS2 里设置 Durability 的 Python 示例:

from rclpy.qos import QoSProfile, DurabilityPolicy qos = QoSProfile( depth=1, durability=DurabilityPolicy.TRANSIENT_LOCAL ) map_pub = node.create_publisher(OccupancyGrid, '/map', qos)

发布端用 Transient Local,订阅端只要请求的 QoS 里 durability 不高于这个等级(也就是也请求 Transient Local 或 Volatile 均可),就能正常工作。这个细节要注意:订阅端的 durability 设置成 Volatile 也能收到 Transient Local 发布端重放的历史数据,因为发布端提供的能力比订阅端要求的高,是允许的。

3.3 History 与 Depth:队列深度的博弈

History 策略规定了发送端和接收端各自保留多少历史数据。它有 Keep Last 和 Keep All 两种模式。Keep Last 只保留最近的 N 条消息,N 就是 depth 参数;Keep All 则保留所有消息直到消费者赶上进度,这种模式对网络和内存的消耗非常大,实际项目里很少直接用,一般都会配合 depth 做限制,所以 Keep Last 是我们最常用的配置。

depth 的取值对通信行为影响很大。depth 越大,允许积压的消息越多,对突发流量的缓冲能力越强,但同时会增加内存占用和延迟。depth 过小,在消息产生速度超过消费速度的时候,消息会被直接丢弃(Best Effort 下);即使是在 Reliable 模式下,新消息也会把队列里最老的消息挤掉,订阅端还没处理的旧消息就丢了。

这里有一个常见的误区:很多人以为把 depth 设置大一点就能避免丢消息,其实增加 depth 只是给了系统更多的缓冲空间,它不能解决消费端处理速度跟不上的根本问题。如果订阅回调函数里做的计算耗时很长,比如做一次 SLAM 回环检测要几百毫秒,那么这个回调处理完之前,新来的消息已经堆积了很多,depth 就算设成 100 也顶不住。

真正处理速度瓶颈的办法是异步处理,也就是在回调函数里只做轻量级的入队操作,把耗时的计算放到另外一个线程里去执行。我在项目里处理相机图像时就采用了这个方案:回调函数把图像帧的指针放进一个无锁队列,专门的图像处理线程从队列里取数据做推理,队列深度设置为 2。这样即使算法偶尔卡顿,也不影响新的图像帧进入,旧的没处理的帧则会被覆盖掉,避免了内存无限增长。

还有一个值得一提的细节,不同 DDS 实现的 depth 语义在并发场景下略有差异。Fast DDS 里当 depth 满了且有新数据到达时,会做丢弃最旧消息还是丢弃最新消息的处理,我记得不同版本行为并不完全一致。所以不要靠深队列去解决通信问题,烦请从源头上优化消费逻辑。

3.4 其他常用 QoS 策略

除了上面三组核心策略,ROS2 的 QoS 系统还包括不少其他参数,虽然用的频率没那么高,但特定场景下会非常有用。

Deadline 策略可以用来监控两个消息之间的最大间隔。发布端声明自己在指定的时间间隔内至少发布一条消息,订阅端也可以声明自己能接受的最大发布间隔。如果发布端在 Deadline 时间里没有发布新消息,系统会触发 deadline 相关的事件回调,这在做远程状态监控和心跳检测时非常有用。比如底盘控制器节点可以发布一个状态消息,设置 deadline 为 100ms,主控节点订阅这个状态,如果超过 100ms 没有收到消息,就认为底盘通信异常,可以触发急停逻辑。

Lifespan 策略为消息设置了最大存活时间。发布端可以给消息带上一个过期时间戳,DDS 实现会在消息层级上自动判断,过期后订阅端不会再收到或者会在收到时丢弃。这个策略适合对实时性要求极高、发送过时的信息反而会造成误导的场景。

Partition 策略可以将同一个 topic 划分为不同的逻辑分区,只有同一个分区的发布者才收得到对应的消息。这个策略在做多机器人的时候基本都用不上,因为不同机器人通常直接跑不同的域 ID(domain_id),比 Partition 简单直接得多。

下面是日常开发中最常用到的 QoS 参数速查表:

参数作用建议取值典型用途
Reliability消息是否可靠送达Reliable / Best Effort指令用 Reliable,传感器流用 Best Effort
Durability是否保留历史数据Volatile / Transient Local地图、参数配置用 Transient Local
History历史消息保留策略Keep Last / Keep All默认 Keep Last
Depth队列深度1~100 根据实际压力调平衡内存和缓冲
Deadline消息最大间隔根据控制频率设定通信异常监控、心跳
Lifespan消息过期时间不超过控制周期实时状态类通信

4. 实操:在 ROS2 里正确配置 QoS 与 RMW

4.1 几种代码设置 QoS 的姿势

ROS2 的客户端库提供了丰富的 QoS 设置方式,从最简到最细都有对应 API。最常用的方式是直接构造 QoSProfile 对象,也可以直接用 rclcpp 里内置的 QoS 预设常量。

C++ 中,rclcpp::QoS 的构造函数接收一个整数表示 depth,然后可以链式调用策略设置方法:

rclcpp::QoS qos_profile(10); qos_profile.reliable(); qos_profile.transient_local(); qos_profile.keep_last(10);

如果只想快速改一个策略,其他保持默认,也可以直接传预设对象:

rclcpp::QoS qos_profile_2 = rclcpp::SensorDataQoS(); // 预置了 best_effort

这个 SensorDataQoS 预置方案非常实用,它包含 depth=5、Best Effort、Volatile、Keep Last 等策略组合,做传感器订阅时直接拿来用,省得每次都要重新配。

Python 里通过 QoSProfile 对象来构造:

from rclpy.qos import QoSProfile, ReliabilityPolicy, DurabilityPolicy, HistoryPolicy qos_profile = QoSProfile( history=HistoryPolicy.KEEP_LAST, depth=10, reliability=ReliabilityPolicy.RELIABLE, durability=DurabilityPolicy.TRANSIENT_LOCAL, ) publisher = node.create_publisher(String, 'chatter', qos_profile)

注意 Python 里的 QoSProfile 构造时还需要 history 参数,不然会使用默认的 KEEP_LAST,但 depth 如果没传,默认是 10。这个还好,但是如果只传 depth 不传 reliability,系统不会帮你自动判断,浅显的例子就是用默认 reliable 去订阅相机 topic 就掉坑里了。

4.2 RMW 的环境变量与动态切换

ROS2 的 RMW 实现选择和环境变量绑定,主要的环境变量是 RMW_IMPLEMENTATION 和 ROS_DOMAIN_ID。RMW_IMPLEMENTATION 决定用哪个 DDS 实现,ROS_DOMAIN_ID 决定使用哪个 DDS Domain。

设置 RMW_IMPLEMENTATION 时要注意,装好 ROS2 不一定就会出现你需要的 RMW 实现。Ubuntu 里安装 ros-humble-rmw-cyclonedds-cpp 之后,你才能使用 Cyclone DDS,否则设置了环境变量也没用。

sudo apt install ros-humble-rmw-cyclonedds-cpp export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp

ROS_DOMAIN_ID 的配置稍微复杂一些,它默认取值 0。如果你在同一台电脑上跑多套独立的 ROS2 系统,比如一个给导航用、一个给机械臂控制用,可以通过设置不同 domain id 来让它们互不干扰。域 ID 的取值范围是 0~101(某些实现更高),每个域的通信完全隔离。

我实际测试过,把 domain id 设置成不同的值之后,两个系统之间的节点完全发现不了彼此,topic 是隔离的。这在同一台机器上调试多家供应商提供的 ROS2 驱动时很有用,避免 node name 和 topic name 互相冲突。

有一个完整的启动脚本可以参考:

export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp export ROS_DOMAIN_ID=2 source /opt/ros/humble/setup.bash ros2 launch my_robot_bringup robot.launch.py

同一个系统里所有节点的 RMW_IMPLEMENTATION 和 ROS_DOMAIN_ID 必须一致,否则节点之间是无法发现的。在终端里跑任何 ROS2 命令之前,都要先检查这两个环境变量。

4.3 命令行查看 QoS 和通信状态

调试 QoS 问题的时候,ros2 CLI 提供了一些命令能帮我们快速看到通信状态。ros2 topic info加上 --verbose 参数,可以列出某个 topic 的发布者和订阅者,以及它们各自使用的 QoS。

ros2 topic info /camera/image_raw --verbose

输出里会显示 Publisher 和 Subscription 的 QoS 配置,包含 Reliability、Durability、Depth 等。只要对比发布端和订阅端的 QoS,就能快速判断是不是 QoS 不匹配导致的通信失败。

ros2 doctor是一个整系统健康检查工具,它可以检查当前环境的 RMW 实现、网络配置、节点发现状态等。如果系统里节点之间互相发现不了,执行ros2 doctor --report可以导出完整的诊断信息,这个信息在论坛上求助时非常有用,相当于一次把环境快照发给了对方。

想要实时看 topic 通信质量,还有一个常用的绕路方法:用ros2 topic hz统计消息频率。

ros2 topic hz /odom

如果频率远低于发布端设定的目标频率,说明存在丢包或者消息调度问题。再用ros2 topic bw看带宽占用情况,结合这两个命令的参数表现,就能大概判断是 QoS 的问题还是网络带宽的问题。

5. 常见问题与排查技巧实录

5.1 QoS incompatible 报错

现象:订阅端节点启动后,终端刷出new publisher discovered on topic /xxx, offering incompatible QoS之类的日志,数据收不到。

原因:发布端和订阅端的某些 QoS 策略不匹配。最常见的是 Reliability 不匹配,发布端是 Best Effort,订阅端是 Reliable;其次是 Durability 不匹配,发布端是 Volatile,订阅端请求 Transient Local,这在普通情况下极少见,但发布端如果配置的是 Transient Local 而订阅端需要更强的持久性,也可能会报错。

排查方法

  1. 查看发布端实际 QoS:ros2 topic info /topic --verbose
  2. 检查订阅端代码里的 QoSProfile,逐一比对 Reliability、Durability、Depth。
  3. 优先满足发布端的设置,让订阅端向发布端靠近。

这里有一个基本规则:只要发布端能够满足订阅端的请求,通信就能建立。也就是说,发布端的 QoS 等级需要不低于订阅端请求的等级。具体到策略上来讲,Reliability 里 Reliable 是较高的保证,Best Effort 是较低的保证;Durability 里 Transient Local 是较高的,Volatile 是较低的。

5.2 节点互相发现不了,但 QoS 看起来没问题

现象:两个节点在同一个域里,topic 和 QoS 完全匹配,但订阅端就是收不到数据,rostopic echo 也是空的。

原因:大概率是 RMW 实现不一致,或者网络发现被隔断了。第一优先级检查echo $RMW_IMPLEMENTATION,不同终端如果值不一样,节点之间当然互相看不见。其次是防火墙,DDS 使用 UDP 多播做发现,如果系统防火墙屏蔽了多播报文和 UDP 端口,节点之间就无法互相发现。

有一个小技巧,同一台机器上用 sys 命令打印当前 DDS 相关环境:

env | grep -E 'RMW|ROS|CYCLONEDDS|FASTRTPS'

如果 RMW、ROS 相关变量都有,但节点还是发现不了,可以检查防火墙。Ubuntu 下 ufw 如果开启,可以临时先关掉试试是否是防火墙的锅:

sudo ufw disable

如果关闭之后问题解决,那就把 DDS 需要的端口加白名单。不同 DDS 实现的默认端口不同,Fast DDS 默认使用的多播地址是 239.255.0.1,Cyclone DDS 默认多播地址也类似,端口分布在 7400~7500 和 7400~7600 区间内。这部分端口和地址在白名单里需要仔细确认,不同版本可能有差异。

5.3 传感器 topic 收不到数据,但命令 cmd_vel 可以收到

现象:cmd_vel 指令能正常收发,导航模块也能正常工作,但 LaserScan、PointCloud2 这些传感器数据在订阅端收不到。

原因:这基本是 Reliability 策略的问题。很多传感器驱动(比如 Livox、Realsense 的 ROS2 驱动)默认把传感器 data topic 设置为 Best Effort,因为数据量大、帧率高,做可靠传输会拖慢整个链路。而你的订阅节点没有显式设置 QoS,用的是默认 Reliable,所以通信匹配失败。

解决:订阅传感器 topic 的时候显式设置 QoS,按照 4.1 节的方式设置 best_effort,或者直接使用 rclcpp::SensorDataQoS() 预设。千万不要为了做实验去修改传感器驱动的 QoS,因为那是官方测试过的稳定配置。

5.4 多机通信失联

现象:A 电脑上的节点发布 topic,B 电脑上的节点订阅,但两边都启动正常,却没数据通信。

原因:多机通信比单机复杂,常见原因有:

  1. 两个机器的 domain id 不一致。
  2. 两个机器的 ROS_DOMAIN_ID 都设置成 0(默认),但因为网络隔离或防火墙阻止了广播,DDS 的多播发现报文发不出去。
  3. 两个机器的 hostname 无法互相解析,DDS 节点发现时需要知道对端的 IP,而 ROS2 里默认使用机器名来宣告地址信息。

第三点是最隐蔽的。DDS 的发现报文里包含参与者的地址信息,而参与者地址通常用 hostname 表示。如果 A 机器叫 robot-a,B 机器叫 robot-b,那么 A 发给 B 的发现报文里会写 "我是 robot-a",B 收到后尝试解析 robot-a 的主机名时解析失败,就会认为这个参与者无法到达,于是通信失败。

解决方式是在两台机器的 /etc/hosts 里加上对方的 IP 和主机名映射,让 DDS 的地址解析能够成功:

192.168.1.10 robot-a 192.168.1.11 robot-b

还有一种更直接的方式,是让 DDS 的所有通信都走单播而不是多播,并指定网卡接口。Fast DDS 里可以通过 XML 配置指定 network interface,Cyclone DDS 也提供了类似的配置项。在配置 XML 里明确指定网卡,一般能解决多机发现异常的问题。

5.5 通信性能不好,延迟高、吞吐低

现象:大数据量 topic(图像、点云)的传输延迟很高,订阅端处理速度跟不上,CPU 占用居高不下。

原因

  1. Reliability 设置成了 Reliable,大数据量下重传频繁。
  2. depth 太大,数据积压导致内存频繁分配和释放。
  3. 序列化及反序列化的 CPU 开销。ROS2 默认的 CDR 序列化实现有较大的计算开销,在处理点云时尤其明显。
  4. 使用了不合适的共享内存传输或者禁用共享内存选项,导致走了 TCP/UDP 模式(其实 ROS2 默认是 UDP)。

优化方向

  • 点云、图像类 topic 的订阅端都用 Best Effort。
  • 检查是否有共享内存传输可用。较新的 ROS2 支持 intra-process 和 shared memory 传输,对单机多节点场景有很大提升。
  • 大数据量 topic 尽量使用数据压缩,比如点云先做降采样再发布,这会减少网络带宽占用,但会增加 CPU 压缩开销,需要根据实际情况平衡。

6. 写在最后:我踩过的一些坑和一个小技巧

做 ROS2 这几个月,我在 QoS 和 DDS 上踩过的坑,比 ROS1 时代踩的所有通信坑加起来还多。最开始遇到 QoS incompatible 的时候,我以为是代码写错了,反复检查发布订阅逻辑,完全没有头绪。后来才知道是默认 QoS 和传感器驱动的 Best Effort 冲突,改一行就通了。从那以后,我养成一个习惯:任何新接手的 ROS2 项目,第一件事就是用ros2 topic info查看所有关键 topic 的 QoS 设置,把发布端的 QoS 记录下来,再同步到订阅端代码里。

还有一个细节,如果你的系统里既有 ROS1 的节点,又有 ROS2 的节点,建议用 ros1_bridge 做桥接,不要指望它们能直接通信,两个时代的通信中间件完全不是一回事。桥接那边也有类似的 QoS 匹配问题,ROS1 侧的 topic 只会被桥接成一组合理的默认 QoS,如果你的 ROS2 订阅端要求 high 等级的 QoS,还是会对不上。

最后再分享一个小技巧:如果调试时怀疑某个 topic 因为 QoS 问题收不到数据,先用ros2 topic echo手动订阅一下这个 topic。如果 CLI 能收到,说明发布端和 QoS 配置本身没问题,问题在你自己代码的订阅端;如果 CLI 也收不到,那就去查发布端和网络。用绕开自己代码的方式缩小问题范围,速度会快很多。这套思路看起来简单,真的能帮你少走几天弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询