故事是这样的。
去年我给一个矿卡项目做 ROS2 架构评审。他们的感知模块跑了12个节点,每个节点一个独立进程。一台工控机上 ps aux 一刷,一整屏的ros2 run。12 个进程,12 份 DDS 参与者的开销,12 条独立的网络路径。激光雷达点云从 driver 节点走到 obstacle_detection 节点,几十兆的数据先序列化,走一遍loopback 网卡,再反序列化。每一步都在烧CPU。
我问他们为什么不把感知链路的节点放进同一个进程。对方说「ROS2 不就是一个节点一个进程吗?」
不是的。
如果你正在用 ROS2 做实际项目,这篇文章会帮你搞清楚三件事。Component 到底是什么,进程内通信为什么快,以及什么情况下你宁愿不要这个快。


先聊聊历史。
ROS1有一个很拧巴的设计。你写代码的时候,要么用 Node API,编译成独立可执行文件,一个进程跑一个节点;要么用 Nodelet API,编译成 .so 共享库,丢给一个叫 nodelet manager 的容器进程去加载。功能上它们能干差不多的事,但 API 不一样。你写了一个节点,想把它变成 nodelet?重写吧。注册机制、初始化流程、线程模型,全得改。
这个设计让很多团队在项目早期选了 Node 方式,因为简单。等项目跑起来发现性能扛不住了,想切 nodelet,一看代码量,算了,加机器吧。
ROS2 的设计者在做架构重构的时候,显然被这个问题戳中了。他们在2014年那篇著名的设计文章里明确提出,ROS2 的节点API必须统一。不管是独立进程还是共享进程,你写代码的方式是一样的。进程布局应该在部署时决定,而不是在写代码时决定。
这个想法后来变成了 ROS2 的 Component 机制。


一个 ROS2 Component,说穿了就是一个继承自rclcpp::Node 的 C++ 类,编译成共享库(.so 文件),没有 main 函数。它不决定自己怎么跑,跑在哪里,这些是部署时的事。
你打开一个典型的 Component 源码,末尾会看到类似这样的注册宏。

这个宏的作用是在共享库里埋一个入口,告诉 ROS2 的类加载器「这里有一个可以实例化的节点类」。就像插件的注册表。
编译的时候,CMakeLists.txt 里也不会像普通节点那样用 add_executable,而是这样写。

注意这里用的是 register_nodes,不是 register_node。多了一个 s,差别在于前者只注册为可组合组件,不生成独立可执行文件。如果你想两全其美,既能在容器里跑,也能单独 ros2 run,那就用register_node,它会额外给你生成一个 standalone 的可执行文件。
ROS2 提供了至少三种把 Component 装进同一个进程的方式。每一种对应不同的部署场景。
编译时组合
你在代码里 new 好几个 Node 对象,把它们加到同一个executor里,然后一起spin。这是最直接的方式,所有节点在编译时就确定好了。
运行时组合
你先启动一个叫 component_container的进程(它本身就是一个特殊的可执行文件),然后通过命令行动态加载 .so 文件。

Talker 和 Listener 现在跑在同一个component_container 进程里。你可以用ros2 component list 看到它们都在那个容器里。
启动文件组合
在 launch 文件里声明 ComposableNodeContainer 和 ComposableNode,ros2 launch 一次性把所有 Component 装进容器。这是生产环境最常用的方式,因为版本管理、参数注入、重映射都可以写进 launch 文件。
这三种方式下,你的 Component 源码不用改动一行。进程布局完全由部署侧决定。
把节点塞进同一个进程的最大收益,不是省了几个进程号,而是通信路径彻底变了。

ROS2 默认的节点间通信走的是 DDS 中间件。一个节点 publish 一条消息,流程是这样的。消息对象序列化成字节流,交给 RMW 层,RMW 层交给 DDS 实现(比如 Fast DDS 或 Cyclone DDS),DDS 通过 UDP 或共享内存发送,接收端 DDS 收包,RMW 层收字节流,反序列化成消息对象。
每一步都要CPU时间。消息越大,代价越高。一个3MB的点云,序列化和反序列化各吃掉几十微秒到上百微秒。循环发送的激光雷达数据,这个开销是持续的。
但如果是同一个进程内的两个节点,ROS2 可以走 Intra-Process Communication。信号路径变成 publisher 直接把消息指针交给 subscriber,没有序列化,没有DDS栈,没有网络协议层。

