第十部分:ROS2工程化实践(第47-51周)¶
定位:软件工程/ROS2高级集成 建立了 ROS2 核心运行时概念(Lifecycle/Composition/QoS/Executor/tf2/DDS),本部分在此基础上深入 ROS2 的工程实践层——从设计哲学理解、构建系统掌握、机器人建模、生态工具使用,到硬件集成与部署调试。
ROS2 设计哲学、架构演进与版本生态¶
难度:⭐~⭐⭐⭐ | 建议用时:1 周 | 前置要求:软件工程/ROS2高级集成(ROS2 运行时特性)、基础 Linux 操作
教学目标:理解ROS2为什么设计成这样——从ROS1的五大架构缺陷到DDS选型决策、Zenoh新兴替代、版本生态演进与迁移策略。软件工程/ROS2高级集成已讲解了ROS2运行时特性(Lifecycle/Composition/QoS/Executor),本章深入其背后的设计哲学与工程决策。
前置自测¶
📋 答不出 ≥ 2 题 → 先回 软件工程/ROS2高级集成 复习
- [软件工程/ROS2高级集成] ROS2 中 QoS 的 Reliability 策略有哪两种?它们的语义差异是什么?
- [软件工程/ROS2高级集成] Lifecycle 节点的四个主要状态是什么?为什么需要显式状态管理?
- [网络基础] 什么是 UDP 多播(multicast)?它与单播(unicast)在网络发现中有什么不同?
- [软件工程/ROS2高级集成] ROS2 的 Component Composition 解决了 ROS1 时代的什么问题?
- [通用] 什么是”中间件”?为什么分布式系统通常需要中间件层?
本章目标¶
学完本章后,你应该能够:
- 解释 ROS1 的五大默认假设分别在什么场景下失效,以及 ROS2 如何通过 DDS、QoS、Lifecycle、Composition 和 RMW 应对
- 区分 DDS 与 Zenoh 的网络假设差异,能根据项目场景选择合适的 RMW
- 使用 版本选择决策流程为新项目选定 ROS2 发行版和 RMW
- 评估 一个 ROS1 项目的迁移工作量,并制定分阶段迁移计划
0. 先回答一个问题:为什么不是”修补 ROS1” ⭐¶
学习 ROS2 时,最容易产生的疑问是:ROS1 已经能跑大量机器人,为什么还要引入 DDS、QoS、RMW、Lifecycle、Component、Domain ID 和一套新的 launch/parameter 机制?如果只从 API 角度看,ROS2 确实显得更复杂;但如果从机器人系统的约束看,ROS2 的复杂度并不是凭空增加,而是把 ROS1 中被隐藏的工程问题显式化。
ROS1 的默认世界可以概括为:
这个世界对实验室机器人非常友好。学生可以快速写节点、发话题、录 bag、接 RViz;但当机器人进入工厂、仓库、户外车队、嵌入式设备和安全敏感环境时,原来的假设逐个失效。
| ROS1 默认假设 | 真实部署中的反例 | ROS2 对应设计 |
|---|---|---|
| 有中心 master | 多机器人、弱网络、局部断连 | DDS 分布式发现 |
| TCPROS 足够 | 传感器流、控制命令、地图缓存需求不同 | QoS 策略 |
| 节点默认可信 | 工业和车队环境需要访问控制 | DDS-Security / SROS2 |
| 进程边界无所谓 | 点云和图像拷贝很重 | Component / 进程内通信 |
| 桌面 Linux 足够 | MCU、实时 Linux、嵌入式部署 | micro-ROS / RMW 抽象 |
| 启动顺序靠 launch | 生产系统需要可控启停 | Lifecycle |
这张表给出本章主线:ROS2 不是在 ROS1 上堆新功能,而是把机器人系统从“研究脚本集合”推向“可部署分布式系统”。
0.1 如果继续修补 ROS1 会怎样 ⭐¶
反事实地看,如果继续沿用 ROS1 架构,只在局部修补,会遇到几类难题:
-
多机器人发现难题
rosmaster是单点协调者。可以通过多 master 桥接缓解,但桥接层需要重新处理命名空间、话题同步、网络断连和冲突恢复。系统越大,桥接逻辑越像重新实现一套中间件。 -
通信语义不足
ROS1 的queue_size和latch无法表达“传感器数据允许丢帧但必须低延迟”“地图需要后加入订阅者立刻收到”“控制命令过期后不应继续执行”等语义。开发者只能在节点内部手写补偿逻辑。 -
嵌入式边界困难
ROS1 协议栈对 MCU 不友好,嵌入式硬件通常要通过主机侧驱动节点代理。这让底层硬件不能自然参与 ROS 图,也不利于端到端系统建模。 -
安全模型缺失
研究环境中默认可信很方便,但生产环境中需要身份认证、权限控制和加密。把安全性事后接到原架构上,会比从通信中间件层设计更困难。
因此,ROS2 选择引入 DDS,不是因为 DDS API 更漂亮,而是因为 DDS 已经把分布式发现、QoS、安全和实时通信作为标准问题处理。
0.2 本章知识树¶
ROS1 的失效假设
↓
DDS:分布式发现 + QoS + 标准化传输
↓
RMW:隔离 ROS2 与具体中间件供应商
↓
QoS / Executor / Lifecycle / Component
↓
版本生态:LTS、非 LTS、RMW 默认值、Zenoh
↓
工程选型:机器人上到底该用哪个发行版和哪个 RMW
读本章时要始终记住:每个机制都在回答一个工程问题,而不是单纯增加 API。
⚠️ 常见陷阱¶
⚠️ 概念误区:认为 ROS2 只是 ROS1 的 API 升级
新手想法:"ROS2 就是换了一套函数名,底层逻辑没变。"
现象/后果:迁移时只改函数签名,忽略 QoS 配置、生命周期管理和通信语义差异。结果是节点能启动,但数据不流动、状态不可控、多机器人场景频繁断连。
根本原因:ROS2 不是 API 替换,而是通信模型、部署模型和安全模型的全面升级。QoS、Lifecycle、DDS 分布式发现都是新增的一等概念,不是可选装饰。
正确做法:迁移时首先理解 ROS2 解决了 ROS1 的哪些假设失效问题,然后逐系统适配通信语义、生命周期和安全策略,而不是逐行替换函数名。
⚠️ 思维陷阱:认为"rosmaster 更简单所以更好"
新手想法:"ROS1 只要启动一个 master 就能发现所有节点,多简单。DDS 那套协议太复杂了。"
现象/后果:在单台机器人上确实如此,但多机器人、弱网络、跨子网环境中,master 成为瓶颈和单点故障。多 master 桥接的同步逻辑比 DDS 发现协议更复杂。
根本原因:单机简单与分布式可靠是不同的设计目标。中心化发现在规模小时省事,在规模大时成为债务。
正确做法:根据部署规模选择架构。单机实验用默认配置即可;多机器人或弱网络场景要理解 DDS 发现机制或 Zenoh 路由。
⚠️ 工程陷阱:在 ROS2 中用业务代码模拟 QoS 行为
新手想法:"我在回调里手动丢弃旧帧、手动检查时间戳、手动缓存最后一帧给新订阅者,不需要学 QoS。"
现象/后果:每个节点各写一套重传、缓存、超时逻辑,代码重复且容易出 bug。不同节点的实现不一致,系统行为不可预测。
根本原因:这些逻辑本质上属于通信语义,应在通信层统一处理。ROS2 的 QoS 就是把这些从业务代码中抽出来变成发布者-订阅者之间的协议。
正确做法:优先使用 DDS QoS 策略表达通信需求。只在 QoS 无法覆盖的极端场景(如自定义超时-恢复逻辑)才在业务代码中补充。
1. 催生 ROS2 的五大压力 ⭐⭐¶
这一节解决什么问题:从 ROS1 的部署实践中提炼出五类架构债务,说明为什么 OSRF 选择重新构建而不是在旧架构上修补。
1.1 PR2 时代的合理默认 ⭐⭐¶
ROS1 的设计围绕 Willow Garage 的 PR2 机器人展开:单台机器人、工作站级别的机载计算能力、优质的有线或近距离 WiFi 连接、研究用途、无实时要求。design.ros2.org/articles/why_ros2.html 文档明确列举了随着社区转向生产环境而失效的假设:机器人团队、MCU 级计算能力、实时控制、网络退化(地面至太空的 WiFi)、已部署的产品以及安全性。
这些默认在 2007-2010 年的实验室里并不荒唐。单台 PR2 在有线局域网中运行,研究人员需要的是快速拼接模块而不是分布式容错。但当机器人进入仓库、车队、嵌入式设备和安全敏感环境时,每一个默认都变成了架构债务。
1.2 五类架构债务的具体表现 ⭐⭐¶
rosmaster 是一个集中式的 XML-RPC 服务器——每个节点都需要它进行发现,而其 XML-RPC 协议过于递归且无边界,无法在微控制器上运行,这就是为什么 ROS1 嵌入式硬件必须始终由主机侧驱动程序节点进行封装。传输协议硬编码为 TCPROS/UDPROS,除了队列深度和一个 latch 标志外,没有任何 QoS 保障。序列化采用的是定制二进制格式,没有标准化的 IDL,这使得跨语言和跨厂商的审核实际上无法实现。多机器人协作被描述为”在单主架构之上的某种权宜之计”(设计文档原话),通常通过 multimaster_fkie 来实现。缺乏安全性——灵活性被置于一切之上。
五类债务可以按系统层次归类,每一类都对应 ROS2 中一个显式的设计响应:
| 债务层次 | ROS1 的具体限制 | 失效时的工程后果 | ROS2 的设计响应 |
|---|---|---|---|
| 发现与协调 | rosmaster 单点 | 多机器人同步膨胀,嵌入式无法参与 | DDS 分布式发现 |
| 传输语义 | TCPROS + queue_size + latch | 每个节点各写一套缓存/超时逻辑 | QoS 策略体系 |
| 序列化与互操作 | 自定义二进制格式 | 跨语言、跨厂商审计困难 | CDR + IDL 标准化 |
| 进程与资源模型 | 一进程一节点 + nodelet | 大消息拷贝开销、双 API 维护 | Composition + 进程内通信 |
| 安全与部署 | 无内建安全 | 工业/车队环境无法满足合规 | DDS-Security / SROS2 |
1.3 OSRF 的决策:重建而非修补 ⭐⭐¶
OSRF 并未对现有架构进行修补,而是选择了”将 ROS2 构建为一组并行的软件包”,并明确采用了成熟的外部标准——Zeroconf、Protocol Buffers、ZeroMQ、WebSockets 和 DDS 均经过评估;最终 DDS 胜出。ZeroMQ 被明确排除,因为它将迫使 ROS 重新实现 DDS 已提供的发现、序列化和 QoS 功能——这等于在应用层重造一套中间件。
这个决策的核心逻辑是”买而不是造”。DDS 在军事、金融和航空电子系统中已经验证了二十年,它不是 OSRF 发明的新技术,而是一个被工业实践打磨过的标准。ROS2 的创新不在于通信协议本身,而在于用 RMW 抽象把工业级通信标准包装成机器人友好的 API。
1.4 版本发布时间线 ⭐¶
时间线记录了这一决策的演进:Alpha 2014 → Beta r2b2/r2b3 → Ardent Apalone 2017 年 12 月(首个稳定版)→ Bouncy → Crystal → Dashing(首个类似 LTS 的版本)→ Eloquent → Foxy 2020 年 5 月(首个真正的 3 年 LTS)→ Galactic(尝试将 CycloneDDS 设为默认)→ Humble 2022 年 5 月(LTS,EOL 2027 年 5 月,恢复 Fast DDS 为默认)→ Iron → Jazzy 2024 年 5 月(LTS,2029 年 5 月终止支持,Ubuntu 24.04)→ Kilted 2025 年 5 月(非 LTS,2026 年 11 月终止支持,将 rmw_zenoh_cpp 提升至 Tier-1)→ Lyrical Luth(2026 年 5 月 LTS,官方文档已列出 Ubuntu 26.04、C++20 与 Tier-1 RMW 支持,依据 https://docs.ros.org/en/lyrical/Releases/Release-Lyrical-Luth.html)。完整发布周期已编入 REP-2000(https://www.ros.org/reps/rep-2000.html):每年 5 月发布,逢双数年发布 LTS 版本,LTS 支持周期 5 年,非 LTS 支持周期 18 个月,每个发行版仅有一个一级 Ubuntu 版本。
反事实推理:如果 OSRF 选择了 ZeroMQ 而不是 DDS,ROS2 团队需要自己实现发现协议、QoS 策略集、安全认证和 IDL 编译器。这不是”技术上不可能”,而是维护成本不可持续——一个开源机器人社区无法与 OMG 标准组织 + RTI/eProsima/Eclipse 等商业实体的二十年累积竞争。DDS 的价值不是”API 更好”,而是”标准化免除了重复建设”。
⚠️ 常见陷阱¶
⚠️ 概念误区:认为 ROS2 的复杂度是”过度设计”
新手想法:”ROS1 又简单又能跑,ROS2 搞 DDS、QoS、Lifecycle 那么复杂,是不是过度设计?”
现象/后果:用 ROS2 时只用最简单的默认配置,忽略 QoS、Lifecycle 和 Composition,结果部署到多机器人或弱网络时问题频发。
根本原因:ROS2 的复杂度不是凭空增加的,而是把 ROS1 隐藏在默认值里的工程问题显式化了。在 ROS1 中你也会遇到这些问题(通信语义、进程间拷贝、安全性),只是被推迟到了部署阶段。
正确做法:把 ROS2 的 QoS、Lifecycle、Composition 理解为”ROS1 中你迟早要手写的补偿逻辑的标准化版本”,而不是”额外的学习负担”。
练习¶
- [调研题] 阅读
design.ros2.org/articles/why_ros2.html,列出文档中明确提到的 ROS1 失效假设。与本节的五类分类对照,看是否有遗漏。 - [分析题] 假设你在 2015 年被问”为什么不直接给 ROS1 加 QoS 支持”,从架构角度分析这样做的技术困难(提示:考虑 TCPROS 的固定格式和 rosmaster 的协调模型)。
- [思考题] ROS2 的 REP-2000 规定每年 5 月发布一个新版本。为什么要固定发布节奏,而不是”功能做完了就发”?从用户(机器人产品开发者)的角度分析。
2. DDS:为何中间件才是关键 ⭐⭐¶
ROS 2 最大的单一决策是将 ROS 1 堆栈(rosmaster + TCPROS + 自定义序列化)替换为 DDS——即 OMG 数据分发服务(OMG Data Distribution Service),该标准于 2004 年正式确立,融合了 RTI NDDS、PrismTech OpenSplice 以及其他几种商业实时发布/订阅中间件。 该技术已被应用于战列舰、水坝、金融系统、AUTOSAR Adaptive 以及 FACE 航空电子框架中。 OSRF的决策依据详见 https://design.ros2.org/articles/ros_on_dds.html::DDS提供了**已发布的端到端规范**(因此第三方可进行审核/重新实现)、默认的分布式发现机制(无单点故障)、丰富的端点级QoS、 基于 UDP 的 DDSI-RTPS(从嵌入式系统到地空系统均可运行)、供应商独立性,以及在任务关键型系统中的实战验证。公认的缺点是 API 复杂度较高以及静态 IDL 编译步骤较为繁重——ROS 2 通过 rclcpp/rclpy 隐藏了这两点。 ZeroMQ 被明确排除,因为它将迫使 ROS 重新实现 DDS 已提供的发现、序列化和 QoS 功能。
发现机制通过 SPDP + SEDP 实现。**每个 DDS 参与者会定期向一个已知地址多播 ParticipantBuiltinTopicData 公告(简单参与者发现协议); 一旦参与者相互识别,它们便通过可靠的单播内置主题(DCPSPublication、DCPSSubscription、DCPSTopic — 简单端点发现协议)交换发布/订阅/主题/类型/QoS信息。这就是为什么 ros2 topic list 无需运行任何守护进程即可工作。 ROS_DOMAIN_ID 通过 DDSI-RTPS 中定义的 UDP 端口运算(即 Port = PB + DG·D + offset 与 PB=7400, DG=250, PG=2 的组合)在传输层隔离参与者,因此域 0 的发现多播端口为 **7400,用户多播端口为 7401,参与者 0 的单播端口为 7410/7411。 安全范围为 0–101,因为 16 位端口空间和每主机约 120 个参与者的上限会导致较高域与下一域的多播端口发生冲突。不同的域 ID 可在 DDS 层实现真正的图隔离,但**这并非安全边界**——数据包仍会穿越网络。生产环境隔离需要 SROS2/DDS-Security。 **主题/类型名称会根据 https://design.ros2.org/articles/topic_and_service_names.html: 映射到 DDS 名称上,/chatter 将变为 DDS 主题 rt/chatter (rt = ROS 主题,rq/rr = 服务请求/响应,rs = 参数),而 std_msgs/msg/String 变为 std_msgs::msg::dds_::String_。这解释了所有“为什么我的 DDS 嗅探器显示奇怪的主题名称”的情况。
序列化迁移至 CDR(通用数据表示法),即作为 DDSI-RTPS 负载传输的 OMG 二进制格式。经测算,将 ROS 原生 C++/Python 结构体逐字段复制到 CDR 映射的开销,相较于序列化本身可忽略不计。正是 CDR 实现了跨厂商互操作,也正是因此,非 ROS 的 DDS 系统能够消费 ROS 2 主题。
本质洞察:DDS 不是"让 ROS2 API 更现代"的工具,而是把机器人中间件从"实验室自建"推向"工业标准采购"。就像汽车行业不会自己造轮胎一样,ROS2 不再自己造通信栈——DDS 已经在军事、金融、航空电子系统中验证了二十年。ROS2 的创新不在于通信协议本身,而在于用 RMW 抽象把工业级通信标准包装成机器人友好的 API。
练习¶
- [调研题] 阅读
design.ros2.org/articles/ros_on_dds.html,列出 DDS 相对 ZeroMQ 的 5 个优势,并解释为什么 OSRF 最终没有选择 ZeroMQ。 - [计算题] 假设
ROS_DOMAIN_ID=42,按照 DDSI-RTPS 端口公式Port = PB + DG*D + offset(PB=7400, DG=250),计算该域的发现多播端口和用户多播端口。 - [思考题] 如果一个 ROS2 系统只在本机运行且不需要多机器人,DDS 的分布式发现是否是"浪费"?从工程扩展性角度分析。
RMW 抽象层与中间件生态 ⭐⭐¶
为确保 ROS 独立于任何单一 DDS 供应商,该栈引入了 RMW(ROS 中间件)接口 —— 这是一个纯 C 语言 API,文档详见 https://design.ros2.org/articles/ros_middleware_interface.html,,位于 rcl(C 通用客户端库)与供应商之间。完整分层结构见 user code → rclcpp / rclpy → rcl → rmw → rmw_<vendor> → vendor library → UDP/TCP/SHM。 供应商切换由 export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp 负责——rmw_implementation 包会在启动时加载对应的 librmw_*.so。RMW 存在的另一个原因是:在 DDS 领域中 Python 绑定实现较为薄弱,因此 rclpy 直接封装了 rcl 的 C API,而非绑定任何 DDS 供应商的 Python 实现。
截至 2026 年 5 月 13 日,重要的实现包括:
| RMW | 供应商 | 仓库 | 备注 |
|---|---|---|---|
rmw_fastrtps_cpp |
eProsima Fast DDS | https://github.com/eProsima/Fast-DDS | 大多数发行版的默认选项; Kilted 更名的包 fastrtps→fastdds |
rmw_cyclonedds_cpp |
Eclipse Cyclone DDS | https://github.com/eclipse-cyclonedds/cyclonedds | 仅在 Galactic 中默认;配置更简单,因可靠性高而广受 Nav2/Autoware 社区青睐 |
rmw_connextdds |
RTI Connext(商业版) | https://github.com/ros2/rmw_connextdds | 工业级/可安全认证 |
rmw_zenoh_cpp |
Eclipse Zenoh(非DDS!) | https://github.com/ros2/rmw_zenoh | Kilted中的Tier-1;流言发现机制,无需多播 |
rmw_gurumdds_cpp |
GurumDDS (KR) | https://github.com/ros2/rmw_gurumdds | 采用率低 |
rmw_iceoryx |
Eclipse Iceoryx | — | 仅支持零拷贝SHM;功能有限 |
默认的RMW(推荐实现)曾多次变动:先是通过Foxy采用Fast-RTPS,随后在**Galactic中采用CycloneDDS**(社区出于对FastDDS稳定性的担忧而更倾向于此),待eProsima解决相关问题后,又在**Humble+中恢复为Fast DDS**。https://osrf.github.io/TSC-RMW-Reports/处的年度OSRF TSC RMW评估报告记录了每次变更,并附有基准测试数据和未解决问题数量。 当多播被阻断时(如 WiFi、Docker 桥接、企业网络、VPN),Fast DDS 提供了一个 发现服务器(export ROS_DISCOVERY_SERVER=IP:11811,通过 fastdds discovery 启动),其作为单播会合点;内省工具需要 SUPER_CLIENT 配置文件。 CycloneDDS 没有这一概念——它接受 CYCLONEDDS_URI XML 中的显式单播对等方列表。
Zenoh,新兴的替代方案 ⭐⭐⭐¶
Zenoh 并非 DDS。 它是由 ZettaScale 发起的一个 Eclipse 基金会项目(与 OpenSplice/Cyclone 同源),采用 Rust 语言实现并提供 C++ 绑定,支持通过 TCP/UDP/QUIC/WebSocket/蓝牙/串行接口提供发布/订阅、查询及分布式存储功能。 rmw_zenoh_cpp 已于 2025年5月23日晋升为 Kilted Kaiju 中的 Tier-1 项目 —— 这意味着它通过了完整的核心测试套件,支持所有 Tier-1 操作系统,并集成了完整的 SROS2 安全套件。 发现机制采用**通过 rmw_zenohd 路由器进行闲聊(gossip)而非多播,因此可在 k8s 环境中、NAT 后端、WiFi 网络以及 4G/网状网络上开箱即用。多项独立基准测试(arXiv:2309.07496 边缘到云; arXiv:2407.03091 planetary mesh;arXiv:2303.09419 Zenoh-vs-DDS-vs-MQTT-vs-Kafka)均表明,**在嵌入式网状网络场景中,Zenoh可将发现流量减少97–99.9%,并在丢包网络环境下超越DDS,且仅需约一半的CPU资源。截至 2026 年 5 月 13 日,工程上仍要注意以下边界:会话建立有明显延迟,缺少与主流 DDS 共享内存路径等价的零拷贝能力,且无法与基于 DDS 的 RMW 直接互操作(也无法与采用不同键表达式模式的独立 zenoh-plugin-ros2dds 桥接器互操作)。实践指南:跨机器人/广域网/网状网络连接请使用 Zenoh;对于需要 SHM 零拷贝的单主机传感器管道,请在机器人上保留 DDS RMW。
3. QoS 策略矩阵:通信不再是"发后不管" ⭐⭐¶
这一节解决什么问题:ROS1 的通信模型只有两个旋钮(queue_size 和 latch)。ROS2 引入了 DDS 的完整 QoS 策略体系,让发布者和订阅者之间可以表达丰富的通信语义——可靠性、持久性、活性、过期时间等。但更多选择也意味着更多出错的可能。本节先解释为什么需要 QoS,再给出工程选型规则。
3.1 为什么 queue_size 和 latch 不够 ⭐⭐¶
在 ROS1 中,一个传感器话题和一个控制命令话题使用完全相同的传输语义——都是 TCPROS 可靠传输加一个本地队列。这导致两个问题:
-
激光雷达以 40 Hz 发布数据。 如果 SLAM 算法某一帧处理慢了,队列堆积,后续帧全部延迟——但对 SLAM 来说,丢掉中间帧、拿到最新帧远比按序收到所有帧重要。TCPROS 的可靠传输恰恰阻止了这种"丢旧帧保新帧"的行为。
-
地图发布后,新启动的 RViz 看不到地图。 ROS1 的
latch=True可以缓存最后一条消息给后加入的订阅者,但只有一种持久性策略(有或没有),不能表达"缓存最近 N 条"或"只缓存一定时间内的数据"。
QoS 的本质是把"这条数据应该如何被传输和缓存"从业务代码中抽出来,变成发布者与订阅者之间的声明式契约。
3.2 七个关键 QoS 策略 ⭐⭐¶
ROS2 的每个发布者和订阅者都携带一个来自 DDS 的 QoS 配置文件。关键的七个策略:
| 策略 | 取值 | 控制内容 | ROS2 默认 |
|---|---|---|---|
HISTORY |
KEEP_LAST(N), KEEP_ALL |
重传/延迟加入的保留深度 | KEEP_LAST, depth 10 |
RELIABILITY |
RELIABLE, BEST_EFFORT |
ACK/NACK 重传 vs 发后不管 | RELIABLE |
DURABILITY |
VOLATILE, TRANSIENT_LOCAL |
延迟加入的订阅者是否能收到缓存的样本 | VOLATILE |
DEADLINE |
Duration |
最大期望的消息间隔;违反触发 on_deadline_missed |
系统默认 |
LIFESPAN |
Duration |
消息最大有效期 | 系统默认 |
LIVELINESS |
AUTOMATIC, MANUAL_BY_TOPIC |
发布者心跳策略 | AUTOMATIC |
LEASE_DURATION |
Duration |
活性超时 | 系统默认 |
兼容性规则(最常踩的坑):订阅者请求的 QoS 不能比发布者提供的更严格。 RELIABLE 订阅者不能从 BEST_EFFORT 发布者读取;TRANSIENT_LOCAL 订阅者不能从 VOLATILE 发布者读取。不匹配时数据不流动也不崩溃——沉默失败。不匹配会触发 on_offered_incompatible_qos / on_requested_incompatible_qos 事件。预定义配置文件简化了选择:SystemDefaultsQoS(Reliable/Volatile/depth 10),SensorDataQoS(Best Effort/Volatile/depth 5——激光雷达/IMU/摄像头推荐),ServicesQoS,ParametersQoS。ROS1 的 latch=True 现需**两端**均设为 TRANSIENT_LOCAL;忽略订阅方是"地图话题消失"的常见原因。
SLAM 工程师的 QoS 经验法则:
- /scan(激光)——用 SensorDataQoS()(BEST_EFFORT, VOLATILE, depth 5)
- /map——用 RELIABLE + TRANSIENT_LOCAL + depth 1
- /tf_static——RELIABLE + TRANSIENT_LOCAL
- /cmd_vel——BEST_EFFORT + VOLATILE + depth 1
- /odom——SensorDataQoS()
⚠️ QoS 常见陷阱¶
⚠️ 编程陷阱:发布者 BestEffort,订阅者 Reliable——数据沉默消失
错误做法:激光雷达驱动以
SensorDataQoS()(BestEffort)发布/scan,SLAM 节点以默认 QoS(Reliable)订阅。现象/后果:话题在
ros2 topic list中可见,但ros2 topic echo /scan无输出。不报错、不警告、不崩溃——完全沉默。根本原因:DDS 的 requested/offered 模型规定订阅者请求的能力不能超过发布者提供的能力。Reliable 请求要求发布者保证送达,但 BestEffort 发布者无法保证——因此 DDS 拒绝建立连接。
正确做法:用
ros2 topic info /scan --verbose检查两端 QoS。传感器话题统一用SensorDataQoS()(BestEffort + Volatile + depth 5)。⚠️ 概念误区:认为 QoS 越可靠越好
新手想法:"Reliable 保证不丢包,肯定比 BestEffort 好。所有话题都用 Reliable。"
实际上:Reliable 会触发 ACK/NACK 重传机制。在 WiFi 或弱网络中,重传会导致消息积压,表现为延迟逐渐增大甚至 DDS 背压使整个系统变慢。对于 40 Hz 的激光雷达,丢一帧比延迟 500 ms 收到旧帧好得多。
正确理解:QoS 的选择取决于数据的时间价值和丢失代价。传感器数据重视新鲜度(BestEffort),地图和静态 TF 重视可达性(Reliable + TransientLocal),控制命令重视过期后失效(BestEffort + 短 Lifespan)。
练习¶
- [设计题] 为一个室内配送机器人的以下话题分别选择 QoS 策略,并解释每个选择背后的失败模式:
/scan、/camera/image、/map、/cmd_vel、/battery_status、/tf_static。 - [实验题] 写一个最小发布/订阅程序,发布者使用
SensorDataQoS(),订阅者使用默认 QoS。观察是否收到数据。然后注册on_requested_incompatible_qos回调,确认不匹配事件。 - [分析题] 解释 ROS1 的
latch=True在 ROS2 中为什么需要发布方和订阅方都设置TRANSIENT_LOCAL,而不只是发布方。
4. 节点模型、Executor 与 Lifecycle ⭐⭐¶
这一节解决什么问题:ROS2 的节点不再是简单的"进程+回调"。Composition 让多节点共享进程以减少拷贝;Executor 控制回调的调度方式;Lifecycle 让节点有显式的状态管理。这三者共同解决了 ROS1 中"节点启动后就是一个黑盒"的问题。
4.1 从"一进程一节点"到 Composition ⭐⭐¶
ROS1 的默认是一个进程一个节点,nodelets 用于进程内零拷贝(在 nodelet_manager 内传递 boost::shared_ptr<const Msg>)。Nodelets 需要一个与 roscpp::NodeHandle 不兼容的 Nodelet::onInit() API——代码要写两份。这很像早期 Web 开发中"独立应用"和"servlet"的双 API 困境:逻辑完全相同,但入口格式不同,导致维护成本翻倍。
ROS2 让**多节点共享进程成为一等公民**。同一个 rclcpp::Node 类既能独立运行也能被组合。子类用 RCLCPP_COMPONENTS_REGISTER_NODE(MyClass) 注册,通过 launch 文件的 LoadComposableNodes 加载到容器中。启用 IPC 用 NodeOptions().use_intra_process_comms(true),使用匹配的 QoS,发布/订阅偏好 std::unique_ptr<Msg>——中间件被完全绕过,获得 move 语义零拷贝。
反事实推理:如果不用 Composition,一个视觉 SLAM 管线中的图像采集节点和特征提取节点分别运行在两个进程中。一帧 1280x720 RGB 图像约 2.76 MB,以 30 Hz 传输意味着每秒约 83 MB 的进程间数据拷贝。这不仅消耗 CPU 和带宽,还会在内存紧张的嵌入式平台(如 Jetson Nano)上触发 OOM。Composition 把这个拷贝变成指针传递——开销从毫秒级降到微秒级。
iRobot 的 composition 论文(arXiv:2305.09933)测量到 33% 的内存减少和 28% 的 CPU 减少。iRobot Create 3 如果不用 composition 据说根本无法实现。对于超过 ~1 MB 的消息(图像、点云、占用栅格),composition + IPC 实际上是**必需的**。
| Composition 模式 | 启动方式 | 优点 | 限制 |
|---|---|---|---|
| 手动加载 | ros2 component load /container pkg::Node |
运行时动态加载/卸载 | 需要容器进程先启动 |
| Launch 文件 | ComposableNodeContainer + ComposableNode |
启动即完成组合 | 配置较复杂 |
| 独立运行(降级) | ros2 run pkg node_exe |
调试方便、GDB 可用 | 无 IPC 零拷贝 |
工程实践中推荐的做法是:开发和调试阶段用独立运行(方便 GDB 和日志),集成和部署阶段切换到 Composition(获得性能收益),通过 launch 文件的 use_composition 参数控制模式切换。
4.2 四种 Executor ⭐⭐¶
| Executor | 特点 | 适用场景 |
|---|---|---|
SingleThreadedExecutor |
默认,所有回调串行 | 简单系统,推理方便 |
MultiThreadedExecutor |
并发执行,由回调组控制并行度 | 需要在回调中同步调用服务而不死锁 |
StaticSingleThreadedExecutor |
只扫描一次实体集,开销更低 | Kilted 中已弃用,改进已并入默认实现 |
EventsExecutor |
事件驱动,无轮询等待集 | 更低延迟和抖动,高频控制循环首选 |
回调组(Callback Groups):MutuallyExclusive(默认,组内串行)和 Reentrant(允许并发执行)。不同组的回调在 MultiThreadedExecutor 下总是可以并行。
经典死锁:在 SingleThreadedExecutor 的回调中调用 spin_until_future_complete() 会死锁——线程阻塞等待响应,但响应回调只能在同一个被阻塞的线程上执行。解决方案:把客户端和定时器放在**不同的** MutuallyExclusive 组中,或使用完全异步的 future 回调。
这个死锁可以用日常类比理解:你在餐厅(SingleThreadedExecutor)只有一个服务员(线程)。如果服务员在给 A 桌上菜时,A 桌说"先帮我问厨房甜品有什么",服务员走去问厨房但厨房说"等我做完这道菜再告诉你"——服务员等在厨房门口,B 桌的菜也无法上。解决方案是多请一个服务员(MultiThreadedExecutor),或者服务员先去上 B 桌的菜,等厨房做好甜品后再回来告诉 A 桌(异步回调)。
本质洞察:Executor 的选择不是"越多线程越好",而是"回调之间的依赖关系决定了需要什么样的调度模型"。如果所有回调都是独立的数据处理(无同步服务调用),SingleThreadedExecutor 就够了且更安全。只有当回调内部需要阻塞等待另一个回调的结果时,才需要 MultiThreadedExecutor 或异步模式。
DDS-Security 与 SROS2:安全不是可选层 ⭐⭐⭐¶
在实验室中,所有节点默认可信——任何进程都可以发布 /cmd_vel 控制机器人运动。但在生产环境(仓库车队、医院配送、公共区域巡逻)中,这等于把机器人的控制权开放给同一网络上的任何人。DDS-Security 是 OMG 标准中定义的安全扩展,提供认证(谁能参与)、授权(谁能读写哪些话题)和加密(数据在网络上不可被窃听)。SROS2 是 ROS2 对 DDS-Security 的封装,提供密钥和权限管理工具。
SROS2 的工程含义是:你不需要自己实现 TLS 或设计权限系统——DDS 已经标准化了这些。只需生成密钥对、定义权限策略、在启动时指定安全目录:
# 生成安全目录和密钥
ros2 security create_keystore ~/sros2_keystore
ros2 security create_enclave ~/sros2_keystore /my_robot/slam_node
# 启动时启用安全
export ROS_SECURITY_KEYSTORE=~/sros2_keystore
export ROS_SECURITY_ENABLE=true
export ROS_SECURITY_STRATEGY=Enforce
ros2 run slam_toolbox async_slam_toolbox_node --ros-args --enclave /my_robot/slam_node
安全认证的代价是启动时的密钥交换延迟(通常 200-500 ms)和运行时的加密开销(对小消息约 5-15%,对大消息可忽略)。对于实验室开发,通常不启用安全;对于即将部署的系统,应在集成测试阶段就开始使用,以确保安全配置不会引入意外的通信问题。
SROS2 安全架构深入 ⭐⭐⭐¶
DDS-Security 标准(OMG DDS-SEC v1.1)定义了五个可插拔安全插件接口(SPI),SROS2 在 ROS2 中落地了其中三个核心能力。理解这三个层次的安全模型,是判断"我的机器人系统是否足够安全"的基础。
第一层:认证(Authentication) 回答"谁可以参与"。每个参与者持有 X.509 证书和私钥,通过 TLS 握手验证身份。SROS2 使用 ros2 security create_enclave 为每个节点(或节点组)生成独立的密钥对和证书,所有证书由同一个 CA(Certificate Authority)签发。只有持有该 CA 签发的有效证书的参与者才能加入 DDS domain。
第二层:授权(Access Control) 回答"谁能做什么"。权限文件(permissions.xml)定义每个 enclave 可以读写哪些话题、提供或调用哪些服务。权限粒度可以细到单个话题。例如,SLAM 节点只能发布 /map 和 /tf,不能发布 /cmd_vel——即使攻击者获得了 SLAM 节点的控制权,也无法直接操纵机器人底盘。
第三层:加密(Cryptographic Operations) 回答"数据能否被窃听"。DDS-Security 支持对数据负载进行 AES-GCM 加密,对元数据进行 HMAC 签名。加密可以按话题启用或禁用。对于高频传感器话题(如 1 kHz 的 IMU),加密开销可能不可接受;对于低频但敏感的话题(如 /cmd_vel、/map),加密是值得的。
反事实推理:如果不使用 SROS2 而是自己在应用层实现安全,会怎样?你需要为每个话题手写认证和加密逻辑,维护自定义证书管理工具,处理密钥轮换、证书过期和撤销。这不是不可能,但维护成本远高于使用 DDS 标准——SROS2 把这些工作压缩到几条命令和一个权限文件。
权限文件的典型结构如下:
<permissions>
<grant name="/my_robot/slam_node">
<subject_name>CN=/my_robot/slam_node</subject_name>
<validity>
<not_before>2024-01-01T00:00:00</not_before>
<not_after>2030-01-01T00:00:00</not_after>
</validity>
<allow_rule>
<domains><id>0</id></domains>
<publish>
<topics><topic>/map</topic><topic>/tf</topic></topics>
</publish>
<subscribe>
<topics><topic>/scan</topic><topic>/odom</topic></topics>
</subscribe>
</allow_rule>
<default>DENY</default>
</grant>
</permissions>
SROS2 的部署检查清单:
| 步骤 | 命令 | 验证标准 |
|---|---|---|
| 创建密钥库 | ros2 security create_keystore ~/sros2_keystore |
目录结构完整 |
| 生成 enclave | ros2 security create_enclave ~/sros2_keystore /robot/node |
证书和私钥存在 |
| 编写权限文件 | 手动编辑 permissions.xml |
每个节点最小权限 |
| 签名权限 | ros2 security sign_permissions ... |
签名文件生成 |
| 启用安全 | export ROS_SECURITY_ENABLE=true |
节点启动时日志显示安全握手 |
| 验证拒绝 | 未授权节点无法发布受限话题 | 日志显示权限拒绝 |
工程经验表明,安全配置最常见的错误不是密钥生成失败,而是权限文件遗漏了某个必要话题——节点启动后日志正常,但某个关键话题的数据不流动。排查方法是把 ROS_SECURITY_STRATEGY 临时设为 Permissive(允许但警告),观察哪些操作被标记为未授权。
DDS 选型深入:FastDDS vs CycloneDDS vs Zenoh 性能基准 ⭐⭐⭐¶
前面已经给出了四种 RMW 的延迟概览,本小节进一步拆解性能差异的来源,帮助工程师做出有依据的选型而不是"听说 CycloneDDS 更好就用它"。
性能对比必须按场景分层。不存在"全面最优"的 RMW——每种实现在不同消息大小、网络环境和 QoS 配置下表现不同。OSRF 的年度 TSC RMW 评估报告(https://osrf.github.io/TSC-RMW-Reports/)提供了官方基准数据,以下是工程选型时最常需要回答的三个问题。
问题一:小消息低延迟
小消息(< 1 KB)的延迟主要来自发现开销、序列化和系统调用。在本机环回测试中(RELIABLE, KEEP_LAST=10),三者差距不大:
| RMW | 256 B 中位延迟 | 256 B p99 延迟 | 发现完成时间 |
|---|---|---|---|
| Fast DDS | ~80 µs | ~200 µs | 2-5 s(默认多播) |
| CycloneDDS | ~65 µs | ~160 µs | 1-3 s(默认多播) |
| Zenoh | ~100 µs | ~300 µs | 0.5-2 s(通过路由器) |
| Connext | ~55 µs | ~140 µs | 1-3 s |
中位延迟的差距在实际系统中通常不是瓶颈——控制器回调本身的计算耗时远大于通信延迟。更重要的指标是 p99 延迟——它反映偶发尖峰的严重程度。CycloneDDS 的 p99 通常最低,因为它的代码路径更短、内部锁粒度更细。
问题二:大消息吞吐
大消息(> 1 MB,如 PointCloud2、Image)的传输瓶颈在 UDP 分片和接收端重组。一帧 4 MB 的点云在 UDP 上需要约 60 个分片(每片 64 KB),任一分片丢失都需要重传(RELIABLE 模式)。
| RMW | 4 MB 延迟(本机) | 4 MB 延迟(WiFi) | 共享内存支持 |
|---|---|---|---|
| Fast DDS | 8-15 ms | 30-100+ ms | 有(Fast DDS SHM Transport) |
| CycloneDDS | 6-12 ms | 25-80+ ms | 有(iceoryx 集成) |
| Zenoh | 10-20 ms | 15-40 ms | 实验阶段 |
| Connext | 5-10 ms | 20-60+ ms | 有 |
值得注意的是 Zenoh 在 WiFi 场景下反而表现更好——因为 Zenoh 使用 TCP/QUIC 而非 UDP,TCP 的拥塞控制在有损网络中比 DDS 的 ACK/NACK 重传更高效。但在本机通信中,DDS 的共享内存路径远快于 Zenoh 的序列化路径。
问题三:弱网可达性
在 WiFi、Docker 桥接、VPN、跨子网或 NAT 环境中,DDS 的多播发现可能完全失效。三者的应对策略截然不同:
| 环境 | Fast DDS | CycloneDDS | Zenoh |
|---|---|---|---|
| WiFi(多播受限) | Discovery Server | 显式 peers 列表 | 路由器 gossip |
| Docker 桥接 | --network host 或 Discovery Server |
--network host 或 XML peers |
路由器 |
| 跨子网 VPN | WireGuard + 多播代理 | WireGuard + 显式 peers | 路由器直连 |
| 4G/mesh/WAN | 基本不可用 | 基本不可用 | 原生支持 |
本质洞察:DDS RMW 的选择不应基于"哪个基准测试更快",而应基于"我的部署网络满足哪种假设"。如果是本机或局域网有线连接,Fast DDS 和 CycloneDDS 都够用,选择默认即可;如果是 WiFi 或跨网络,Zenoh 的可达性优势是决定性的;如果需要安全认证或商业支持,Connext 是唯一选择。
工程选型决策流程:
你的机器人系统需要通信吗?
↓ 是
所有节点在同一台机器上吗?
↓ 是 → Composition + IPC(完全绕过中间件)
↓ 否
所有节点在同一个有线局域网中吗?
↓ 是 → Fast DDS(默认)或 CycloneDDS(更简单配置)
↓ 否
网络支持 UDP 多播吗?
↓ 是 → DDS 默认配置即可
↓ 否
网络是 WiFi / Docker / VPN 但仍在局域网范围?
↓ 是 → Fast DDS Discovery Server 或 CycloneDDS 显式 peers
↓ 否
需要跨子网 / WAN / mesh / 边缘到云?
↓ 是 → Zenoh
ROS2 生态定位对比 ⭐⭐¶
ROS2 不是唯一的机器人中间件选择。理解 ROS2 相对于其他方案的定位——ROS1、LCM、DDS-only——有助于判断在什么场景下 ROS2 是最优选择,什么场景下可能需要其他工具。
ROS2 vs ROS1:这个对比在前面已经从架构层面展开。这里补充一个生态视角——ROS1 的软件包生态在 2025 年 5 月 Noetic EOL 后进入纯遗留状态。新的 SLAM 算法(KISS-ICP、FAST-LIO2 的官方 ROS2 分支、slam_toolbox)、导航栈(Nav2)、控制框架(ros2_control)和仿真器集成(gz_ros2_control)都只在 ROS2 上活跃开发。继续基于 ROS1 启动新项目,意味着放弃未来 3-5 年的生态增量。
ROS2 vs LCM(Lightweight Communications and Marshalling):LCM 由 MIT 开发,在 DARPA 挑战赛和早期足式机器人研究中广泛使用。它的设计哲学是"最小化"——UDP 多播 + 自定义 IDL 编译 + 零依赖。
| 维度 | ROS2 | LCM |
|---|---|---|
| 通信模型 | DDS 发布/订阅 + 服务 + 动作 | UDP 多播发布/订阅 |
| 序列化 | CDR(OMG 标准) | 自定义 LCM 格式 |
| 发现 | DDS 分布式发现 | 多播组 |
| QoS | 丰富的端点 QoS | 无(应用层自己处理) |
| 生态 | 数千个包、Nav2、MoveIt2、ros2_control | 极少公开包,主要内部使用 |
| 实时性 | 需要 PREEMPT_RT + 配置 | 设计上低延迟 |
| 学习曲线 | 中等 | 低 |
| 工业采用 | 广泛(NASA、Amazon、Autoware) | 有限(MIT 系实验室) |
LCM 的优势是启动极快、延迟极低、没有中间件层的复杂度。它非常适合"少量节点、单台机器人、低延迟控制"的场景——这也是为什么很多足式机器人研究团队最初使用 LCM。但 LCM 没有服务、动作、生命周期管理和安全机制,跨网络能力也很弱。当系统复杂度增长到需要多机器人协作、传感器数据记录回放、标准化 SLAM 和导航栈时,ROS2 的生态优势变得不可替代。
跨领域类比:LCM 之于 ROS2,就像 SQLite 之于 PostgreSQL。SQLite 在单用户、本地、低开销场景下无可匹敌;PostgreSQL 在多用户、分布式、事务管理和扩展性上远超 SQLite。选择哪个取决于系统的复杂度阈值,而不是"哪个更先进"。
ROS2 vs DDS-only(直接使用 DDS API):一些工业用户(尤其是 AUTOSAR Adaptive 和军事领域)直接使用 RTI Connext 或 eProsima Fast DDS 的原生 API,不使用 ROS2 的 rclcpp/rclpy 封装。这种做法的优势是完全控制 DDS 配置、不依赖 ROS2 构建系统、可以使用 DDS 的全部 QoS 能力。劣势是放弃了 ROS2 生态——Nav2、MoveIt2、ros2_control、slam_toolbox、rosbag2 都无法使用。对于"通信只是系统的一小部分,核心逻辑完全自研"的场景,DDS-only 可能合适;对于"需要快速集成 SLAM、导航、控制和仿真"的机器人项目,ROS2 的生态价值远超它引入的额外抽象层。
| 方案 | 通信开销 | 生态宽度 | 配置灵活性 | 适用场景 |
|---|---|---|---|---|
| ROS2 | 中(RMW 抽象层) | 极广 | 中 | 通用机器人,需要 SLAM/导航/仿真 |
| LCM | 极低 | 极窄 | 低 | 少节点低延迟实验室原型 |
| DDS-only | 低 | 无 ROS 生态 | 高 | 工业系统自研所有上层 |
| ROS1 | 低 | 遗留 | 低 | 仅用于维护已有系统 |
⚠️ 中间件选型常见陷阱¶
⚠️ 思维陷阱:认为基准测试中"更快"的 RMW 在所有场景下都更好
新手想法:"CycloneDDS 在某个基准测试中延迟比 Fast DDS 低 20%,所以项目全部换成 CycloneDDS。"
现象/后果:切换后发现 Fast DDS Discovery Server 的单播发现在 WiFi 中很好用,但 CycloneDDS 的显式 peers 在节点动态加入时不灵活。或者发现 CycloneDDS 的某些 QoS 组合行为与 Fast DDS 不同,导致现有配置需要重新调试。
根本原因:基准测试只覆盖特定消息大小、QoS 和网络环境。实际系统中影响最大的往往不是中位延迟,而是发现机制、安全支持、共享内存、大消息分片策略和 p99 尾延迟。
正确做法:在自己的硬件和网络环境中做针对性测试。使用
performance_test(github.com/ros2/performance_test)跑与实际使用场景匹配的消息大小和 QoS 配置。⚠️ 概念误区:认为 Zenoh 是"DDS 的下一代替代品"
新手想法:"Zenoh 是 Tier-1 了,以后只用 Zenoh 就行。"
实际上:Zenoh 和 DDS 解决不同层面的问题。Zenoh 在跨网络可达性上优于 DDS,但在同主机零拷贝(共享内存)和工业安全认证方面落后于 DDS。Zenoh RMW 与 DDS RMW 无法互操作——同一系统中不能混用。
正确做法:把 Zenoh 用于跨网络和弱网场景(多机器人、远程监控),把 DDS 用于机器人本体内的高吞吐通信(传感器管线、控制循环)。如果两者需要共存,使用
zenoh-plugin-ros2dds桥接。
练习¶
- [设计题] 为一个仓库物流系统(20 台 AGV + 1 台中央调度 + WiFi 覆盖)选择 RMW。分析以下约束如何影响选择:AGV 内部传感器管线需要低延迟、AGV 之间需要跨 WiFi 通信、中央调度需要看到所有 AGV 的状态。是否需要混合 RMW 方案?
- [调研题] 访问 OSRF TSC RMW 报告(https://osrf.github.io/TSC-RMW-Reports/),找到最新一期的延迟基准数据。比较 Fast DDS 和 CycloneDDS 在 1 MB 消息下的 p99 延迟差异,分析差异来源。
- [分析题] 假设你的团队之前使用 LCM 做四足机器人控制(2 个节点、500 Hz),现在要迁移到 ROS2 以使用 Nav2 和 slam_toolbox。列出迁移后可能增加的通信延迟来源,评估是否需要为控制回路单独保留 LCM 或使用 Composition + IPC。
4.3 Lifecycle 节点:为什么需要显式状态管理 ⭐⭐¶
ROS1 节点的生命周期是隐式的:构造函数里初始化所有资源,spin 开始后进入稳态,进程被 kill 时资源由操作系统回收。这在实验室里够用——你手动启动节点、手动关闭,顺序由人控制。但在生产系统中,这个模型有三个严重问题:
-
启动顺序不可控。
roslaunch保证节点被启动,但不保证节点间的初始化顺序。如果 SLAM 节点在地图服务器之前完成初始化,它可能在无地图状态下就开始处理传感器数据。 -
优雅停机不存在。ROS1 节点被 SIGTERM 杀死时,构造函数里分配的硬件资源(串口、CAN 总线)可能不会被正确释放。底层驱动可能保持最后一帧命令继续执行。
-
状态不可观测。你无法从外部知道一个节点是"正在初始化""已准备好""正在运行"还是"出了错误"。只能通过话题是否有数据流动来间接推断。
ROS2 的 Lifecycle 节点(也称 Managed Node,REP-2002 定义)把节点状态显式化为一个状态机:
Unconfigured
↓ on_configure() → 成功
Inactive
↓ on_activate() → 成功
Active
↓ on_deactivate() → 成功
Inactive
↓ on_cleanup() → 成功
Unconfigured
↓ on_shutdown() → 成功
Finalized
每个状态转换都是一个可观测、可控制的操作。nav2_lifecycle_manager 就是利用这个机制,按顺序把 Nav2 的所有节点从 Unconfigured 切换到 Active——如果某个节点配置失败,整个栈不会进入半工作状态。
这就像飞行前检查清单:不是"引擎启动了就起飞",而是"引擎启动 → 液压检查 → 通信检查 → 滑行许可",每一步都有明确的成功/失败判断。
| 状态 | 含义 | 可以做什么 | 不应该做什么 |
|---|---|---|---|
| Unconfigured | 已加载但未初始化 | 等待配置命令 | 处理传感器数据 |
| Inactive | 已配置但不处理数据 | 等待激活 | 发布输出或修改外部状态 |
| Active | 正常运行 | 读取输入、计算、发布输出 | 重新配置参数 |
| Finalized | 准备销毁 | 释放资源 | 任何其他操作 |
本质洞察:Lifecycle 的价值不是"多了几个回调",而是让系统在每个过渡点都有可验证的前置条件和可控的失败出口。没有 Lifecycle,系统启动失败时的状态是"某些节点在跑、某些没跑、不知道哪些资源被分配了";有了 Lifecycle,失败状态变成"节点 X 在 on_configure 失败,停在 Unconfigured,不影响其他节点"。
与 ros2_control 的关系:ros2_control 的硬件接口也有自己的生命周期(on_init → on_configure → on_activate → read/write 循环 → on_deactivate)。这不是巧合——硬件启动比纯软件更需要可控的过渡阶段。回顾 硬件集成与RL部署 会看到,很多硬件事故发生在"刚启动""刚切控制器"这些过渡阶段,而不是稳态运行中。
⚠️ 常见陷阱¶
⚠️ 工程陷阱:把 Lifecycle 当"可选的高级功能"跳过
错误做法:用
rclcpp::Node而不是rclcpp_lifecycle::LifecycleNode写所有节点,因为"简单"。现象/后果:多节点系统启动时偶发出现时序问题——某些节点已经在处理数据,但它们依赖的服务或变换还没就绪。
根本原因:没有 Lifecycle,节点的"就绪"状态是隐式的。你无法从外部确认节点是否完成了初始化,也无法按顺序编排启动。
正确做法:在多节点系统中,尤其是 Nav2、ros2_control 和自定义 SLAM 管线中,使用 LifecycleNode 并配合 lifecycle_manager 管理启动顺序。
练习¶
- [分析题] Nav2 的
nav2_lifecycle_manager为什么要用 200 ms bond 心跳监控每个被管节点?如果某个节点崩溃,lifecycle_manager 应该做什么? - [设计题] 为一个包含 SLAM 节点、地图服务器、导航控制器的系统设计生命周期启动顺序。说明哪个节点应该最先 activate,为什么。
- [思考题] 比较 ROS2 Lifecycle 和 systemd 的 service 管理(start/stop/restart/enable)。两者解决什么共同问题?有什么关键区别?
5. 构建系统与 Launch 系统演进 ⭐¶
这一节解决什么问题:从 catkin 到 colcon、从 XML roslaunch 到 Python launch、从 format 2 到 format 3 package.xml——这些工具链的演进反映了 ROS2 对跨平台、多构建系统和可编程启动的需求。详细的构建系统内容在 构建系统与机器人建模 中展开,本节只介绍演进动机。
5.1 package.xml 演进 ⭐¶
- format 1 → format 2:
<run_depend>拆分为<exec_depend>+<build_export_depend>,新增<depend>快捷方式 - format 3(当前标准,ament 要求):新增条件依赖(
<depend condition="$ROS_DISTRO == humble">...)、组成员关系
5.2 Launch 系统 ⭐¶
ROS1 用 XML roslaunch 文件;ROS2 用 Python launch 文件(主要),XML 和 YAML(次要)。
Python 被选为主要方式是因为 launch 描述越来越需要*计算*:从参数推导话题名、读取 YAML 决定生成哪些节点、根据 use_sim_time 进行条件判断、声明式地串联事件。XML 无法简洁表达这些。
核心概念词汇:LaunchDescription + DeclareLaunchArgument + LaunchConfiguration(命令行参数);Substitution 原语(PathJoinSubstitution, FindPackageShare, Command, PythonExpression);ComposableNode + ComposableNodeContainer(composition);RegisterEventHandler(OnProcessExit(...))(事件驱动排序)。
Launch 系统的详细用法和代码模式在 CLI调试与性能工具 中展开,此处只需理解它从 XML 声明式变成了 Python 可编程这一核心变化。
6. ROS1 到 ROS2 的 API 差异速查 ⭐¶
这一节解决什么问题:从 ROS1 迁移的工程师需要一张快速映射表,知道熟悉的 API 在 ROS2 中变成了什么。更重要的是理解映射背后的架构差异——不只是"函数名变了",而是"设计模型变了"。
6.1 核心变化 ⭐¶
| 概念 | ROS1 (roscpp) | ROS2 (rclcpp) |
|---|---|---|
| 节点创建 | ros::NodeHandle nh; |
class MyNode : public rclcpp::Node |
| 头文件 | #include <ros/ros.h> |
#include <rclcpp/rclcpp.hpp> |
| 初始化 | ros::init(argc, argv, "name"); |
rclcpp::init(argc, argv); |
| spin | ros::spin(); |
rclcpp::spin(node); |
| 日志 | ROS_INFO("msg"); |
RCLCPP_INFO(get_logger(), "msg"); |
| 发布者 | ros::Publisher pub = nh.advertise<T>(topic, 10); |
auto pub = create_publisher<T>(topic, 10); |
| 订阅者 | ros::Subscriber sub = nh.subscribe(topic, 10, cb); |
auto sub = create_subscription<T>(topic, 10, cb); |
| 参数 | nh.getParam("key", val); |
declare_parameter("key", default); get_parameter("key").as_double(); |
| 时间 | ros::Time::now() |
this->now()(会尊重 use_sim_time) |
| TF | tf::TransformListener |
tf2_ros::Buffer + tf2_ros::TransformListener |
| 定时器 | ros::Rate 循环 |
create_wall_timer(100ms, callback) |
| ROS 参数传递 | node __name:=foo |
--ros-args -r __node:=foo |
6.2 ROS2 最小发布者示例 ⭐¶
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>
using namespace std::chrono_literals;
class Talker : public rclcpp::Node {
public:
Talker() : Node("talker") {
pub_ = create_publisher<std_msgs::msg::String>("chatter", 10);
timer_ = create_wall_timer(1s, [this]() {
std_msgs::msg::String m; m.data = "hi";
pub_->publish(m);
});
}
private:
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr pub_;
rclcpp::TimerBase::SharedPtr timer_;
};
int main(int argc, char** argv) {
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<Talker>());
rclcpp::shutdown();
}
6.3 ROS2 最小订阅者示例(Python) ⭐¶
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class Listener(Node):
def __init__(self):
super().__init__('listener')
self.create_subscription(String, 'chatter', self.cb, 10)
def cb(self, m):
self.get_logger().info(m.data)
def main():
rclpy.init()
rclpy.spin(Listener())
rclpy.shutdown()
上面的代码示例展示了 ROS2 节点的基本形态:面向对象的节点类、create_publisher/create_subscription 工厂方法、定时器驱动的回调。与 ROS1 最大的风格差异是节点不再是全局函数集合,而是一个继承 rclcpp::Node 的类——这让资源管理、测试和 Composition 都更自然。
7. 性能对比:实测数据 ⭐⭐¶
这一节解决什么问题:DDS 引入了额外的抽象层,这是否意味着 ROS2 比 ROS1 更慢?早期确实如此,但通过 Composition、IPC、共享内存和 EventsExecutor,差距已经被关闭。本节用实测数据说明"慢"不是 DDS 的固有属性,而是配置和使用方式的问题。
DDS 在早期发行版(Ardent 到 Dashing)为小消息高频回环通信引入了开销,使 ROS2 比 ROS1 更差。通过 composition+IPC、loaned messages、共享内存传输和 EventsExecutor,差距已经被关闭。
7.1 延迟基准(进程间,RELIABLE,KEEP_LAST=10,localhost) ⭐⭐¶
| 场景 | Fast DDS | Cyclone DDS | RTI Connext | Zenoh |
|---|---|---|---|---|
| 256 B 延迟 | ~70–120 µs | ~60–100 µs | ~50–90 µs | ~80–150 µs |
| 1 KB 延迟 | ~100–150 µs | ~90–140 µs | ~70–120 µs | ~120–180 µs |
| 1 MB 图像延迟 | 2–5 ms | 2–4 ms | 1.5–3 ms | 3–6 ms |
| 4 MB 点云 | 8–15 ms | 6–12 ms | 5–10 ms | 10–20 ms |
| 同主机零拷贝(SHM) | <20 µs | <20 µs | <15 µs | 暂不支持 |
| 有损 WiFi / mesh | 高方差,丢包多 | 高方差 | 高方差 | 延迟降低约 2×,可达性更好 |
7.2 三个关键结论 ⭐⭐¶
- 对图像或点云管线,composition + IPC 不是可选的——超过约 1 MB 的消息"如果不用节点组合几乎不可能"
- **loaned messages + 共享内存传输**可以把多 MB 载荷的延迟降低到指针交接级别
- **1 kHz 实时控制**需要 PREEMPT_RT +
mlockall+TLSFAllocator+SCHED_FIFO+ EventsExecutor
7.3 RL 推理循环的实用建议 ⭐⭐¶
- 对 >200 Hz 的实时推理,在组合的 C++ 容器中用 loaned messages + EventsExecutor + TensorRT
- Python rclpy 在热路径上可接受到 ~50–100 Hz,但 GIL 抖动是瓶颈
- 基准测试工具:Apex.AI
performance_test(github.com/ros2/performance_test)、iRobotros2-performance(github.com/irobot-ros/ros2-performance)、ros2-benchmark-container(github.com/irobot-ros/ros2-benchmark-container)
性能数据说明 DDS 不是瓶颈——使用方式才是。Composition + IPC 对大消息是必需的,不是可选优化。
8. 多机器人支持 ⭐⭐¶
这一节解决什么问题:从单机器人到多机器人,ROS1 的中心化发现模型遇到了根本困难。ROS2 通过 Domain ID 和命名空间提供原生支持,Zenoh 进一步解决了跨子网的可达性问题。
8.1 ROS1 的多机器人方案 ⭐⭐¶
ROS1 的多机器人支持是在单 master 上的变通方案:multimaster_fkie(github.com/fkie/multimaster_fkie)用 master_discovery_fkie + master_sync_fkie 是最完整的,但同步风暴、时钟偏移和名称冲突是常态。
8.2 ROS2 的原生支持 ⭐⭐¶
第一层隔离是 ROS_DOMAIN_ID(0–101 安全范围);第二层是**命名空间**,通过 launch 文件的 PushRosNamespace / GroupAction 实现,产生 /robot1/scan、/robot2/scan。frame ID 也需要相同前缀。Nav2 的 nav2_bringup 内含多机器人 launch 示例作为标准起点。
8.3 跨子网解决方案 ⭐⭐⭐¶
当多播被阻断时:
- Fast DDS Discovery Server——在已知 IP 上做单播汇合
- CycloneDDS 显式对端列表——在 CYCLONEDDS_URI XML 中配置
- rmw_zenoh——基于路由器的 gossip,2026 年最佳的有损 WiFi/mesh/WAN 方案
- L3 覆盖网络(WireGuard、Husarnet)——扁平化子网让 DDS 多播"正常工作"
- SROS2 为共享网络上的生产车队提供 X.509 认证和逐话题加密
9. 迁移策略与 ros1_bridge ⭐⭐¶
这一节解决什么问题:ROS1 Noetic 已于 2025 年 5 月 EOL。对于仍有 ROS1 代码的团队,迁移不是"要不要"的问题,而是"怎么迁"的问题。本节覆盖桥接方案、逐包迁移步骤和常见陷阱。
9.1 ROS1 Noetic 已 EOL ⭐⭐¶
Noetic EOL 是 2025 年 5 月 31 日(与 Ubuntu 20.04 Focal 同步)。截至 2026 年 5 月 13 日,Noetic 已经停止上游支持接近一年。ROS build farm 不再同步,二进制包冻结。主流生态(Nav2、MoveIt 2、ros2_control、Autoware Universe、slam_toolbox、Isaac ROS)已完全转向 ROS2。新代码不应再基于 ROS1 编写。
9.2 ros1_bridge ⭐⭐¶
ros1_bridge(github.com/ros2/ros1_bridge)是一个同时链接 roscpp 和 rclcpp 的进程,在同一地址空间运行一个 ROS1 节点和一个 ROS2 节点。两种模式:dynamic_bridge(自动配对)和 parameter_bridge(显式 YAML 列表)。
核心痛点:自定义消息需要完整重建,同时 source 两个 ROS 版本的 setup.bash——source 顺序和 Python 2/3 分歧经常导致构建失败。Ubuntu 24.04 没有 Noetic 包,所以构建需要 20.04 或 ROS-O 源码构建。
9.3 替代桥接方案 ⭐⭐¶
zenoh-plugin-ros1+zenoh-plugin-ros2dds——通过 Zenoh 桥接 ROS1 ↔ ROS2,解耦双 source 构建问题foxglove_bridge——CDR-over-WebSocket,用于可视化而非数据平面rosbridge_suite——JSON-over-WebSocket,用于浏览器
9.4 逐包迁移步骤 ⭐⭐¶
| 步骤 | 工作量 |
|---|---|
package.xml → format 3 |
15 分钟,机械操作 |
CMakeLists.txt:catkin → ament_cmake |
每个包 1–2 小时 |
| 源码改写为 rclcpp/rclpy | 每个非平凡节点 1 天到 1 周 |
| Launch XML → Python | 每个文件 2–4 小时 |
| 参数 YAML 格式调整 | 15 分钟 |
| 自定义消息 | 每个包 30 分钟(只改构建系统) |
9.5 SLAM/RL 相关的常见迁移陷阱 ⭐⭐¶
- QoS 反转规则——Reliable 发布 + BestEffort 订阅可以,反过来静默不连接
- 必须声明参数——
declare_parameter必须在get_parameter之前 - 回调组——防止慢速 SLAM 优化器饿死传感器回调
tf→tf2——tf::TransformListener消失了,用tf2_ros::Buffer+tf2::doTransform<T>ros::Rate循环 → 定时器——create_wall_timerros::Time::now()→node->now()——用于仿真时间感知的时钟
9.6 成功的迁移案例 ⭐⭐¶
- Nav2——
move_base完全重写,围绕 lifecycle 节点、行为树、可插拔规划器/控制器 - MoveIt 2——引入
moveit_cpp(不需要 move_group 进程)、混合规划、ros2_control 集成 - slam_toolbox——从 Dashing 起就是 ROS2 原生的——典型的"重写而非移植"
- Autoware——两阶段迁移:Autoware.Auto(干净子集)→ Autoware Universe(完整 AD 栈)
- Gazebo——Classic EOL 2025 年;现代 Gazebo 通过
ros_gz桥接到 ROS2
10. 版本选择指南 ⭐⭐¶
10.1 当前活跃的发行版(2026 年 5 月 13 日) ⭐⭐¶
| 发行版 | 发布时间 | EOL | Ubuntu | 定位 |
|---|---|---|---|---|
| Humble Hawksbill | 2022 年 5 月 | 2027 年 5 月 | 22.04 | 遗留 LTS;仅用于与现有 Humble 代码库集成 |
| Iron Irwini | 2023 年 5 月 | 2024 年 11 月 | 22.04 | 已 EOL——不要用于新项目 |
| Jazzy Jalisco | 2024 年 5 月 | 2029 年 5 月 | 24.04 | 当前生产默认 LTS |
| Kilted Kaiju | 2025 年 5 月 | 2026 年 11 月 | 24.04 | 非 LTS;rmw_zenoh_cpp Tier-1 |
| Lyrical Luth | 2026 年 5 月(LTS) | 2031 年 5 月 | 26.04 | 官方文档已列出平台与 RMW 支持;新项目需等待关键生态二进制包齐全 |
| Rolling Ridley | 持续 | — | 跟踪下一个 | 开发分支 |
10.2 推荐 ⭐⭐¶
对于现在启动的 SLAM + RL 项目:
- 今天用 Jazzy 开发,本地用 rmw_cyclonedds_cpp 确保稳定性
- 计划在 2026 年 Q3 迁移到 Lyrical(等 Nav2/MoveIt2/ros2_control/slam_toolbox/Isaac ROS 都有 Lyrical 二进制包)
- 多机器人/车队场景按需添加 rmw_zenoh
- 不要把 Humble 作为新项目目标——只剩一年支持
- 不要基于 Kilted 做多年产品——2026 年 11 月就 EOL
11. 生态成熟度评估 ⭐¶
11.1 成熟可用(放心使用) ⭐¶
Nav2、MoveIt 2、ros2_control(RL 策略和真实硬件之间的必经之路,PREEMPT_RT 下 1 kHz 运行)、现代 Gazebo + ros_gz、rviz2、rosbag2 + MCAP、Autoware Universe、micro-ROS(MCU 的 DDS-XRCE)、NVIDIA Isaac ROS(NITROS 零拷贝 CUDA)、Foxglove Studio
11.2 仍在追赶 ⭐¶
部分小众传感器驱动(某些厂商激光/工业相机仍只有 Humble 版或社区移植)、旧版 rqt/rviz 插件(Foxglove 已基本替代)、rclpy tf2 Python 绑定(比 C++ 慢)
11.3 工业采用(2026 年 5 月 13 日快照) ⭐¶
Autoware Foundation(汽车)、Open-RMF(医院/机场/仓库车队管理)、NASA(VIPER/CADRE 月球任务)、Amazon Robotics、iRobot Create 3、Apex.AI(汽车安全认证)、NVIDIA Isaac Sim/Lab、BMW(via RoboStack)、Boston Dynamics Spot、Clearpath(现 Rockwell)、Agility Digit、Figure、1X——全部暴露 ROS2 API
11.4 SLAM 生态 ⭐⭐¶
- 2D 轮式:
slam_toolbox - 2D + IMU 室内:
cartographer_ros - 3D RGB-D/立体/激光:RTAB-Map
- 3D 激光惯性里程计:社区移植的 LIO-SAM、FAST-LIO2、Point-LIO
- GPU 加速视觉 SLAM:Isaac ROS cuVSLAM
11.5 RL 运动控制生态 ⭐⭐¶
- 在 Isaac Lab 或 MuJoCo MJX 中训练
- 导出策略为 ONNX/TorchScript
- 部署为
ros2_control控制器或组合推理节点 - 永远不要让 RL 计算图直接与硬件通信而不经过
ros2_control -
200 Hz 实时推理:组合 C++ 节点 + loaned messages + TensorRT
- Python rclpy 可接受到 ~50–100 Hz
接下来深入讲解 ROS1 假设失效的历史脉络和 DDS/RMW/QoS 的机制与工程取舍。前面的概述已经给出了全貌,本节把每个机制拆开,从"为什么这样设计"的角度展开。
12. ROS1 假设失效的历史脉络 ⭐⭐¶
这一节解决什么问题:ROS2 的设计不是从”更现代 API”出发,而是从 ROS1 的部署假设逐步失效出发。只有理解这些假设为什么失效,才能理解 DDS、QoS、Lifecycle、Composition 和 RMW 为什么同时出现。本节比第 1 节更深入,用具体工程案例展示每个假设失效时的具体症状和修复路径。
12.1 PR2 时代的合理选择 ⭐⭐¶
ROS1 的很多设计在今天看起来脆弱,但放回 2007-2010 年的 PR2 时代并不荒唐。那时的主要目标是让研究人员快速把感知、规划、控制模块拼起来,而不是让几百台机器人在弱网络和安全边界中运行多年。理解"当时为什么合理"比直接批评"今天为什么不行"更有价值——因为同样的设计权衡思维在今天仍然适用。
| 当时环境 | 设计选择 | 为什么当时合理 |
|---|---|---|
| 单台机器人 | 一个 rosmaster |
节点发现简单,调试直观 |
| 实验室局域网 | TCPROS 默认可靠传输 | 网络稳定,丢包不是主要矛盾 |
| 研究原型 | 参数服务器集中存储 | 启动脚本方便覆盖参数 |
| 桌面级 CPU | XML-RPC + Python 工具链 | 易开发,性能足够 |
| 默认可信 | 无内建认证和授权 | 学术环境中安全不是首要目标 |
这很像早期单机程序到分布式服务的演进。单机程序可以把配置写在本地文件中,可以默认所有模块都可信,可以靠进程启动顺序保证依赖存在;一旦变成跨机器服务,配置一致性、服务发现、身份认证、网络分区就会变成一等问题。ROS1 到 ROS2 的迁移,本质上也是从“机器人内部脚本集合”走向“机器人分布式系统”。
12.2 五个默认假设逐个失效 ⭐⭐¶
ROS1 的默认假设可以拆成五类,每一类都对应一个后来在 ROS2 中被显式建模的机制:
| ROS1 假设 | 失效场景 | ROS2 机制 |
|---|---|---|
| 中心 master 总是可达 | 多机器人、跨子网、网络断连 | DDS 分布式发现、Zenoh 路由 |
| 一种传输语义足够 | 图像、控制、地图缓存语义不同 | QoS |
| 进程间拷贝可接受 | 点云、图像、深度图吞吐量爆炸 | Composition、进程内通信、共享内存 |
| 节点启动后一直正常 | 工业设备需要可控启停 | Lifecycle |
| 运行环境默认可信 | 车队、工厂、医院、公共网络 | DDS-Security、SROS2 |
反事实地看,如果 ROS1 继续沿着“给 rosmaster 加高可用、给 TCPROS 加更多选项、给参数服务器加权限”的路线走,最终会得到一套越来越像 DDS 的系统,但还要自己维护标准、互操作、安全和实时语义。ROS2 选择 DDS,就是把这些已经被工业中间件长期处理的问题下沉到标准层。
本质洞察:ROS2 的复杂度不是“新框架变复杂”,而是 ROS1 把通信语义、生命周期、安全和部署边界隐藏在默认值里;ROS2 把这些边界拿到接口层,让工程师必须显式选择。
12.3 rosmaster 的中心化代价 ⭐⭐¶
在 ROS1 中,节点启动时先向 rosmaster 注册自己发布和订阅的话题。rosmaster 告诉发布者与订阅者彼此的位置,之后数据面通常直接走 TCPROS。这个模型有两个好处:
rosnode list、rostopic list非常直观。- 初学者能快速理解“先启动 master,再启动节点”。
但中心化发现也带来三类代价:
| 代价 | 表现 |
|---|---|
| 单点协调 | master 不在时新节点无法完成发现 |
| 多机器人复杂 | 多个 master 间同步命名空间和话题非常脆弱 |
| 嵌入式困难 | XML-RPC 与动态字符串处理不适合 MCU |
在一台机器人上,这些代价可以接受;在仓库车队中,假设 50 台机器人同时进出 WiFi 覆盖区,中心化同步会遇到频繁重连、名称冲突、话题重复和状态陈旧问题。DDS 的分布式发现不是为了“更酷”,而是为了让每个参与者独立发布自己的存在和端点信息。
12.4 TCPROS 与 UDPROS 的语义不足 ⭐⭐¶
ROS1 的通信选项主要是:
| 机制 | 能表达什么 | 不能表达什么 |
|---|---|---|
queue_size |
本地队列深度 | 可靠性、过期时间、延迟加入缓存 |
latch |
后加入订阅者收到最后一条 | 多样化持久性策略 |
| TCPROS | 可靠字节流 | 低延迟丢帧优先 |
| UDPROS | UDP 传输 | 标准化 QoS 兼容规则 |
这导致开发者把通信语义写进业务代码。例如:
- 激光雷达回调里手动丢弃旧帧。
- 地图发布节点手动保存最后一帧并在新订阅者出现时重发。
- 控制命令订阅者手动检查时间戳,过期后停车。
- 诊断节点自己维护心跳超时。
这些逻辑不是不该写,而是不应该每个包都各写一套。QoS 的价值在于把“这条数据应该如何被传输和缓存”从业务代码中抽出来,变成发布者与订阅者之间的协议。
12.5 nodelet 的经验与 Composition 的升级 ⭐⭐¶
ROS1 后期已经意识到进程间拷贝对图像和点云很重,于是引入 nodelet。nodelet 的经验非常重要:它证明了”多个计算组件共享进程”能显著降低内存和 CPU 开销。image_pipeline 中的 image_proc 就是典型案例——把去畸变、颜色转换和校正放在同一个 nodelet manager 中,避免了大图像的多次拷贝。
但 nodelet 有两个结构问题:
| 问题 | 后果 | 具体表现 |
|---|---|---|
| API 与普通节点不同 | 同一个算法常要适配两套入口 | 必须同时维护 main() 入口和 Nodelet::onInit() 入口 |
| 管理器模型额外复杂 | 调试崩溃、加载顺序、符号冲突更麻烦 | 一个 nodelet 崩溃会带走整个 manager 内的所有 nodelet |
这很像 Web 服务器中”独立进程模式”和”线程池模式”的选择——线程池更高效,但一个线程的崩溃可能影响整个进程。ROS2 的 Composition 把”可组合节点”变成一等能力。一个 rclcpp::Node 子类既可以单独运行,也可以加载进 component container。这样,开发者不需要在”普通节点”和”进程内组件”之间维护两套代码。
反事实推理:如果 ROS2 没有统一 Node API 和 Composition,而是保留了 nodelet 式的双 API,那么 Nav2 的 30+ 个包每个都需要同时支持独立运行和组合运行两种入口。维护成本会翻倍,bug 也更容易在”只在一种模式下复现”的情况中出现。Composition 的统一 API 让 Nav2 可以在 launch 文件中通过一个参数切换模式,而不需要改动任何 C++ 代码。
12.6 安全性缺失的真实代价 ⭐⭐⭐¶
ROS1 没有内建安全机制。在学术实验室中,这几乎不是问题——所有用户都是可信的研究人员,网络是受保护的局域网。但当机器人进入以下场景时,安全缺失的代价变得很高:
- 仓库车队:多台机器人共享 WiFi,一台机器人的恶意或错误的
/cmd_vel发布可能控制另一台机器人。 - 医院配送:机器人在公共走廊运行,需要防止未授权的远程操作。
- 自动驾驶测试车:ROS2 节点暴露在车载网络中,需要与 AUTOSAR Adaptive 的安全要求对齐。
ROS2 通过 DDS-Security 标准和 SROS2 工具链提供了三层安全:认证(X.509 证书验证参与者身份)、授权(权限文件定义谁能读写哪些话题)、加密(数据在 UDP 上加密传输)。这不是 ROS2 自己发明的安全协议,而是复用了 DDS 标准中已有的安全扩展——与军事和金融系统使用相同的标准。
12.7 常见陷阱¶
| 类型 | 错误理解 | 后果 | 正确理解 |
|---|---|---|---|
| 概念误区 | ROS2 只是 ROS1 换 API | 迁移时只改函数名,忽略 QoS 和生命周期 | ROS2 是通信语义和部署模型升级 |
| 思维陷阱 | 认为中心 master 更简单所以总是更好 | 多机器人和弱网络中同步逻辑膨胀 | 单机简单与分布式可靠是不同目标 |
| 工程陷阱 | 用业务代码模拟 QoS | 每个节点一套重传、缓存、超时逻辑 | 优先使用 DDS QoS 或 RMW 能力 |
| 调试陷阱 | 只看节点是否存在 | 节点存在但 QoS 不兼容,数据不流动 | 同时检查端点 QoS |
12.8 练习¶
- 选择一个 ROS1 激光 SLAM 包,列出它依赖的 ROS1 默认假设:master、TCPROS、参数服务器、tf、bag。逐项说明迁移到 ROS2 时哪个假设需要重新设计。
- 设计一个仓库中 20 台移动机器人的发现方案。比较“单 master 桥接”“DDS 多播”“Fast DDS Discovery Server”“Zenoh 路由”四种方案的网络假设。
- 回顾 软件工程/ROS2高级集成 的 QoS 内容,给
/scan、/map、/cmd_vel、/diagnostics分别选择 QoS,并解释每个选择背后的失败模式。
13. DDS / RMW / QoS 的机制与工程取舍 ⭐⭐¶
这一节解决什么问题:把 ROS2 通信栈拆成可理解的层次:DDS 负责数据分发语义,RMW 负责隔离供应商实现,QoS 负责表达端点契约。工程选型时要问“问题在哪一层”,不能把所有通信问题都归咎于 ROS2。
13.1 DDS 的实体模型 ⭐⭐¶
DDS 的核心对象可以按层次理解:
对应到 ROS2:
| DDS 概念 | ROS2 中的直观对应 | 作用 |
|---|---|---|
| DomainParticipant | 一个进程中的 DDS 参与者 | 加入某个 domain |
| Topic | ROS2 话题映射后的 DDS 主题 | 定义名称与类型 |
| DataWriter | Publisher 端点 | 写样本 |
| DataReader | Subscription 端点 | 读样本 |
| QoS | 发布/订阅策略 | 决定是否匹配与如何传输 |
ROS_DOMAIN_ID 不是 ROS2 自己维护的逻辑分组,而是进入 DDS domain 的参数。domain 不同,发现端口不同,图也隔离。它像局域网中的 VLAN:能隔离流量和发现范围,但不是完整安全边界。安全仍然要依赖认证、权限和加密。
13.2 发现机制:SPDP 与 SEDP ⭐⭐¶
DDS 发现分两步:
- SPDP:参与者发现。每个参与者公告“我在这个 domain 中存在”。
- SEDP:端点发现。参与者之间交换“我有哪些 DataWriter、DataReader、Topic、Type 和 QoS”。
这解释了两个常见现象:
| 现象 | 机制解释 |
|---|---|
ros2 topic list 不需要 master |
发现信息来自 DDS 内置端点 |
| 某些 WiFi 下 ROS2 图不完整 | 多播或单播发现包被网络设备拦截 |
如果网络禁止多播,Fast DDS Discovery Server、CycloneDDS 显式 peers、Zenoh 路由就是在解决“发现信息如何到达对方”的问题,而不是改变 ROS2 的上层 API。
13.3 RMW:为什么 ROS2 不直接绑定一个 DDS ⭐⭐¶
RMW 的层次如下:
应用代码
↓
rclcpp / rclpy
↓
rcl
↓
rmw
↓
rmw_fastrtps_cpp / rmw_cyclonedds_cpp / rmw_connextdds / rmw_zenoh_cpp
↓
具体中间件库
RMW 的价值是隔离实现差异:
| 没有 RMW 会怎样 | RMW 提供的能力 |
|---|---|
| ROS2 被某个 DDS 供应商绑定 | 可切换 Fast DDS、Cyclone DDS、Connext、Zenoh |
| Python 绑定必须绑定 DDS C++ API | rclpy 绑定稳定的 C API |
| 生态无法适配非 DDS 中间件 | Zenoh 可作为 RMW 接入 |
| 不同平台修复困难 | RMW 层隔离平台和供应商差异 |
RMW 不是”性能开销很大的中间层”。它更像操作系统中的驱动接口:统一上层语义,让底层实现可以替换。实际瓶颈通常来自序列化、拷贝、网络发现、QoS 重传和应用回调,而不是 RMW 函数调用本身。
跨领域类比:RMW 层就像数据库应用中的 ORM(对象关系映射)。应用代码通过 ORM 的统一 API 操作数据,不直接写 SQL。切换数据库(MySQL → PostgreSQL)只需换驱动,应用代码不变。RMW 起同样的作用——应用代码通过 rclcpp/rclpy 操作话题和服务,切换中间件(Fast DDS → CycloneDDS → Zenoh)只需换一个环境变量,节点代码不变。ORM 有性能开销,但对大多数应用来说远小于业务逻辑本身;RMW 也是如此。
工程上确认 RMW 的做法很简单:
# 确认当前使用的 RMW
ros2 doctor --report | grep middleware
# 环境变量切换
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
# 在 launch 文件中为特定节点设置
from launch.actions import SetEnvironmentVariable
SetEnvironmentVariable('RMW_IMPLEMENTATION', 'rmw_cyclonedds_cpp')
切换 RMW 后务必确认所有节点使用相同的实现——不同 RMW 的节点之间无法通信(Fast DDS 节点看不到 Zenoh 节点,反之亦然)。这是一个常见的调试陷阱:在某个终端 export 了不同的 RMW,导致该终端的节点与其他终端隔离。
13.4 QoS 兼容性:请求不能强于提供 ⭐⭐¶
QoS 匹配遵循 requested/offered 模型:订阅者请求某种能力,发布者提供某种能力。请求不能超过提供。
| 发布者提供 | 订阅者请求 | 是否匹配 | 解释 |
|---|---|---|---|
| Reliable | BestEffort | 是 | 发布者能力更强 |
| BestEffort | Reliable | 否 | 订阅者要求可靠,但发布者不提供 |
| TransientLocal | Volatile | 是 | 发布者有缓存,订阅者不用也可以 |
| Volatile | TransientLocal | 否 | 订阅者要求延迟加入缓存,但发布者没有 |
这条规则是很多 ROS2 “话题存在但没有数据”的根源。节点图只能说明端点存在,不能说明 QoS 兼容。
13.5 QoS 事件回调示例 ⭐⭐¶
生产节点不应该让 QoS 不兼容沉默发生。可以注册事件回调:
#include <rclcpp/rclcpp.hpp>
#include <sensor_msgs/msg/laser_scan.hpp>
class ScanConsumer : public rclcpp::Node {
public:
ScanConsumer() : Node("scan_consumer") {
rclcpp::SubscriptionOptions options;
options.event_callbacks.incompatible_qos_callback =
[this](rclcpp::QOSRequestedIncompatibleQoSInfo& info) {
RCLCPP_ERROR(
get_logger(),
"QoS 不兼容,策略编号: %d,累计次数: %d",
info.last_policy_kind,
info.total_count);
};
sub_ = create_subscription<sensor_msgs::msg::LaserScan>(
"scan",
rclcpp::SensorDataQoS(),
[this](sensor_msgs::msg::LaserScan::ConstSharedPtr msg) {
// 高频传感器回调中只做轻量处理,避免阻塞后续数据
last_stamp_ = msg->header.stamp;
},
options);
}
private:
builtin_interfaces::msg::Time last_stamp_;
rclcpp::Subscription<sensor_msgs::msg::LaserScan>::SharedPtr sub_;
};
这个例子体现了一个原则:通信问题要在通信层暴露,而不是等 SLAM 后端“没有新数据”时才猜测原因。
13.6 常见话题的 QoS 决策表 ⭐⭐¶
| 话题 | 推荐 QoS | 原因 |
|---|---|---|
/camera/image_raw |
BestEffort + Volatile + depth 2-5 | 新图像比旧图像重要,允许丢帧 |
/scan |
BestEffort + Volatile + depth 5 | 低延迟优先,激光数据频率高 |
/imu |
BestEffort 或 Reliable,视链路质量决定 | 高频、小消息,控制链路可用 Reliable |
/map |
Reliable + TransientLocal + depth 1 | 地图状态需要延迟加入可见 |
/tf_static |
Reliable + TransientLocal | 静态变换必须被后启动节点收到 |
/cmd_vel |
BestEffort + Volatile + depth 1 | 过期命令不能继续执行 |
/diagnostics |
Reliable + Volatile + depth 10 | 诊断信息不应轻易丢失 |
本质洞察:QoS 不是“可靠越高越好”,而是把每条数据的时间价值、丢失代价和缓存需求写成通信契约。传感器流重视新鲜度,地图和静态 TF 重视可达性,控制命令重视过期后失效。
13.7 RMW 选型不是单纯性能排名 ⭐⭐¶
RMW 选择应按网络环境和产品约束判断:
| 约束 | 优先考虑 |
|---|---|
| 默认发行版兼容 | Fast DDS |
| 本机或局域网稳定通信 | Cyclone DDS 或 Fast DDS |
| 企业网络、多子网、多播受限 | Fast DDS Discovery Server、Cyclone peers、Zenoh |
| 广域网、弱网、网状网络 | Zenoh |
| 安全认证或商业支持 | RTI Connext |
| 单机大消息低拷贝 | DDS + shared memory / composition |
不要把“某个基准测试中更快”直接等同于“项目中更好”。同一个 RMW 在不同消息大小、QoS、线程模型、发现配置和网络设备下表现会明显不同。
13.8 RMW 切换与确认 ⭐⭐¶
切换 RMW:
确认当前进程使用的 RMW:
在 launch 文件中为单个进程设置环境变量:
from launch import LaunchDescription
from launch.actions import SetEnvironmentVariable
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
# 只影响后续 launch action 的环境变量
SetEnvironmentVariable(
name='RMW_IMPLEMENTATION',
value='rmw_cyclonedds_cpp'),
Node(
package='demo_nodes_cpp',
executable='talker',
name='talker'),
])
13.9 常见陷阱¶
上面讲完了 RMW 切换的具体操作,下面整理在实际项目中最容易踩的坑——这些陷阱往往不是某一层单独的问题,而是 DDS、RMW 和 QoS 三层交叉作用的结果:
| 类型 | 错误做法 | 现象 | 正确做法 |
|---|---|---|---|
| 概念误区 | 认为换 RMW 能解决所有通信问题 | QoS 不兼容仍然不通 | 先检查 QoS、domain、网络发现 |
| 编程陷阱 | 订阅者 Reliable,发布者 BestEffort | 话题存在但无数据 | 让请求不强于提供 |
| 思维陷阱 | 把 ROS_DOMAIN_ID 当安全机制 |
不同 domain 仍在同一网络中可被观察 | 生产隔离使用 SROS2/DDS-Security |
| 工程陷阱 | 在同一系统中混用多个 RMW 但不做隔离 | 部分节点互相看不见 | 统一 RMW 或显式桥接 |
13.10 练习¶
- 写一个最小发布/订阅程序,发布者使用
SensorDataQoS(),订阅者使用 Reliable。观察是否收到数据,并解释 requested/offered 规则。 - 在同一台机器上启动两个终端,分别设置不同
ROS_DOMAIN_ID。用ros2 topic list验证图隔离。 - 对同一个点云话题分别测试 Fast DDS、Cyclone DDS 和 Zenoh,记录发现时间、平均延迟和丢包情况下的恢复行为。
14. Zenoh 的位置:不是更快 DDS,而是不同网络假设 ⭐⭐⭐¶
这一节解决什么问题:Zenoh 常被误解成“DDS 的高速替代品”。更准确的理解是:Zenoh 面向跨网络、弱连接和边缘到云的数据空间,而 DDS 面向实时局域网数据分发。两者解决的问题重叠,但假设不同。
14.1 DDS 与 Zenoh 的默认网络假设 ⭐⭐⭐¶
| 维度 | DDS 默认假设 | Zenoh 默认假设 |
|---|---|---|
| 发现 | 局域网多播或显式对端 | 路由器、gossip、可跨 NAT |
| 数据模型 | Topic + typed sample | Key expression + pub/sub/query |
| 典型网络 | 机器人内部、局域网 | 广域网、边缘、云、mesh |
| 互操作 | DDS 标准互操作 | Zenoh 生态互操作 |
| 强项 | 实时 QoS、共享内存、工业 DDS 生态 | 弱网可达性、低发现流量、跨域连接 |
这很像局域网文件共享和对象存储的区别。局域网共享在同一网络内非常直接,延迟低;对象存储天然考虑跨地域、权限、路由和缓存。Zenoh 的优势不是“永远比 DDS 快”,而是在 DDS 多播假设不成立时,仍然能维持可达的数据空间。
14.2 rmw_zenoh_cpp 的工程含义 ⭐⭐⭐¶
rmw_zenoh_cpp 把 ROS2 的发布/订阅、服务、动作等语义映射到 Zenoh。对应用层来说,代码仍然是:
切换的是底层通信实现:
这带来两个好处:
- 应用代码不用直接使用 Zenoh API。
- ROS2 图中的节点可以在弱网络中获得更好的发现和路由能力。
但也要接受两个边界:
| 边界 | 工程影响 |
|---|---|
| 非 DDS | 不能直接与 DDS RMW 节点互通 |
| 缺少等价 DDS SHM 路径 | 单机多 GB/s 图像管线仍优先用 composition + DDS/SHM |
14.3 三种跨网络方案对比 ⭐⭐⭐¶
| 方案 | 适合场景 | 优点 | 代价 |
|---|---|---|---|
| Fast DDS Discovery Server | 多播受限但有固定服务器 | 保持 DDS 语义,配置集中 | 需要维护服务器和客户端配置 |
| CycloneDDS peers | 节点 IP 相对固定 | 简单、透明 | 动态拓扑维护成本高 |
| Zenoh | WAN、NAT、mesh、云边协同 | 路由友好,发现流量低 | 与 DDS RMW 不直接互通 |
如果机器人都在同一台主机内运行,Zenoh 不一定是最优答案;如果机器人穿过多个子网、VPN、蜂窝网络和云端服务,Zenoh 的网络假设更贴近实际。
14.4 混合系统的边界设计 ⭐⭐⭐¶
一个常见架构是:
这样本机大消息管线保持低拷贝,跨机器人状态同步交给 Zenoh。边界节点只发布必要数据,例如位姿、任务状态、低频地图块和诊断摘要,而不是把每帧原始点云都跨网络广播。
这种混合设计的关键是定义清晰的数据边界。一个实用的原则是:带宽密集数据(点云 10 Hz × 4 MB = 40 MB/s、图像 30 Hz × 3 MB = 90 MB/s)只在本机内流动;状态数据(位姿 50 Hz × 100 B、任务状态 1 Hz × 1 KB)可以跨网络。这个边界不是技术限制,而是带宽现实——4G 网络上行通常只有 5-10 Mbps,远不够传输原始点云。
具体到多机器人 SLAM 场景:
| 数据类型 | 本机处理 | 跨网络传输 | 原因 |
|---|---|---|---|
| 原始点云 | SLAM 前端处理 | 不传 | 带宽不可承受 |
| 关键帧位姿 | 存储在本地图中 | 1 Hz 传输 | 数据量小,用于多机器人地图合并 |
| 局部子地图 | 本地 costmap | 按需拉取 | 只在协作建图时需要 |
| 诊断摘要 | 持续监控 | 1 Hz 传输 | 车队管理需要 |
| 紧急停车信号 | 立即执行 | 立即广播 | 安全关键 |
14.5 常见陷阱¶
混合架构设计看起来优雅,但工程实践中最常出错的地方恰恰在本机通信与跨网络通信的边界上。以下是四类典型陷阱:
| 类型 | 错误做法 | 现象 | 正确做法 |
|---|---|---|---|
| 概念误区 | 认为 Zenoh 是 DDS 的一个配置 | DDS 节点与 Zenoh RMW 节点互相不可见 | 明确桥接或统一 RMW |
| 工程陷阱 | 把原始图像和点云直接推到 WAN | 带宽耗尽,延迟波动 | 跨网络只传压缩、降采样或状态摘要 |
| 思维陷阱 | 因为 Zenoh 弱网好就替换所有本机通信 | 本机共享内存优势丢失 | 本机大消息仍优先 composition |
| 调试陷阱 | 只检查 ROS2 图,不检查路由器状态 | 节点存在但跨域不可达 | 同时检查 RMW、路由和防火墙 |
14.6 练习¶
- 设计一个三机器人协同建图系统:本机点云处理用 DDS,机器人之间交换子地图摘要用 Zenoh。画出数据边界。
- 比较“直接传原始点云”和“传关键帧位姿 + 局部地图块”的带宽需求。假设点云 10 Hz,每帧 4 MB,关键帧 1 Hz,每帧 200 KB。
- 解释为什么
rmw_zenoh_cpp晋升 Tier-1 不等于所有 ROS2 项目都应该默认使用 Zenoh。
15. 版本选择决策流程 ⭐⭐¶
这一节解决什么问题:发行版选择不是背表格,而是把项目寿命、Ubuntu 版本、依赖生态、硬件平台和通信中间件一起纳入决策。
15.1 先问项目寿命 ⭐⭐¶
第一层问题:项目要活多久?
只是课程实验或论文复现
→ 可用当前稳定 LTS,也可试新发行版
一年内交付的研究平台
→ 优先 Jazzy,必要时评估 Lyrical
三到五年产品
→ 选择 LTS,避开非 LTS 作为主线
已有 Humble 车队
→ 保持维护,但制定 Jazzy/Lyrical 迁移窗口
非 LTS 的价值是提前接触新特性,例如 Kilted 中的 Zenoh Tier-1;它不适合作为多年产品主线,因为支持周期只有 18 个月。
15.2 再问操作系统 ⭐⭐¶
ROS2 发行版与 Ubuntu 版本强绑定:
| Ubuntu | 推荐 ROS2 | 说明 |
|---|---|---|
| 22.04 | Humble | 现有系统维护;新项目不优先 |
| 24.04 | Jazzy / Kilted | 2026 年上半年生产主线仍以 Jazzy 最稳 |
| 26.04 | Lyrical | 适合新一代平台,但要确认依赖生态 |
如果硬件供应商只提供 Ubuntu 22.04 驱动,新项目也可能不得不留在 Humble 一段时间。此时应把 Humble 视为”硬件约束下的过渡选择”,而不是新的长期技术基线。
嵌入式平台的特殊考量:Jetson 系列(Orin/Xavier)通常使用 NVIDIA 提供的 JetPack SDK,其 Ubuntu 版本可能落后于桌面发行版。JetPack 6.x 基于 Ubuntu 22.04,与 Humble 对应;JetPack 7.x 预计基于 Ubuntu 24.04,与 Jazzy 对应。在 Jetson 上使用 ROS2 时,操作系统版本通常由 JetPack 决定而非自由选择——这是嵌入式部署中常见的版本约束。容器化(Docker + NVIDIA Container Toolkit)可以缓解这个约束,但会增加部署复杂度和调试难度。
ROS2 与 C++ 标准的演进:ROS2 的 C++ 标准也在随发行版升级。Humble 使用 C++17,Jazzy 和 Kilted 仍使用 C++17,Lyrical 预计支持 C++20。C++20 的 Concepts、Ranges 和 Coroutines 对机器人软件开发有实际价值——Concepts 可以让模板接口更清晰(替代 SFINAE),Ranges 简化数据管线,Coroutines 为异步 I/O 提供更自然的表达。但迁移到 C++20 需要确认所有依赖库(Eigen、PCL、Pinocchio 等)兼容。
反事实推理:如果 ROS2 不绑定 Ubuntu 版本而是像 Conda 那样独立管理依赖,开发者可以在任意 Linux 发行版上使用任意 ROS2 版本。这听起来很灵活,但实际上会导致组合爆炸——每种 Linux 发行版 × 每种编译器版本 × 每种库版本的组合都需要测试。ROS2 选择与 Ubuntu LTS 绑定,本质上是用灵活性换测试覆盖率和二进制包的可靠性。RoboStack(通过 Conda 分发 ROS2)提供了跨平台替代,但支持的包数量远少于官方 apt 仓库。
15.3 发行版决策流程图 ⭐⭐¶
开始
│
├─ 是否必须使用 Ubuntu 22.04?
│ ├─ 是 → Humble,仅用于维护或硬件约束,安排迁移
│ └─ 否
│
├─ 是否需要 2026 年立刻稳定交付?
│ ├─ 是 → Jazzy
│ └─ 否
│
├─ 是否需要 Ubuntu 26.04 / C++20 / 新 LTS 周期?
│ ├─ 是 → 评估 Lyrical,等待关键包二进制齐全
│ └─ 否
│
├─ 是否主要为了验证新 RMW 或新 API?
│ ├─ 是 → Kilted 或 Rolling,仅用于实验分支
│ └─ 否 → Jazzy
15.4 RMW 决策流程图 ⭐⭐¶
开始
│
├─ 是否有商业认证或供应商支持要求?
│ ├─ 是 → 评估 RTI Connext
│ └─ 否
│
├─ 是否跨 WAN / NAT / mesh / 云边网络?
│ ├─ 是 → 评估 rmw_zenoh_cpp
│ └─ 否
│
├─ 是否本机大消息管线占主导?
│ ├─ 是 → composition + DDS/SHM,比较 Fast DDS 与 Cyclone DDS
│ └─ 否
│
├─ 是否希望最贴近发行版默认?
│ ├─ 是 → Fast DDS
│ └─ 否 → Cyclone DDS 与 Fast DDS 做项目基准
15.5 三个典型决策案例 ⭐⭐¶
| 项目 | 推荐选择 | 理由 |
|---|---|---|
| 2026 年 Q2 启动的 SLAM + RL 研究平台 | Jazzy + Cyclone DDS/Fast DDS 对比 | 生态成熟,Ubuntu 24.04 稳定,迁移成本低 |
| 车队跨园区弱网协同 | Jazzy 或 Lyrical + Zenoh 试点 | 重点矛盾是跨网络可达性 |
| 已部署 Humble 机械臂产线 | 短期维护 Humble,规划 Lyrical/Jazzy 迁移 | 直接换发行版会影响驱动和验证流程 |
“今天用 Jazzy,规划 Lyrical 验证”是 2026 年 5 月比较稳妥的节奏:Jazzy 仍是成熟 LTS,Lyrical 代表新的 LTS 周期,但关键生态包的二进制可用性需要按项目确认。
跨领域类比:发行版选择很像选择手机操作系统版本。最新版(Rolling/Kilted)有新功能但应用兼容性不完整;上一代 LTS(Jazzy)是稳定的主力;再上一代(Humble)进入维护末期。你不会在生产手机上装开发者预览版,同样也不应该在产品机器人上用非 LTS 发行版。
15.6 为什么不能同时追 LTS 和 Rolling ⭐⭐¶
一些团队试图在产品主线上用 LTS,同时在开发分支上跟踪 Rolling 以获取最新功能。这种做法在短期内看起来"两全其美",但长期会引入维护负担:
-
API 差异累积。Rolling 上的 API 变更(如
ament_target_dependencies弃用)可能不向后兼容。如果开发分支大量使用 Rolling API,合并回 LTS 分支时需要批量改写。 -
依赖冲突。Rolling 版本的第三方包(如 Nav2、MoveIt2)可能与 LTS 版本不兼容。在 Rolling 上验证通过的配置迁移到 LTS 时可能编译失败。
-
测试覆盖翻倍。CI 需要同时在 LTS 和 Rolling 上构建和测试,资源消耗翻倍。
更可持续的做法是:产品主线锁定当前 LTS(Jazzy),在新 LTS 发布后(Lyrical)开一个验证分支,在验证分支上逐步完成迁移和回归测试,验证通过后整体切换。Rolling 只用于个人实验和新功能预览,不进入产品分支。
15.7 迁移窗口的计算 ⭐⭐¶
迁移不是等 EOL 前一个月开始。一个生产机器人系统迁移通常包含:
| 阶段 | 典型耗时 | 输出 |
|---|---|---|
| 依赖盘点 | 1-2 周 | 包列表、驱动约束、系统镜像约束 |
| 构建迁移 | 2-6 周 | colcon workspace 可构建 |
| API 适配 | 1-3 月 | 节点、launch、参数、QoS 适配 |
| 仿真回归 | 1-2 月 | rosbag、Gazebo、Isaac Sim 回归 |
| 实机验证 | 1-3 月 | 安全、性能、稳定性验证 |
因此 Humble 系统不能等到 2027 年 5 月才考虑迁移。对于有实机安全要求的项目,2026 年就应该开始新发行版分支验证。
15.8 版本选择常见陷阱¶
迁移窗口的规划解决了"何时迁移"的问题,但版本选择本身也有常见误区。以下陷阱贯穿发行版选择、RMW 绑定和环境记录三个层面:
| 类型 | 错误做法 | 现象 | 正确做法 |
|---|---|---|---|
| 思维陷阱 | 永远追最新发行版 | 依赖包缺失、接口变化频繁 | 产品主线优先 LTS |
| 工程陷阱 | 新项目继续锁定 Humble | 支持窗口不足,后续迁移更急 | 除硬件约束外优先 Jazzy/Lyrical |
| 概念误区 | 认为同一 ROS2 API 在所有发行版完全一致 | launch、QoS、RMW 默认行为有差异 | 固定发行版并做回归测试 |
| 调试陷阱 | 只记录 ROS_DISTRO,不记录 RMW | 同一代码通信行为不同 | 日志中记录 ROS_DISTRO、RMW、domain、QoS |
15.9 练习¶
- 为一个计划运行到 2030 年的室内配送机器人选择 ROS2 发行版和 RMW。要求列出操作系统、支持周期、Nav2、ros2_control、传感器驱动四类约束。
- 为一个本科课程实验平台选择发行版。比较 Jazzy、Kilted、Lyrical 的学习价值和维护成本。
- 给一个 Humble 车队写迁移计划,要求从 2026 年 6 月开始,到 2027 年 3 月完成实机验证。
16. 设计决策总结:ROS2 的六个核心工程权衡 ⭐⭐¶
这一节解决什么问题:回顾全章,把 ROS2 的设计归纳为六个核心工程权衡。每个权衡都不是"对错"而是"取舍"——理解取舍比记住结论更重要。
读完本章的 15 个小节后,可以把 ROS2 的设计决策总结为六个核心权衡。每一个权衡都反映了从实验室到生产环境的需求变化:
| 权衡 | ROS1 的选择 | ROS2 的选择 | 代价 | 收益 |
|---|---|---|---|---|
| 发现模型 | 中心化(rosmaster) | 分布式(DDS SPDP+SEDP) | 网络配置更复杂 | 无单点故障 |
| 通信语义 | 固定(TCP + queue_size) | 可配置(QoS 策略体系) | 学习成本更高、QoS 不匹配是新的错误类型 | 通信行为可预测、可约束 |
| 进程模型 | 一进程一节点 + nodelet | 统一 Node 类 + Composition | Composition 配置较复杂 | 一套代码支持独立和组合运行 |
| 节点状态 | 隐式(构造即运行) | 显式(Lifecycle 状态机) | 多写几个回调 | 启动顺序可控、状态可观测 |
| 安全模型 | 无(默认可信) | DDS-Security / SROS2 | 密钥管理、启动延迟 | 认证、授权、加密 |
| 中间件绑定 | 自有协议栈 | RMW 抽象 + 多供应商 | 间接层和供应商差异 | 可切换 DDS/Zenoh、供应商竞争 |
这些权衡不是"ROS2 比 ROS1 好"的简单结论。在单台机器人、局域网、实验室环境中,ROS1 的默认选择完全合理——简单、直接、调试容易。ROS2 的设计面向的是更广泛的部署场景:多机器人、弱网络、嵌入式、安全敏感和长期产品维护。
本质洞察:ROS2 的设计哲学可以概括为"把 ROS1 中被隐藏的工程假设显式化"。ROS1 假设网络可靠、单台机器人、默认可信、进程间拷贝可接受——这些假设在实验室里是透明的,在生产环境中变成了故障源。ROS2 不是"发明了新问题",而是"把旧问题从默认值里拿出来,放到接口上让工程师显式处理"。
练习¶
- [综合分析题] 从本章学到的六个权衡中,选择你认为对 SLAM 系统影响最大的两个,解释为什么,并说明在什么条件下 ROS1 的选择可能仍然是合理的。
- [设计题] 如果你今天要设计一个新的机器人中间件(不基于 DDS),你会保留 ROS2 的哪些设计决策、放弃哪些?为什么?
- [反事实推理题] 假设 ROS2 没有引入 Lifecycle 节点,而是继续用 ROS1 的隐式生命周期。Nav2 的启动编排(lifecycle_manager)需要用什么替代机制来保证节点按序初始化?
16-2. micro-ROS 与嵌入式边界 ⭐⭐¶
这一节解决什么问题:ROS2 的完整运行时需要 Linux 操作系统和数百 MB 内存,但机器人的底层硬件(电机驱动、传感器采样、急停保护)运行在资源极其有限的微控制器(MCU)上。micro-ROS 解决了"MCU 如何以 ROS2 消息格式参与通信图"的问题,但它不是"小型 ROS2"——理解它的能力边界和架构约束,是避免在嵌入式集成中走弯路的关键。
micro-ROS 的架构模型 ⭐⭐¶
micro-ROS(https://micro.ros.org/)不是在 MCU 上运行完整的 ROS2 栈。它的架构是客户端-代理模式:MCU 上运行一个极度精简的 ROS2 客户端库(基于 rcl 的子集),通过串口、UDP 或自定义传输连接到运行在上位机上的 micro-ROS Agent(一个 ROS2 节点)。Agent 负责把 MCU 的消息桥接到完整的 ROS2 图中。
MCU(STM32/ESP32/Teensy)
运行:micro-ROS 客户端
能力:发布话题、订阅话题、使用参数、服务
限制:无 DDS 发现、无完整 QoS、内存 < 100 KB
↕ 串口 / UDP / USB
上位机(Jetson / x86)
运行:micro-ROS Agent + 完整 ROS2
作用:桥接 MCU 消息到 DDS 图
这个架构意味着 MCU 不是 DDS 参与者——它不参与 SPDP/SEDP 发现,不维护 QoS 协商,不做 CDR 序列化。这些工作都由 Agent 代理完成。MCU 和 Agent 之间使用 DDS-XRCE(eXtremely Resource Constrained Environments)协议——一个专门为嵌入式设计的轻量传输协议,由 eProsima 的 Micro XRCE-DDS 实现。
跨领域类比:micro-ROS 之于完整 ROS2,就像蓝牙外设之于 WiFi 设备。蓝牙外设不直接参与 TCP/IP 网络——它通过手机(网关)代理通信。micro-ROS 的 MCU 也不直接参与 DDS 网络——它通过 Agent 代理。这个代理模式让 MCU 可以用极少资源"说 ROS2 的语言",但也意味着 Agent 是单点依赖——Agent 进程崩溃时,MCU 的 ROS2 连接会断开。
micro-ROS 能做什么与不能做什么 ⭐⭐¶
| 能力 | 支持 | 限制 |
|---|---|---|
| 发布话题 | 支持 | 消息大小受 MCU 内存限制 |
| 订阅话题 | 支持 | 回调深度有限 |
| 服务客户端/服务端 | 支持 | 超时处理由应用层负责 |
| 参数 | 支持 | 参数数量和名称长度受限 |
| 动作 | 不支持 | 动作的复杂状态机超出 MCU 能力 |
| Lifecycle | 部分支持 | 简化版生命周期 |
| 自定义消息 | 支持 | 需要交叉编译 IDL |
| QoS 配置 | 基础 | 仅支持 Reliability 和 History |
| TF2 | 不支持 | 内存需求过大 |
micro-ROS 的典型应用场景:
- STM32 上的 IMU 预处理和发布——MCU 以 1 kHz 读取 IMU,做卡尔曼滤波或互补滤波,以 200 Hz 发布到 ROS2 图。
- ESP32 上的电机驱动——订阅
/cmd_vel,转换为 PWM 或 CAN 命令。 - Teensy 上的急停和限位监控——读取急停开关和限位传感器,发布状态到 ROS2,但急停保护逻辑在 MCU 本地闭环,不依赖 Agent 存活。
不应该用 micro-ROS 做的事:
- 在 MCU 上运行 SLAM 或点云处理——计算和内存都不够。
- 把 micro-ROS 当作替代 ros2_control 硬件接口的方案——ros2_control 的 read-update-write 循环需要在上位机运行,MCU 只负责底层驱动。
- 依赖 micro-ROS Agent 做安全关键通信——Agent 是普通用户态进程,可能崩溃或延迟。
⚠️ micro-ROS 常见陷阱¶
⚠️ 工程陷阱:把急停逻辑放在 micro-ROS 话题回调中
错误做法:MCU 订阅
/estop话题,收到true时停止电机。现象/后果:Agent 进程崩溃或串口断连时,MCU 收不到
/estop消息,电机继续运行。根本原因:micro-ROS 通信依赖 Agent 存活。安全关键功能不能依赖任何通信链路的正常运行。
正确做法:急停开关直接接 MCU GPIO,在中断中立即切断电机使能。ROS2 话题只用于报告急停状态,不用于触发急停动作。
⚠️ 概念误区:认为 micro-ROS 让 MCU "变成了 ROS2 节点"
新手想法:"我的 STM32 运行了 micro-ROS,所以它是一个完整的 ROS2 节点,可以和其他节点一样使用。"
实际上:MCU 上的 micro-ROS 客户端不参与 DDS 发现,不支持完整 QoS,不能被
ros2 node info直接查询(需要通过 Agent 间接看到)。它是一个"代理参与者",不是独立的 DDS 参与者。正确理解:把 MCU 理解为"通过 Agent 桥接到 ROS2 图的外围设备",而不是"运行 ROS2 的小型计算机"。
练习¶
- [设计题] 为一个差速驱动移动机器人设计 MCU 和上位机的职责划分。MCU 负责什么、上位机负责什么?micro-ROS 在两者之间传递什么消息?
- [分析题] 如果 micro-ROS Agent 进程被 OOM killer 杀死,MCU 上的电机控制器应该做什么?设计一个不依赖 ROS2 通信的本地安全降级方案。
- [调研题] 访问 https://micro.ros.org/docs/tutorials/ ,找到 ESP32 上的 micro-ROS 示例。分析它的内存占用和消息发布频率上限。
17. ROS2 架构设计的七个核心权衡 ⭐⭐¶
这一节解决什么问题:贯穿本章的各个机制——DDS、QoS、Lifecycle、Composition、RMW、SROS2、Zenoh——看似是独立的技术选择,实际上都是同一组工程权衡的不同表现。把这些权衡显式化,有助于在未来面对新技术决策时使用同样的分析框架。
回顾全章,ROS2 的每一个设计决策都是在两个相互矛盾的目标之间做权衡。理解这些权衡比记住具体 API 更有长期价值——因为 API 会变,但设计权衡的结构不会变。
| 权衡 | 一端 | 另一端 | ROS2 的选择 |
|---|---|---|---|
| 简单性 vs 灵活性 | ROS1 的单一传输 | ROS2 的多 QoS 策略 | 偏向灵活性,代价是学习曲线 |
| 标准化 vs 自研 | 自建通信协议 | 采用 DDS 标准 | 偏向标准化,代价是 DDS 复杂度 |
| 性能 vs 抽象 | 直接调用 DDS API | 通过 RMW 隔离供应商 | 偏向抽象,代价是微小的调用开销 |
| 安全 vs 开箱即用 | 默认安全 | 默认可用 | 默认不安全,可选启用 |
| 显式 vs 隐式 | Lifecycle 显式状态 | ROS1 隐式启动 | 偏向显式,代价是更多代码 |
| 同构 vs 异构 | 统一中间件 | 多 RMW 共存 | 单次运行统一 RMW,可切换 |
| 本机性能 vs 跨网可达 | DDS SHM 零拷贝 | Zenoh 跨 NAT | 不做单一选择,提供多 RMW |
本质洞察:ROS2 的设计不是在追求"最优解",而是在追求"可选择的解"。ROS1 在每个维度上做了隐式选择(单一传输、无安全、隐式生命周期),ROS2 把这些选择暴露出来让工程师显式决定。这增加了初期的学习成本,但减少了后期的部署意外。
练习¶
- [思考题] 从上述七个权衡中选择你认为最重要的一个,解释为什么 ROS2 的选择是合理的,以及在什么极端场景下另一端可能更好。
- [综合题] 假设 2030 年要设计 ROS3,你会改变哪些权衡的方向?为什么?
- [类比题] 找到软件工程中另一个经历了类似"从隐式到显式"演进的框架或语言(提示:考虑 Python 2→3 的类型注解、JavaScript→TypeScript 等),分析其演进动机和社区反应与 ROS1→ROS2 的相似性。
18. QoS 高级模式与实时配置 ⭐⭐⭐¶
这一节解决什么问题:基础 QoS 配置(第 3 节)覆盖了"怎么让消息流动"。本节深入到"怎么让消息在实时控制系统中可预测地流动"——包括 Deadline/Liveliness QoS 的工程用法、QoS 事件驱动的系统健康监控、以及实时系统中的 DDS 配置策略。
18.1 Deadline 和 Liveliness:超越 Reliability ⭐⭐⭐¶
Reliability(可靠/尽力)和 Durability(持久/易失)是最常用的 QoS 策略,但在实时控制系统中,Deadline 和 Liveliness 同样重要——它们回答"数据是否按时到达"和"发布者是否仍然存活"。
Deadline QoS 定义了消息的最大期望间隔。如果发布者在 Deadline 内没有发布新消息,或订阅者在 Deadline 内没有收到新消息,DDS 会触发 on_offered_deadline_missed 或 on_requested_deadline_missed 事件。这对控制系统至关重要——如果传感器 500 ms 没有新数据,控制器应该知道并采取降级措施,而不是继续使用过期数据。
// 设置 Deadline QoS:期望每 100 ms 至少收到一帧
rclcpp::QoS qos_with_deadline(10);
qos_with_deadline.deadline(std::chrono::milliseconds(100));
rclcpp::SubscriptionOptions options;
options.event_callbacks.deadline_callback =
[this](rclcpp::QOSDeadlineRequestedInfo& info) {
RCLCPP_WARN(get_logger(),
"传感器数据超时!累计 %d 次,已超过 %d 个周期",
info.total_count, info.total_count_change);
// 触发降级:使用最后一帧数据 + 增大不确定性
};
auto sub = create_subscription<sensor_msgs::msg::LaserScan>(
"/scan", qos_with_deadline, scan_callback, options);
Liveliness QoS 监控发布者是否仍然"活着"。AUTOMATIC 模式下,DDS 运行时自动发送心跳;MANUAL_BY_TOPIC 模式下,应用代码必须定期调用 assert_liveliness()。如果心跳超时,订阅方触发 on_liveliness_changed 事件。
| QoS 策略 | 监控对象 | 触发条件 | 典型用途 |
|---|---|---|---|
| Deadline | 消息到达间隔 | 超过设定周期没有新消息 | 传感器超时检测 |
| Liveliness | 发布者存活 | 心跳超时 | 节点崩溃检测 |
| Lifespan | 消息有效期 | 消息在缓存中停留超过设定时间 | 控制命令过期丢弃 |
反事实推理:如果不使用 Deadline QoS 而是在应用层手写超时检测,会怎样?每个订阅者都需要维护一个定时器和最后收到消息的时间戳,在回调外检查是否超时。这不是不可行,但会在每个节点中重复同样的逻辑——与 ROS1 中手写通信语义的问题一样。QoS 的价值是把这些"通信层面的关注"统一下沉到中间件。
18.2 Lifespan:让过期命令自动失效 ⭐⭐⭐¶
Lifespan QoS 对控制命令特别有价值。考虑一个场景:控制节点以 50 Hz 发布 /cmd_vel,底盘驱动以 100 Hz 读取。如果控制节点卡顿了 200 ms,底盘驱动不应该继续执行 200 ms 前的速度命令——那时候机器人可能已经到了障碍物前面。
// 发布者设置 Lifespan:命令 50 ms 后过期
rclcpp::QoS cmd_qos(1);
cmd_qos.lifespan(std::chrono::milliseconds(50));
cmd_qos.reliability(RMW_QOS_POLICY_RELIABILITY_BEST_EFFORT);
auto pub = create_publisher<geometry_msgs::msg::Twist>(
"/cmd_vel", cmd_qos);
设置 Lifespan 后,DDS 会在消息超过有效期时自动丢弃——即使消息已经到达订阅者的缓存。这比在订阅回调中检查时间戳更可靠,因为检查逻辑由中间件保证,不会被应用层的 bug 绕过。
18.3 实时系统的 DDS 配置策略 ⭐⭐⭐⭐¶
对于 1 kHz 控制循环,DDS 的默认配置可能引入不可接受的延迟抖动。以下是生产系统中常见的实时配置步骤:
步骤一:锁定内存
// 在 main() 中锁定所有页面,防止页面错误
#include <sys/mman.h>
if (mlockall(MCL_CURRENT | MCL_FUTURE) != 0) {
perror("mlockall 失败");
// 页面错误会导致 ms 级延迟——控制系统不可接受
}
步骤二:使用实时调度
# 设置控制器进程为 SCHED_FIFO(需要 root 或 capabilities)
sudo chrt -f 80 ros2 run my_controller controller_node
步骤三:使用 TLSF 分配器
// 在实时路径上使用 TLSF(Two-Level Segregated Fit)分配器
// 避免 malloc/free 的不确定耗时
#include <tlsf_cpp/tlsf.hpp>
using TLSFAllocator = tlsf_heap_allocator<void>;
步骤四:使用 EventsExecutor
// EventsExecutor 避免了轮询等待集的开销
rclcpp::executors::EventsExecutor executor;
executor.add_node(node);
executor.spin();
步骤五:PREEMPT_RT 内核
标准 Linux 内核的调度延迟在 50-500 µs 范围内波动;PREEMPT_RT 补丁把这个范围压缩到 10-50 µs。对于 1 kHz 控制循环(1 ms 预算),标准内核的调度抖动可能消耗 10-50% 的周期预算;PREEMPT_RT 把这个比例压到 1-5%。
| 配置项 | 标准 Linux | PREEMPT_RT | 差异来源 |
|---|---|---|---|
| 最大调度延迟 | 50-500 µs | 10-50 µs | 中断线程化、优先级继承 |
| 页面错误延迟 | 0.5-5 ms | 被 mlockall 消除 | 内存锁定 |
| malloc 延迟 | 1-100 µs | TLSF 固定 ~1 µs | 分配器选择 |
| Executor 唤醒 | 轮询周期 | 事件驱动 | EventsExecutor |
⚠️ 实时配置陷阱¶
⚠️ 工程陷阱:只配置了 PREEMPT_RT 但没有锁内存
错误做法:安装了 PREEMPT_RT 内核,但没有调用
mlockall()。现象/后果:控制周期在运行几分钟后偶发出现 5-10 ms 的尖峰——因为新分配的内存页第一次被访问时触发了页面错误。
根本原因:PREEMPT_RT 让调度更确定,但不影响内存管理。页面错误仍然需要内核分配物理页并清零——这个过程可能需要毫秒级时间。
正确做法:在进入实时循环前调用
mlockall(MCL_CURRENT | MCL_FUTURE),并预分配所有可能需要的内存(预热 vector、预创建 buffer)。⚠️ 思维陷阱:认为 EventsExecutor 总是比 SingleThreadedExecutor 好
新手想法:"EventsExecutor 是事件驱动的,肯定比轮询好。"
实际上:EventsExecutor 在 Kilted 之前的某些发行版中存在稳定性问题。在 Humble 上它是实验性的,某些 QoS + 服务组合可能触发死锁。在 Jazzy/Kilted 上已经显著改善,但仍需在自己的系统上充分测试后再用于生产。
正确做法:在生产系统中切换 Executor 前,跑 24 小时以上的稳定性测试,监控 p99 延迟和内存增长。
练习¶
- [设计题] 为一个 1 kHz 控制循环设计完整的实时配置方案,包括内核选择、内存锁定、调度策略、分配器和 Executor。说明每一项配置解决什么延迟来源。
- [分析题] 假设一个系统的控制周期 p99 是 0.8 ms、max 是 12 ms。分析 max 尖峰可能来自哪些来源(提示:页面错误、日志、DDS 重传、系统级中断),设计排查顺序。
- [实验题] 写一个最小 ROS2 节点,设置
/scan的 Deadline QoS 为 200 ms。当激光雷达停止发布时,观察 deadline missed 回调是否触发。
19. GitHub 项目与官方文档汇总 ⭐¶
设计与规范¶
- https://design.ros2.org/ — 所有设计文档
- https://design.ros2.org/articles/why_ros2.html — 为什么需要 ROS2
- https://design.ros2.org/articles/ros_on_dds.html — DDS 选择理由
- https://design.ros2.org/articles/ros_middleware_interface.html — RMW 接口
- https://design.ros2.org/articles/changes.html — ROS1→ROS2 变更清单
- https://design.ros2.org/articles/qos.html — QoS 设计
- https://design.ros2.org/articles/actions.html — Actions 设计
- https://design.ros2.org/articles/node_lifecycle.html — Lifecycle 节点
- https://design.ros2.org/articles/roslaunch.html — Launch 系统
- https://www.ros.org/reps/rep-2000.html — 发行版与目标平台
核心基础设施¶
- https://github.com/ros2/ros2 — 元仓库
- https://github.com/ros2/rclcpp — C++ 客户端库
- https://github.com/ros2/rclpy — Python 客户端库
- https://github.com/ros2/rcl — C 通用客户端库
- https://github.com/ros2/rmw — RMW 接口
- https://github.com/ros2/launch — Launch 框架
- https://github.com/ros2/launch_ros — ROS Launch 扩展
- https://github.com/colcon — 构建工具
- https://github.com/ament — 构建系统
中间件¶
- https://github.com/eProsima/Fast-DDS — 默认 DDS
- https://github.com/ros2/rmw_fastrtps — Fast DDS RMW
- https://github.com/eclipse-cyclonedds/cyclonedds — Cyclone DDS
- https://github.com/ros2/rmw_cyclonedds — Cyclone DDS RMW
- https://github.com/ros2/rmw_connextdds — RTI Connext RMW
- https://github.com/ros2/rmw_zenoh — Zenoh RMW(非 DDS)
- https://github.com/eclipse-zenoh/zenoh — Zenoh 项目
- https://github.com/eclipse-iceoryx/iceoryx — 零拷贝共享内存
桥接与迁移¶
- https://github.com/ros2/ros1_bridge — ROS1↔ROS2 桥接
- https://github.com/fkie/multimaster_fkie — ROS1 多 master
- https://github.com/eclipse-zenoh/zenoh-plugin-ros2dds — Zenoh↔ROS2 桥接
- https://github.com/eclipse-zenoh/zenoh-plugin-ros1 — Zenoh↔ROS1 桥接
- https://github.com/foxglove/ros-foxglove-bridge — WebSocket 桥接
- https://github.com/RobotWebTools/rosbridge_suite — JSON WebSocket
导航、操作、控制¶
- https://github.com/ros-navigation/navigation2 — Nav2
- https://github.com/moveit/moveit2 — MoveIt 2
- https://github.com/ros-controls/ros2_control — ros2_control 框架
- https://github.com/ros-controls/gz_ros2_control — Gazebo 集成
SLAM¶
- https://github.com/SteveMacenski/slam_toolbox — slam_toolbox
- https://github.com/ros2/cartographer_ros — Cartographer ROS2
- https://github.com/introlab/rtabmap_ros — RTAB-Map
- 社区移植的 ORB-SLAM3 / LIO-SAM / FAST-LIO2 / Point-LIO
仿真与可视化¶
- https://github.com/gazebosim — 现代 Gazebo
- https://github.com/gazebosim/ros_gz — ROS↔Gazebo 桥接
- https://github.com/ros2/rviz — RViz2
- https://github.com/foxglove/studio — Foxglove Studio
- https://github.com/isaac-sim/IsaacLab — Isaac Lab
嵌入式与安全¶
- https://github.com/micro-ROS/micro_ros_setup — micro-ROS
- https://github.com/ros2/sros2 — SROS2 安全
自动驾驶与车队¶
- https://github.com/autowarefoundation/autoware — Autoware
- https://github.com/open-rmf/rmf — 开放式多机器人车队管理
- https://github.com/NVIDIA-ISAAC-ROS — NVIDIA Isaac ROS
Bag 录制与基准测试¶
- https://github.com/ros2/rosbag2 — rosbag2
- https://github.com/foxglove/mcap — MCAP 格式
- https://github.com/ros2/performance_test — 性能测试
- https://github.com/irobot-ros/ros2-performance — iRobot 性能框架
- https://github.com/irobot-ros/ros2-benchmark-container — 基准测试容器
迁移指南¶
- https://docs.ros.org/en/humble/How-To-Guides/Migrating-from-ROS1.html
- https://docs.ros.org/en/jazzy/How-To-Guides/Migrating-from-ROS1.html
- https://docs.ros.org/en/rolling/The-ROS2-Project/Contributing/Migration-Guide.html
本章小结¶
| 知识点 | 核心要点 | 工程意义 |
|---|---|---|
| ROS1 假设失效 | 中心 master、单一传输、无安全、进程间拷贝、节点不可控 | 理解 ROS2 每个新机制的设计动机 |
| DDS 选型 | 分布式发现、QoS、CDR 序列化、安全、实时 | ROS2 通信层的基石,决定系统行为 |
| RMW 抽象 | 隔离 ROS2 与具体中间件供应商 | 可切换 Fast DDS / Cyclone / Zenoh |
| QoS 兼容性 | 请求不能强于提供,不匹配时沉默失败 | 90% 的"话题存在但没数据"问题的根源 |
| QoS 高级策略 | Deadline/Liveliness/Lifespan 检测超时和节点存活 | 实时控制系统的必备健康监控 |
| Zenoh | 非 DDS,面向弱网/WAN/mesh,gossip 发现 | 跨网络可达性优于 DDS 多播 |
| SROS2 安全 | X.509 认证 + 权限文件 + AES-GCM 加密 | 生产部署的安全基础设施 |
| micro-ROS | MCU 客户端-代理模式,DDS-XRCE 协议 | 让 MCU 以 ROS2 格式参与图,但不替代硬件安全 |
| Composition | 多节点共享进程,IPC 零拷贝 | 大消息(>1 MB)管线的必需优化 |
| Executor 模型 | 四种 Executor 对应不同回调调度需求 | 回调依赖关系决定选择,不是"越多线程越好" |
| Lifecycle 节点 | 显式状态机,可控启停,bond 心跳 | 过渡阶段比稳态更危险,需要每步可验证 |
| 实时配置 | PREEMPT_RT + mlockall + TLSF + EventsExecutor | 控制循环的确定性不只是"代码快",是"耗时有上界" |
| 版本生态 | LTS 5 年支持,非 LTS 18 个月,发行版绑定 Ubuntu | 产品主线用 LTS,实验分支用非 LTS |
| DDS 性能基准 | FastDDS/CycloneDDS/Zenoh 在不同场景各有优劣 | 选型基于部署网络假设,不是基准排名 |
| 生态定位 | ROS2 vs ROS1 vs LCM vs DDS-only 各有适用场景 | 理解生态边界,避免在不适合的场景强行使用 |
| 迁移策略 | 逐包迁移,先盘点再适配再验证 | 不能等 EOL 前一个月才开始 |
| 七个核心权衡 | 简单/灵活、标准/自研、性能/抽象、安全/可用等 | 理解权衡结构比记住 API 更有长期价值 |
本质洞察:ROS2 的复杂度不是"新框架变复杂",而是 ROS1 把通信语义、生命周期、安全和部署边界隐藏在默认值里;ROS2 把这些边界拿到接口层,让工程师必须显式选择。这像从"脚本语言"转向"强类型语言"——前期多写几行声明,换来的是运行时行为可预测、可调试、可部署。
回顾本章的知识树结构。读完全章后,你脑中应该形成这样一棵树:
根:机器人系统需要什么样的中间件?
│
├── ROS1 的五个隐式假设(为什么失效)
│ ├── 中心发现 → DDS 分布式发现
│ ├── 单一传输 → QoS 策略体系
│ ├── 进程间拷贝 → Composition + IPC
│ ├── 隐式生命周期 → Lifecycle 节点
│ └── 无安全 → DDS-Security / SROS2
│
├── DDS 层(通信基石)
│ ├── SPDP/SEDP 发现机制
│ ├── CDR 序列化
│ ├── QoS 请求/提供匹配
│ ├── Deadline/Liveliness/Lifespan
│ └── DDS-Security 三层模型
│
├── RMW 层(供应商隔离)
│ ├── Fast DDS(默认,功能全面)
│ ├── CycloneDDS(简单,低延迟)
│ ├── Zenoh(跨网络,弱网)
│ └── Connext(商业,安全认证)
│
├── 节点模型
│ ├── Composition(零拷贝)
│ ├── Executor(回调调度)
│ └── Lifecycle(状态管理)
│
├── 嵌入式边界
│ ├── micro-ROS(MCU 客户端-代理)
│ └── 安全关键功能本地闭环
│
├── 实时配置
│ ├── PREEMPT_RT + mlockall
│ ├── TLSF 分配器
│ └── EventsExecutor
│
└── 版本与迁移
├── LTS vs 非 LTS 选择
├── 发行版-Ubuntu 绑定
└── ROS1→ROS2 迁移路径
这棵树的根是"机器人系统需要什么样的中间件"——每个分支都在回答这个问题的某个维度。理解树的结构比记住叶子节点的细节更重要——因为新的 ROS2 特性(比如未来可能出现的新 RMW、新 QoS 策略、新 Executor 类型)都可以挂在已有的分支上,而不需要重建整棵树。
累积项目:本章新增模块¶
ROS2 工程化实践项目:从零搭建一个多传感器 SLAM + 导航机器人
本章新增模块:工程决策文档
为你的项目创建一个 docs/engineering_decisions.md 文件,记录以下决策及其理由:
- 选择的 ROS2 发行版及其 EOL 时间
- 选择的 RMW 及其理由(网络环境、消息大小、部署场景)
- 关键话题的 QoS 策略表(至少覆盖
/scan、/map、/cmd_vel、/tf_static、/imu、/odom) - 是否需要 Zenoh 及其使用范围
- 安全策略:是否启用 SROS2,哪些话题需要加密
- 实时需求:是否需要 PREEMPT_RT,控制循环目标频率
- 嵌入式边界:MCU 和上位机的职责划分
这个文档将在后续章节中持续更新,最终成为项目的工程规范参考。
工程决策文档模板:
# 工程决策文档
## 1. 发行版选择
- **选择**:ROS2 Jazzy (LTS, EOL 2029-05)
- **理由**:Ubuntu 24.04 兼容、Nav2/MoveIt2/ros2_control 均有稳定二进制包
- **备选方案**:Lyrical(等待 2026 Q3 生态成熟后迁移)
## 2. RMW 选择
- **机器人本体**:rmw_cyclonedds_cpp(配置简单、延迟低)
- **跨网络监控**:rmw_zenoh_cpp(WiFi 覆盖不稳定区域)
- **桥接方案**:zenoh-plugin-ros2dds 连接两个 RMW 域
## 3. QoS 策略表
| 话题 | Reliability | Durability | History | Depth | Deadline |
|------|------------|------------|---------|-------|----------|
| /scan | BestEffort | Volatile | KeepLast | 5 | 200ms |
| /imu | BestEffort | Volatile | KeepLast | 10 | 50ms |
| /map | Reliable | TransientLocal | KeepLast | 1 | - |
| /cmd_vel | BestEffort | Volatile | KeepLast | 1 | - |
| /tf_static | Reliable | TransientLocal | KeepAll | - | - |
| /odom | BestEffort | Volatile | KeepLast | 10 | 100ms |
## 4. 安全策略
- 实验室阶段:不启用 SROS2
- 集成测试阶段:Permissive 模式,识别权限需求
- 部署阶段:Enforce 模式,/cmd_vel 加密
## 5. 实时需求
- 控制循环:500 Hz(ros2_control)
- 内核:PREEMPT_RT 6.x
- 分配器:TLSF(控制路径)
- Executor:EventsExecutor(Jazzy 稳定版)
## 6. 嵌入式分层
- MCU (STM32H7):电机驱动、编码器采样、急停保护
- 上位机 (Jetson Orin):ROS2、SLAM、RL 推理
- 通信:CAN-FD @ 2 Mbps
这个模板在后续章节中会不断完善——SLAM导航与仿真生态 会补充仿真器选择和 SLAM 算法选型,硬件集成与RL部署 会补充硬件接口设计和 RL 部署方案,CLI调试与性能工具 会补充调试工具链和数据管理策略。
工程决策文档的价值不在于"写了一份漂亮的文档",而在于**迫使你在动手之前把关键决策显式化**。很多项目在启动时不做这些决策,等到部署阶段才发现 RMW 不兼容、QoS 策略混乱、安全模型缺失——此时改动成本远高于一开始就做好规划。
跨章综合练习¶
-
[软件工程/ROS2高级集成 + 设计哲学与架构演进] 回顾 软件工程/ROS2高级集成 中的 QoS 内容。设计一个多传感器 SLAM 系统(激光雷达 + IMU + 相机 + 地图发布),为每个话题选择 QoS 策略。然后从 设计哲学与架构演进 的 RMW 选型视角,分析如果部署在 WiFi 环境中(多播受限),应选择哪个 RMW 以及如何配置发现机制。
-
[软件工程/ROS2高级集成 + 设计哲学与架构演进 + 构建系统与机器人建模] 假设你有一个 ROS1 的激光 SLAM 项目(使用 gmapping + move_base + rosbridge),需要迁移到 ROS2。列出该项目依赖的 ROS1 默认假设(master、TCPROS、参数服务器、tf、bag),逐项说明迁移到 ROS2 时需要重新设计的内容,并估算总工作量。
-
[设计哲学与架构演进 + 网络基础] 设计一个仓库中 20 台移动机器人的发现方案。比较"单 master 桥接""DDS 多播""Fast DDS Discovery Server""Zenoh 路由"四种方案,列出每种方案的网络假设、单点故障风险、带宽开销和配置复杂度。
🔧 故障排查手册¶
| 症状 | 可能原因 | 排查步骤 | 相关章节 |
|---|---|---|---|
ros2 topic list 为空但节点已启动 |
ROS_DOMAIN_ID 不一致 | 1. 两端 echo $ROS_DOMAIN_ID 2. 确认在 0-101 范围内 3. 统一设置 |
本章 §13 |
话题存在但 ros2 topic echo 无输出 |
QoS 不匹配(Reliable 订阅 BestEffort 发布) | 1. ros2 topic info /topic --verbose 查看两端 QoS 2. 调整订阅方为 BestEffort |
本章 §13, CLI调试与性能工具 |
| Docker 容器中节点互相看不到 | 多播被 Docker 网络隔离 | 1. 使用 --network host 2. 或配置 Fast DDS Discovery Server 3. 或使用 Zenoh |
本章 §14 |
| 多机器人 TF 命名冲突 | frame ID 未加命名空间前缀 | 1. launch 中使用 PushRosNamespace 2. robot_state_publisher 设置 frame_prefix |
本章 §8, CLI调试与性能工具 |
| 切换 RMW 后部分节点不可见 | 同一系统中混用不同 RMW | 1. echo $RMW_IMPLEMENTATION 确认所有终端一致 2. 统一 RMW 或显式桥接 |
本章 §13 |
| Zenoh RMW 节点与 DDS 节点不互通 | Zenoh 与 DDS 是不同协议栈 | 1. 统一 RMW 2. 或使用 zenoh-plugin-ros2dds 桥接 |
本章 §14 |
| 新订阅者收不到地图 | 发布方未设置 TransientLocal durability | 1. 确认发布方和订阅方都使用 TRANSIENT_LOCAL 2. 检查 history depth |
本章 §3 |
| 节点发现慢(>10 s 才出现) | 发现协议被网络设备拦截 | 1. ros2 multicast send/receive 测试 2. 检查交换机 IGMP snooping 3. 尝试 Discovery Server |
本章 §13 |
| Lifecycle 节点 configure 失败 | 依赖的参数或设备未就绪 | 1. ros2 lifecycle get /node 确认状态 2. 查看节点日志中的 ERROR 3. 检查参数文件路径 |
本章 §4 |
| SROS2 启用后通信中断 | 权限文件遗漏话题或证书过期 | 1. 临时设 ROS_SECURITY_STRATEGY=Permissive 2. 查看安全日志 3. 检查权限 XML |
本章 §4 |
| 控制周期偶发超时(非算法) | 页面错误、日志阻塞或 DDS 重传 | 1. 确认 mlockall 2. 关闭高频日志 3. 用 ros2 trace 定位尖峰回调 |
本章 §18 |
ros2 daemon 状态异常 |
daemon 缓存过期或崩溃 | 1. ros2 daemon stop && ros2 daemon start 2. 清除 ~/.ros/ 下的缓存 |
CLI调试与性能工具 |
故障排查的通用原则:
- 先检查基础设施,再检查算法。90% 的"SLAM 不工作"问题来自 QoS、TF 或时间源,不是 SLAM 算法参数。
- 每次只改一个变量。同时改 QoS、RMW 和参数文件,即使问题消失了也不知道是哪个起作用。
- 保留证据。用
ros2 topic info --verbose和ros2 doctor --report的输出记录问题状态,方便后续复现和分析。 - 区分"消失"和"修复"。重启后问题消失不等于修复——记录重启前的状态,构建最小复现步骤。
延伸阅读¶
| 资源 | 难度 | 说明 |
|---|---|---|
| design.ros2.org/articles/why_ros2.html | ⭐ | ROS2 设计动机原文,必读 |
| design.ros2.org/articles/ros_on_dds.html | ⭐⭐ | DDS 选型决策的完整论证 |
| REP-2000: ros.org/reps/rep-2000.html | ⭐ | 发行版与目标平台的权威规范 |
| REP-2002: ros.org/reps/rep-2002.html | ⭐⭐ | Lifecycle 节点 REP 规范 |
| OMG DDS-Security 标准 | ⭐⭐⭐ | SROS2 底层安全标准 |
| arXiv:2309.07496 — Zenoh 边缘到云基准 | ⭐⭐⭐ | Zenoh 在嵌入式网状网络中的性能数据 |
| arXiv:2407.03091 — Planetary mesh 通信基准 | ⭐⭐⭐ | Zenoh vs DDS 在行星探索网络中的对比 |
| arXiv:2303.09419 — Zenoh vs DDS vs MQTT vs Kafka | ⭐⭐⭐ | 四种中间件在 IoT/机器人场景下的系统对比 |
| OSRF TSC RMW 报告: osrf.github.io/TSC-RMW-Reports | ⭐⭐ | 年度 RMW 评估,含基准和未解决问题 |
| 《Robot Operating System 2: Design, Architecture, and Uses In The Wild》(Macenski et al., 2022) | ⭐⭐⭐ | Nav2 核心作者写的 ROS2 架构综述论文 |
| micro-ROS 官方文档 | ⭐⭐ | MCU 上运行 ROS2 客户端的完整指南 |
| LCM 项目 | ⭐⭐ | MIT 的轻量通信库,理解 ROS2 的替代方案 |
| performance_test 工具 | ⭐⭐ | ROS2 官方通信性能基准测试工具 |
| Apex.AI — Real-time ROS2 | ⭐⭐⭐ | 实时 ROS2 参考系统实现 |
| ros2_tracing 论文 (Bedard et al., 2022) | ⭐⭐⭐ | LTTng 集成 ROS2 的架构与性能分析方法论 |
阅读建议:
- 如果只读一篇,读 why_ros2.html——理解设计动机比学 API 更重要。
- 如果要做 RMW 选型决策,读 OSRF TSC RMW 报告中最新年度的基准数据。
- 如果要理解 Zenoh 的技术细节,读 arXiv:2303.09419 的中间件对比论文。
- 如果要做实时系统,先读 Apex.AI 的参考系统实现,再结合本章 §18 的配置指南。
- 如果要在 MCU 上集成 ROS2,先读 micro-ROS 官方文档的 Overview,再看 ESP32/STM32 示例。
📎 ROS2运行时六大特性(Lifecycle Node、Component Composition、QoS策略、Actions、Executors、Parameters)的生产级代码与SLAM实战,详见**软件工程/ROS2高级集成**。