“把节点合到一个进程里,就省掉序列化和网络传输了。”这句话我第一次听到时也觉得合理,直到看到有人在同一个容器里跑图像链路,延迟一点没降。
原因不复杂:合并进程只是部署方式变了,数据怎么走是另一回事。同进程、开了进程内通信、消息所有权能让出去,这三个条件缺任何一个,收益都会打折。
所以我现在的习惯是先确认数据落在哪里,再谈进程边界。
进程内通信是节点选项,不是进程属性
最常见的误解是:节点在同一个容器里,拷贝就没了。实际上它是每个节点创建时显式开的一个选项:
1 | auto options = rclcpp::NodeOptions() |
用组合节点加载时也一样要传:
1 | ros2 component load /ComponentManager my_package \ |
命令行写了 true 不代表一定生效。加载完我会回读一遍,确认节点确实在目标容器里:
1 | ros2 component list |
如果节点出现在 node list 里,但它并不属于你预期的容器进程,那合并这件事压根没发生。
省掉的拷贝来自所有权,不是来自进程数
进程内通信能省的那次拷贝,靠的是发布者把消息移动给订阅者,而不是复制一份再交出去。要做到这点通常要满足两条:只有一个订阅者,并且发布之后不再依赖这个对象。
如果发布者还要继续用这份数据,或者有多个订阅者各自需要一份,那就只能复制。这时候即便在同一个进程里,数据照样被复制多次,省下的只是序列化和跨进程传输。
这也是“同进程”和“零拷贝”老被混着说的原因。前者是部署事实,后者是缓冲区所有权的结果。判断一条链路到底少没少拷贝,看的是大缓冲区地址在两侧是否一致,不是数进程个数。
一个容器不是性能补丁
组合节点在工程上的收益其实有三个:进程更少、启动开销更小,共享同一份大只读资源,以及在合适的大消息链路上提供所有权移交的可能。没有哪一条等同于“控制更实时”。
把实时控制环塞进共享容器尤其要小心。容器里所有节点共享线程池和调度资源,一个节点里的阻塞调用或者大量分配,会影响同进程的其他人。反过来,如果节点本来就需要进程隔离、独立资源限制或者故障隔离,合并反而把边界拆了。
我自己的判断顺序是先测瓶颈在哪。瓶颈是写盘、前处理还是队列积压,合并进程都不解决。只有当瓶颈确实出在中件间对大图像做序列化和反序列化,而且同进程前提成立时,组合节点才值得试。
测的时候把变量固定住
想在两次运行之间比出差异,除了通信方式,其它都得一样——同一份数据、同一个 QoS、同一台机器,还得记录 CPU 占用。不然测到的差异可能来自缓存命中、CPU 频率变化或者后台任务。
1 | ros2 run my_package latency_probe --ros-args -p intra:=false |
顺便留意 QoS。进程内通信一样要求两端 QoS 兼容,不兼容时连接不建立,此时“延迟没降”的原因是根本没有数据流,跟进程内通信没关系。
什么情况值得合
值得合的情况大致是:多个节点处理同一条大消息,链路顺序稳定,其中一个是明确的中间环节,而且没有独立隔离的需求。
不值得合的情况:需要独立重启和故障隔离,需要单独限制 CPU 或内存,或者消息很小、拷贝本来就不是瓶颈。这时候合并只是把复杂度从通信搬到了共享资源管理上,得不偿失。
参考资料
证据边界:本文不提供任何平台上的实测延迟。到底省没省拷贝、省了多少,得在目标机器上针对具体消息类型测;同样的配置换一种消息大小或者换硬件,结论可能完全反过来。