ROS2 官方提供了一个很直观的 demo 来验证这件事情。intra_process_demo 包里有一个 two_node_pipeline,一个 Producer 节点和一个 Consumer 节点跑在同一个进程里。Producer 每次发布时打印消息的内存地址,Consumer 收到时也打印地址。如图4所示,Published 和 Received 的地址是一样的。Consumer 收到的就是 Producer 发出去的那块内存,没有拷贝。
这个能力的前提是使用 std::unique_ptr来 publish。ROS2 看到 unique_ptr,就知道所有权可以转移,消息对象不需要深拷贝。如果你用 const shared_ptr 或者 const reference,那就还是会走一次拷贝。
不过这里有个坑。如果同一个 topic 有两个 intra-process subscriber,消息只能零拷贝给其中一个。另一个会收到一份拷贝。官方文档也明确说了这一点,在 image_pipeline_with_two_image_view 这个 demo 里,两个 image_view 节点订阅同一个 watermark 节点的输出,其中一个拿到了原始指针,另一个拿到了拷贝。
这个限制是合理的,unique_ptr 的语义决定了所有权只能给一个人。


读到这里你可能会想,那我把所有节点都塞进一个进程不就行了。
别急。
进程隔离不是没用的东西。一个节点 crash 了,如果它在独立进程里,只会死它一个。如果它在一个装了12个节点的component_container 里……12 个一起死。
调试也是一个问题。独立进程你可以单独 attach gdb,单独看日志,单独重启。容器里的节点出了问题,你得先搞清楚是哪个 Component 崩了,然后再去查共享库的符号表。
还有线程模型。component_container 默认用的是SingleThreadedExecutor,所有 Component 的回调在同一个线程里排队执行。如果你的某个 Component 里有一个耗时的回调(比如做点云配准),它会阻塞同一个容器里所有其他 Component。你可以切 MultiThreadedExecutor,但多线程带来的竞争和死锁风险又得你自己兜着。
ROS2 给了一个折中方案,isolated模式。启动容器时加上这个参数。

这样每个 Component 都有自己独立的 executor 和线程,互不干扰。代价是线程数量变多,上下文切换开销增加。
所以这事没有标准答案。一般我自己的判断原则是这样的。
把高吞吐、低延迟要求的数据处理链路放进一个容器。比如激光雷达驱动、点云滤波、障碍物检测,这三者之间有大量数据流动,放在一起收益最大。
把生命周期独立、功能边界清晰的模块放在独立进程。比如诊断节点、日志记录节点、UI桥接节点,它们崩了不应该影响主链路。
把需要隔离风险的模块放在独立进程。比如和硬件直接打交道的驱动、第三方的闭源组件。
这些决策可以通过 launch 文件集中管理。你在 launch 文件里声明哪些节点进哪个容器,部署时一键拉起。切换方案只需要改 launch 文件,不用动源码。我觉得这是 ROS2 组件模型最漂亮的地方。
ROS2 的 Component 设计,说到底是在回答一个工程问题。代码组织方式能不能不绑定进程拓扑?
答案是可以的。你写一个继承了 rclcpp::Node 的类,编译成共享库,注册一下,然后想让它独立跑就 ros2 run,想让它和别人挤一个进程就丢进 component_container。代码不用改,CMakeLists.txt 加一行注册的事。
这比 ROS1 的 node vs nodelet 分裂方案优雅太多了。
如果你的项目现在还是一堆 ros2 run,每个节点一个进程,建议花半天时间把数据密集的链路改成 Component 容器,然后跑一遍性能对比。你大概率会看到 CPU 占用和端到端延迟的明显下降。
本文也会同步整理在公众号「智驾芯视野」。如果你对 ROS2 在自动驾驶和机器人领域的工程落地感兴趣,可以在公众号里查看系列文章。
[1] ROS2 Composition Tutorial https://docs.ros.org/en/rolling/Tutorials/Intermediate/Composition.html
[2] ROS2 About Composition https://docs.ros.org/en/rolling/Concepts/Intermediate/About-Composition.html
[3] ROS2 Intra-Process Communication Demo https://docs.ros.org/en/rolling/Tutorials/Demos/Intra-Process-Communication.html
[4] ROS2 Composition Demos (GitHub) https://github.com/ros2/demos/tree/rolling/composition
[5] rclcpp_components Package https://github.com/ros2/rclcpp/tree/rolling/rclcpp_components
[6] ROS2 Launch Composable Nodes Guide https://docs.ros.org/en/rolling/How-To-Guides/Launching-composable-nodes.html
京公网安备11010502056287号