引言
ROS 2 最根本的架构决策是把通信下沉给 DDS,自己只保留一层薄薄的抽象(RMW)。这个决策带来一个后果:ROS 2 的通信行为,尤其是「能不能连上」「延迟多少」「丢不丢包」,几乎完全由 QoS 与底层 DDS 实现决定,而不由 ROS 2 的 API 决定。
工程上的第一个难点是 QoS 的匹配规则。两个端点即使话题名与类型都一致,只要 QoS 不兼容就「静默连不上」——没有报错、没有警告,订阅者就是不收数据。这是 ROS 2 新手最迷惑的问题,也是现场最难查的一类故障。理解 QoS 的兼容矩阵是排查这类问题的前提。
第二个难点是 DDS 实现的选择。ROS 2 支持多个 RMW 实现(Fast DDS、CycloneDDS、Zenoh),它们在中低负载下行为相似,但在节点数、话题数、网络拓扑复杂时表现差异巨大。选错实现会让「系统规模一上去就启动慢、CPU 高」。
第三个难点是实时性。通信层与执行器(executor)的交互方式决定了回调的调度延迟。控制回路对通信抖动极其敏感,而 DDS 默认的线程模型与内存分配并不为实时设计。本文聚焦通信层本身,节点的生命周期、参数系统与整体架构见ROS2 架构:节点、DDS 与生命周期 ,话题/服务/动作的工程写法见 ROS2 实战:话题、服务与动作 。
目录
- 通信层在 ROS 2 中的位置
- RMW 抽象与 DDS 实现选型
- QoS 策略全解
- QoS 匹配与协商机制
- deadline、liveliness 与事件
- 发现机制:SDP、Discovery Server 与静态端点
- Zenoh 与新型 RMW
- 实时性与执行器的交互
- 零拷贝与共享内存传输
- 多机通信与网络拓扑
- 通信诊断与性能剖析
- 选型与调参清单
1. 通信层在 ROS 2 中的位置
ROS 2 的通信栈是分层的,理解每层职责才能定位问题出在哪一层。
分层结构(自下而上):
DDS 实现(Fast DDS / CycloneDDS / Zenoh)
—— 提供发布订阅、发现、序列化、传输
RMW(ROS Middleware)接口层
—— 把 DDS 的 API 抽象成 ROS 2 需要的统一接口
—— rmw_fastrtps_cpp / rmw_cyclonedds_cpp / rmw_zenoh_cpp
rcl(ROS Client Library 通用层)
—— 话题/服务/动作的通用逻辑,与语言无关
rclcpp / rclpy
—— C++ 与 Python 的客户端库,面向用户
关键认知:
ROS 2 的「话题」概念映射到 DDS 的「Topic」+「DataWriter/DataReader」
ROS 2 的「服务」映射到 DDS 的两个 Topic(请求与响应)
ROS 2 的「动作」映射到 DDS 的三个服务(目标、结果、反馈)
QoS 直接映射到 DDS 的 QoS 策略,ROS 2 只做了少量预设封装
切换 RMW 只需环境变量,不改代码:
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
因为 QoS 是「透传」到 DDS 的,所以 DDS 的 QoS 语义就是 ROS 2 的 QoS 语义。理解这一点后,很多「ROS 2 行为诡异」的问题都可以用 DDS 的文档来解释,而不是在 ROS 2 层面猜。
2. RMW 抽象与 DDS 实现选型
RMW 是 ROS 2 与 DDS 之间的适配层,选择哪个 RMW 是通信调优的第一决策。
三个主流 RMW 的定位:
1. rmw_fastrtps_cpp(Fast DDS,默认):生态最完整、工具齐全
(Discovery Server、Monitor),但默认线程模型偏激进,
节点多时线程数爆炸。适合中小规模、要官方工具
2. rmw_cyclonedds_cpp(CycloneDDS):内存与线程占用低、组播可控、
稳定性口碑好,TRANSIENT_LOCAL 样本数需显式配置。
适合资源受限平台、大规模节点、长期运行
3. rmw_zenoh_cpp(Zenoh,较新):天然支持广域网、路由与穿透。
适合跨网段、跨机房、云端协同(详见第 7 节)
实测经验(20 节点、100 话题、千兆网、空闲):
Fast DDS 默认 :CPU 约 15%,启动 3 s
CycloneDDS 默认:CPU 约 6%,启动 1.5 s
结论:节点数增长时 CycloneDDS 的差距会进一步拉大
线程数对比(每节点默认):
Fast DDS:为每个端点可能开多个线程,100 话题可达数百线程
CycloneDDS:线程模型更省,通常几十个
选型的判断顺序是「规模 → 资源 → 工具需求」。小规模(< 20 节点)用默认的 Fast DDS 即可;中大规模(> 50 节点)或资源受限平台优先 CycloneDDS;跨网络拓扑用 Zenoh。切换 RMW 后必须重跑一遍 QoS 兼容性检查,因为两者对边界情况(如 KEEP_ALL 的容量上限)的默认行为不同。
3. QoS 策略全解
QoS 策略是通信行为的完整控制面板,常用的是三项,但全部策略在特定场景都重要。
Reliability(可靠性):
RELIABLE :保证送达,失败重传(TCP 语义)
BEST_EFFORT :尽力送达,可能丢(UDP 语义)
Durability(持久性):
VOLATILE :只给当前及之后的订阅者
TRANSIENT_LOCAL:为晚加入的订阅者保留历史(地图、标定结果)
History(历史):KEEP_LAST(n) 只保留最近 n 条;KEEP_ALL 全保留
Depth(队列深度):与 History 配合,决定缓存多少条
Deadline(截止时间):数据必须在指定周期内更新,否则触发事件
Liveliness(活跃性):AUTOMATIC 由 DDS 判定存活;MANUAL_BY_TOPIC 显式声明
Lifespan(寿命):消息超过指定时间自动失效,不再投递
Ownership(所有权):SHARED 多发布者共存;EXCLUSIVE 配合 OwnershipStrength
DestinationOrder:BY_RECEPTION_TIME / BY_SOURCE_TIMESTAMP
ResourceLimits:限制历史、样本、实例数量
// 自定义 QoS:中等深度 + 可靠 + 保留历史
rclcpp::QoS qos(100); // depth = 100
qos.reliable();
qos.transient_local();
qos.deadline(rclcpp::Duration::from_seconds(0.1)); // 100ms 未更新则告警
qos.liveliness(rclcpp::LivelinessPolicy::Automatic);
qos.lifespan(rclcpp::Duration::from_seconds(1.0)); // 1s 后失效
auto pub = create_publisher<sensor_msgs::msg::PointCloud2>("/points", qos);
标准预设(覆盖 90% 场景):
rclcpp::SensorDataQoS() // BEST_EFFORT, KEEP_LAST 5, VOLATILE
rclcpp::SystemDefaultsQoS()// RELIABLE, KEEP_LAST 10, VOLATILE
rclcpp::ServicesQoS() // RELIABLE, KEEP_LAST 10(服务用)
rclcpp::ParametersQoS() // RELIABLE + TRANSIENT_LOCAL(参数事件)
选型经验:传感器数据一律 BEST_EFFORT(旧帧无价值,丢帧优于延迟);状态、命令、地图用 RELIABLE;地图与标定结果用 TRANSIENT_LOCAL(晚启动的节点也能拿到)。
4. QoS 匹配与协商机制
QoS 不兼容不会报错,只会「连不上」,理解兼容规则是排查的关键。
兼容规则(发布者的能力必须 ≥ 订阅者的要求):
Reliability:RELIABLE 发布可被 BEST_EFFORT 订阅(兼容)
BEST_EFFORT 发布不能被 RELIABLE 订阅(不兼容!)
Durability :TRANSIENT_LOCAL 发布可被 VOLATILE 订阅(兼容)
VOLATILE 发布不能被 TRANSIENT_LOCAL 订阅(不兼容)
Deadline :发布的 deadline 必须 ≤ 订阅要求的 deadline
Liveliness :发布的租约必须 ≤ 订阅要求的租约
后果:DDS 不为这对端点建立连接,数据完全不流动,
ROS 2 层面通常无任何报错(除非显式检查)
# 查看实际生效的 QoS 与端点匹配情况
ros2 topic info /scan --verbose
# Type: sensor_msgs/msg/LaserScan
# Publisher count: 1
# Node name: lidar_driver
# Reliability: BEST_EFFORT
# Durability: VOLATILE
# History (Depth): KEEP_LAST (5)
# Subscription count: 0 ← 收不到数据时先看这里
# 强制用指定 QoS 订阅,验证是否是兼容性问题
ros2 topic echo /scan --qos-reliability best_effort
调试顺序(固定,不要跳步):
1. ros2 topic list 确认话题存在
2. ros2 topic info --verbose 比对两端 QoS
3. ros2 topic hz 看发布频率
4. ros2 topic echo 看内容
跳过第 2 步直接看内容是最浪费时间的做法
一个隐蔽的坑是 depth 太小。KEEP_LAST(1) 在订阅者回调较慢时持续丢帧,而 ros2 topic hz 显示的是发布频率不是接收频率。要确认接收侧的实际速率,需要在回调里自己统计打点。
5. deadline、liveliness 与事件
除了「连不连得上」,QoS 还能表达「数据是否及时」「发布者是否还活着」这类时序语义。
Deadline 事件:
发布端 deadline 内没发出 → OFFERED_DEADLINE_MISSED
订阅端 deadline 内没收到 → REQUESTED_DEADLINE_MISSED
用途:把「传感器卡死」「控制指令超时」变成可观测事件
Liveliness 事件:
订阅端检测到发布者不再活跃 → LIVELINESS_CHANGED
用途:检测节点崩溃、网络断开,比心跳话题更底层可靠
Lifespan:消息在队列中超过 lifespan 自动丢弃
用途:避免过期指令被延迟执行(1 秒前的速度指令不该再执行)
// 订阅端监听 deadline 未命中事件
auto sub = create_subscription<sensor_msgs::msg::Image>(
"/camera/image", rclcpp::SensorDataQoS(),
[](sensor_msgs::msg::Image::ConstSharedPtr) {});
auto deadline_qos = sub->get_actual_qos().deadline();
// 通过 rclcpp 的事件机制或 rcl 的 qos_event 监听
// 也可用 ros2 topic 工具查看事件
把 deadline 当作健康监控用是 ROS 2 通信的一个高级技巧:给关键话题(如 /joint_states、/scan)设 deadline,订阅端监听 REQUESTED_DEADLINE_MISSED 事件,就能第一时间发现传感器掉线或节点卡顿,而不需要额外的看门狗话题。
6. 发现机制:SDP、Discovery Server 与静态端点
DDS 的发现机制决定节点如何互相「找到对方」,是系统规模化的瓶颈所在。
Simple Discovery Protocol(SDP,默认):
节点通过组播周期性宣告自己的端点,节点少时良好,多时组播风暴
节点数与启动时间(实测经验值):
5 个 < 1 s;20 个 2~5 s;50 个 10~30 s(开始丢包);
100 个分钟级,甚至发现失败
三种应对手段(侵入性递增):
1. 限制组播到指定网卡(避免跨网段泄漏)
2. Discovery Server(集中式发现,节点只与服务器交互)
3. 静态端点(initialPeersList,拓扑固定时最可靠)
# 方案一:限制组播到指定网卡
export CYCLONEDDS_URI='<CycloneDDS><Domain><General>
<NetworkInterfaceAddress>eth0</NetworkInterfaceAddress>
<AllowMulticast>spdp</AllowMulticast>
</General></Domain></CycloneDDS>'
# 方案二:Discovery Server(ROS 2 内置支持,以 CycloneDDS 为例)
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
export ROS_DISCOVERY_SERVER=192.168.1.10:11811
ros2 run rmw_cyclonedds_cpp ddsperf # 或 fastdds discovery -i 0
# 方案三:静态端点(Fast DDS 的 initialPeersList,写在 XML 里)
export FASTRTPS_DEFAULT_PROFILES_FILE=/etc/ros/fastdds_profile.xml
三种机制的取舍:
SDP :零配置、小规模最好;大规模不可用
Discovery Server:集中式、可跨网段;引入单点(需冗余)
静态端点 :最可靠、无发现开销;新增节点要改配置
跨网段或 VPN 场景下组播通常不可用,必须用 Discovery Server 或静态端点。这也是 Zenoh 的用武之地(见下一节)。域 ID(ROS_DOMAIN_ID)是最简单的隔离手段:不同机器人用不同域 ID,避免互相发现;机器人域规划建议 010 留给开发、11100 留给机器人编号、101+ 留给工具。
7. Zenoh 与新型 RMW
Zenoh 是为「广域、异构、动态」网络设计的协议,作为 ROS 2 的 RMW 解决了传统 DDS 的若干痛点。
Zenoh 相对 DDS 的差异:
1. 无组播依赖:用 router 做发现与转发,天然穿透 NAT
2. 跨网络:支持互联网、5G、VPN,节点无需在同一二层网络
3. 资源占用低:为嵌入式与大规模 IoT 设计
4. 路由与桥接:Zenoh router 可桥接不同域、不同网络
使用方式:
export RMW_IMPLEMENTATION=rmw_zenoh_cpp
ros2 run rmw_zenoh_cpp rmw_zenohd # 启动 router
取舍:优点是跨网段天然支持、资源省、可路由;
缺点是生态较新、部分 QoS 语义支持待完善、引入 router 需运维
Zenoh 与传统 DDS 不是替代关系而是互补:单机与局域网内用 DDS(成熟、工具全),跨网段与云端协作用 Zenoh。混合部署时可用 Zenoh 的 DDS 桥接,让两套网络互通。选型时务必实测目标场景下的延迟与吞吐,不要只看宣传数字。
8. 实时性与执行器的交互
通信层与执行器(executor)的交互方式决定回调的调度延迟,是实时控制的关键。
回调延迟的构成:
1. DDS 接收线程收到数据(DDS 内部线程,优先级通常不可控)
2. 放入等待集合(waitset)
3. 执行器线程取出并调用回调
4. 回调执行时间
延迟的主要来源:
DDS 内部线程的调度延迟(可能被低优先级线程抢占)
执行器的线程模型(单线程 vs 多线程)
回调组配置(互斥 vs 可重入)
内存分配与锁竞争(回调内的动态分配)
实时通信的三条纪律:
1. 控制回调必须绑定到隔离核(CPU 亲和性),而非靠调度碰运气
2. 控制回调内禁止动态分配、禁止日志、禁止阻塞调用
3. 用 StaticSingleThreadedExecutor 减少每轮遍历开销
(订阅关系固定时最快,因为它预建了回调到执行的映射)
// 实时控制节点的执行器配置
rclcpp::NodeOptions opts;
opts.use_intra_process_comms(true); // 同进程零拷贝
auto node = std::make_shared<ControlNode>(opts);
rclcpp::executors::StaticSingleThreadedExecutor exec;
exec.add_node(node);
// 回调组:控制相关用 MutuallyExclusive,避免并发访问共享状态
// 感知相关用 Reentrant 提升吞吐(见架构一篇)
exec.spin();
通信抖动必须在端到端上测量,而不是只看 DDS 或只看回调。用 ROS 2 的 tracing(ros2_tracing、LTTng)能端到端追踪从「发布调用」到「订阅回调返回」的完整链路,定位延迟在哪个环节累积。这部分与机器人实时控制与嵌入式一篇的控制周期预算直接相关。
9. 零拷贝与共享内存传输
大消息(点云、图像)的通信成本主要是内存拷贝,零拷贝是提升吞吐的关键手段。
三个层次的「零拷贝」:
1. 进程内通信(Intra-process):同进程节点绕过 DDS 直接传指针
要求 use_intra_process_comms(true) + unique_ptr 发布,
durability 必须为 VOLATILE
2. Loaned Messages:从 DDS 预分配内存池借一块填充后发布,
避免构造→序列化→拷贝的多次分配,适合高频定长消息
3. 共享内存传输:跨进程但同机时用共享内存而非网络回环
Iceoryx / Fast DDS SHM,适合大消息(图像、点云)跨进程高频传输
// 进程内通信:必须用 unique_ptr 发布,否则退化为拷贝
auto msg = std::make_unique<sensor_msgs::msg::PointCloud2>();
fillPointCloud(*msg);
pub_->publish(std::move(msg)); // 移动语义,零拷贝
// 借出消息(Fast DDS 风格伪码)
auto loaned = pub_->borrow_loaned_message();
fill(*loaned);
pub_->publish(std::move(loaned));
验证零拷贝是否生效:
ros2 topic info /points --verbose
看端点是否标记为 intra-process;intra-process 下 Publisher count
可能显示为 0 但数据仍在流动——这是最容易误判的一点
用 ros2 topic hz 确认数据实际到达更可靠
带宽也要算清楚:一路 10 Hz 的 64 线雷达点云约 2 MB/帧,未压缩就是 160 Mbps,三路相机轻松超过 1 Gbps。对策是分层:同机走共享内存,跨机只传降采样后的特征或压缩图像。
10. 多机通信与网络拓扑
多机器人系统的通信比单机复杂得多,网络拓扑是设计的一部分。
多机部署的三个必配项:
ROS_DOMAIN_ID :区分逻辑域,不同机器人不同 ID,避免互相发现
ROS_LOCALHOST_ONLY=1 :单机调试时避免污染共享网络
ROS_STATIC_PEERS / Discovery Server:跨网段通信
网络拓扑的三种形态:
1. 扁平二层网络(同一交换机):组播可用,SDP 即可,< 50 节点
2. 路由网络(跨网段):组播通常不可用,须 Discovery Server/静态端点/Zenoh
3. 星型(中心 + 边缘):中心做 Discovery Server 与汇聚,适合车队与云端
带宽与延迟的预算:
同交换机 < 1 ms;跨交换机 1~5 ms;WiFi 5~50 ms 且抖动大;
蜂窝/广域几十到几百 ms
无线策略:传感器用 BEST_EFFORT + 大 depth,感知尽量下沉到本地
无线网络是 ROS 2 通信的痛点。组播在 WiFi 上经常不可靠,SDP 会发现失败;TCP 语义的 RELIABLE 在丢包时重传导致延迟尖峰。无线场景优先考虑 Zenoh 或 Discovery Server,并接受「传感器数据丢帧」,而不是强行要求可靠传输。
11. 通信诊断与性能剖析
通信问题必须可观测,否则只能在出事后倒推。
常用诊断工具:
ros2 topic info --verbose :端点、QoS、类型、匹配情况
ros2 topic hz / bw :发布频率 / 带宽占用
ros2 topic delay :端到端延迟(需时间戳)
ros2 doctor --report :整体健康检查
ddsperf(CycloneDDS) :底层 DDS 吞吐与延迟基准
ros2 topic bw /points # 逐话题看带宽,定位「谁吃满网络」
ros2 topic delay /scan # 看端到端延迟分布
ros2 doctor --report # 整体诊断
ddsperf -c -n 10 -s 1024 -r 1Hz pub sub # 底层 DDS 基准
关键监控指标:
每个话题的实际接收频率(对比发布频率,看丢帧率)
端到端延迟的 p50/p99(不是均值)
带宽占用(是否接近网卡上限)
DDS 线程数与 CPU 占用(发现线程泄漏)、发现耗时
延迟与频率要按话题分别监控,因为不同话题的预算不同。控制话题要求延迟 < 5 ms、频率稳定;感知话题可以接受更大的延迟与抖动。把两者混在一起看会掩盖问题。这套可观测性设计与机器人软件栈全景 中的可观测性章节是同一套方法论。
12. 选型与调参清单
把通信层的决策收敛成一份可执行的清单。
RMW 选型:
节点 < 20、要官方工具 → rmw_fastrtps_cpp(默认)
节点 > 50 或资源受限 → rmw_cyclonedds_cpp
跨网段 / 云端协同 → rmw_zenoh_cpp
QoS 配置:
传感器数据 → SensorDataQoS(BEST_EFFORT, KEEP_LAST 5)
状态与命令 → RELIABLE + KEEP_LAST(10)
地图与标定 → RELIABLE + TRANSIENT_LOCAL + KEEP_LAST(1)
静态变换 → RELIABLE + TRANSIENT_LOCAL + KEEP_ALL
发现机制:< 20 节点用 SDP;> 50 用 Discovery Server;
拓扑固定用静态端点;跨网段用 Discovery Server 或 Zenoh
性能:同进程大消息用 intra-process + unique_ptr;跨进程同机用共享内存;
实时控制用隔离核 + 静态单线程执行器 + 回调内无分配
调参优先级(从高到低):
1. RMW 实现(影响最大,改环境变量即可)
2. QoS 匹配(连不上先查这里)
3. 发现机制(规模上不去先查这里)
4. 零拷贝与共享内存(大消息吞吐)
5. 执行器与回调组(实时性)
权衡取舍
| 决策 | 选 A | 选 B |
|---|---|---|
| RMW | Fast DDS:默认、工具全 | CycloneDDS:省资源、大规模稳 |
| 可靠性 | RELIABLE:不丢、可能延迟 | BEST_EFFORT:低延迟、可丢 |
| 持久性 | VOLATILE:省内存 | TRANSIENT_LOCAL:晚启动能拿到 |
| 发现 | SDP:零配置、小规模 | Discovery Server:大规模、跨网段 |
| 网络 | DDS:局域网成熟 | Zenoh:跨网段、云端 |
| 大消息 | 网络回环:简单 | 共享内存:快、需配置 |
| 执行器 | 多线程:并发高 | 静态单线程:低开销、实时 |
核心原则:通信配置由「数据的重要性」与「网络的可靠性」共同决定。重要且网络可靠的数据用 RELIABLE + 小 depth;不重要或网络不可靠的数据用 BEST_EFFORT + 大 depth。把这两条想清楚,绝大多数 QoS 决策就有了依据。
常见坑清单
- 发布者 BEST_EFFORT、订阅者 RELIABLE,静默收不到数据——
ros2 topic info --verbose比对两端 QoS。 - 忘记
TRANSIENT_LOCAL,晚启动的节点拿不到地图——地图与标定结果必须配 durability。 - 节点数过百仍用组播 SDP,启动耗时分钟级——改用 Discovery Server 或静态端点。
- Fast DDS 默认线程模型在节点多时线程数爆炸,CPU 飙升——换 CycloneDDS 或限制线程池。
- intra-process 下用了拷贝发布而非 unique_ptr,零拷贝退化为拷贝——必须
publish(std::move(msg))。 - intra-process 配了非 VOLATILE 的 durability,行为未定义——进程内通信的 durability 必须为 VOLATILE。
KEEP_LAST(1)且回调慢,持续丢帧却看不出来——hz显示的是发布频率,需在回调侧统计。- 无线网络强行用 RELIABLE,丢包重传导致延迟尖峰——传感器数据用 BEST_EFFORT。
- 多机器人同域 ID 互相发现,话题串台——规划域 ID 并显式设置
ROS_DOMAIN_ID。 - 切换 RMW 后不重跑兼容性检查,边界情况行为不同——切换后必须重验 QoS。
小结
ROS 2 通信的核心是「QoS 决定行为,RMW 决定性能」。QoS 的三个常用策略(Reliability、Durability、History)覆盖大多数场景,而 deadline、liveliness、lifespan 在健康监控与安全语义上有独特价值。RMW 的选择在系统规模化时影响巨大,Fast DDS 与 CycloneDDS 是主流,Zenoh 在跨网段场景有独特优势。
下一步建议阅读ROS2 架构:节点、DDS 与生命周期 ,理解通信层之上的节点、生命周期与执行器模型;ROS2 实战一篇讲清三种通信模式的工程写法;机器人实时控制与嵌入式一篇把通信延迟纳入控制周期预算。
最后一句经验:「收不到数据」先查 QoS 匹配,「系统规模上不去」先查发现机制。这两类问题占了 ROS 2 通信故障的绝大多数,而它们都有确定的排查方法。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。