资讯动态

从ROS到Cyber RT:自动驾驶实时通信框架的核心设计与工程实践

发布时间:2026/10/4 6:36:19 来源:尧图企业网站定制
1. 先搞清楚一件事为什么 Apollo 不用 ROS——Cyber RT 的设计动机我最早接触百度 Apollo 的时候第一反应跟大部分人一样为什么一个这么大的自动驾驶项目放着现成的 ROS 不用非要自己搞一套通信框架毕竟 ROS 在机器人领域已经成了事实标准教程多、生态全、踩坑经验到处都是。尤其是我这种从 ROS 入门、在 ROS 上写过几年机器人应用的工程师看到 Apollo 的代码结构时确实需要一点时间适应。这个问题的答案其实要回到自动驾驶场景本身的特殊性来看。ROS 1 的通信模型基于 TCPROS/UDPROS节点之间通过 roscore 做名称服务消息走 TCP socket 或者 UDP。单机小规模没问题但到了自动驾驶这种场景——多路摄像头数据、激光雷达点云、毫米波雷达目标、高精地图、定位结果、规划轨迹、控制指令全部在毫秒级同时流动——ROS 1 的吞吐量和确定性调度就成了明显的瓶颈。我印象很深的是以前在 ROS 1 上调一个 64 线激光雷达点云话题发布频率 10Hz、单帧点云好几 MB几个订阅节点同时收的时候CPU 占用和网络延迟立刻上来了。这种压力在 Apollo 里只会更夸张因为它要同时处理十几路传感器输入。ROS 2 后来引入了 DDS 来解决这些问题但 Apollo 立项和早期版本起步的时候ROS 2 还不够成熟DDS 在嵌入式设备和高频通信上的裁剪适配也远没有今天这么顺手。与其等一个不确定的外部技术成熟不如基于自己的场景需求做一个专门为自动驾驶实时性而生的通信中间件这就是 Cyber RT 的来历。它要解决的核心问题有三个高吞吐、确定性调度、低延迟。注意“确定性调度”这个词ROS 1 里你很难控制一个回调函数什么时候被执行、执行多久但 Cyber RT 通过协程和用户态调度器把计算任务组织成有优先级的执行单元让关键链路的延迟变得可控。所以在开始读 Cyber RT 源码之前我建议你先建立这样一个认知框架Cyber RT 不是 ROS 的复制品也不是简单的“高性能版 ROS”它是一套以协程调度为核心的实时任务框架通信只是它的一个能力而不是全部。理解了这一点后面看 Component、Scheduler、协程池这些概念就不会觉得散。2. Cyber RT 的基本概念逐个过一遍建议配合源码一起看2.1 Component你的算法模块都跑在它里面在 ROS 里你创建一个 Node然后在 Node 里订阅话题、发布话题、写回调基本上想怎么组织代码都行。Cyber RT 则把算法模块抽象成一个更固定的单元——Component。它不是一个随意写逻辑的类而是一个有约定模板的基类你必须继承它、实现两个关键方法Init 和 Proc然后通过注册宏把组件类暴露给框架。这里我强烈建议你把 Cyber 源码里的cyber/examples/common_component_example/目录打开对着看。一个典型的 Component 类长这样class CommonComponentSample : public ComponentChatter, TimerComponent { public: bool Init() override; bool Proc(const std::shared_ptrChatter msg0, const std::shared_ptrTimerComponent msg1) override; }; CYBER_REGISTER_COMPONENT(CommonComponentSample)Init 在组件启动时被调用一次适合做初始化工作比如加载参数、分配内存、打开文件、连接外部服务。Proc 在每次收到消息时被调用消息类型和数量由模板参数决定。上面这个例子绑定了两个消息类型也就是说只有当两个通道都有数据到达时Proc 才会被触发具体触发策略可以在配置文件里调。这种设计习惯上很像 ROS 里的message_filters::sync::ApproximateTimeSynchronizer但 Cyber RT 把“多通道同步触发”直接做成了框架能力你不用自己写同步逻辑只需要在配置里声明这个组件关心哪些消息通道。我自己用下来的感觉是多传感器融合这种场景相机激光雷达毫米波用 Component 写真的非常舒服代码结构天然就是“等待所有输入 - 做融合 - 输出结果”的模式。2.2 Channel、Writer 与 Reader数据是怎么流动的Channel 在概念上对标 ROS 的 Topic是一根命名的数据管道。Writer 对标 PublisherReader 对标 Subscriber。auto writer node-CreateWriterChatter(/apollo/test_chatter); auto reader node-CreateReaderChatter(/apollo/test_chatter, [](const std::shared_ptrChatter msg) { // 处理消息 });但是里头的传输方式和 ROS 1 有本质区别。ROS 1 默认走 TCP就算在同一台机器上的两个节点通信数据也要经过内核协议栈、socket 缓冲区序列化和反序列化也会吃 CPU。Cyber RT 做了三层递进优化同一进程内通过 intra-process 方式直接传对象指针不经过序列化跨进程但同机时走共享内存Shared Memory跨机时才走网络默认用的是 CycloneDDS 或者 FastDDS 这类实现。这三层对用户是透明的你只管创建 Writer/Reader底层自动选择最快的传输路径。这个设计直接带来的好处就是同一台机器上多模块通信的延迟能压到微秒级CPU 占用也比 ROS 1 低不少。我在实际测试里跑过同样的点云数据ROS 1 方式订阅一帧 10Hz 的 64 线点云CPU 占用大概多出 15% 到 20%Cyber RT 的 intra-process 和共享内存方式明显更轻。2.3 Service 与 Client同步请求/响应怎么搞自动驾驶里也有很多“我发一个请求你给我一个结果”的同步交互场景比如请求高精地图、请求路径规划结果、获取某个模块的状态快照。Cyber RT 用 Service 和 Client 来支持这种模式对标 ROS 1 的 Service。定义服务接口用 protomessage Request { string content 1; } message Response { bool success 1; string message 2; }服务端的创建方式auto service node-CreateServiceRequest, Response(/apollo/test_service, [](const std::shared_ptrRequest req, std::shared_ptrResponse res) { res-set_success(true); });客户端的请求方式支持同步和异步两种。同步调用适合那种“我就等这个结果等不到就报错”的场景异步调用适合“我发出去之后继续干别的事有结果了再回调”的场景。从工程习惯上讲我建议自动驾驶链路里的高频同步请求尽量改成 Channel 状态机因为同步阻塞会占用协程资源而 Cyber RT 的调度器是按协程管理的如果一个协程长期阻塞可能会拖累同优先级的其它任务。2.4 参数服务器与配置文件Coff、DAG 文件到底是什么Cyber RT 的可配置性和 ROS 的 launch 文件机制有些像但更强调配置与代码分离。一个组件的加载路径是这样的DAG 文件描述“启动哪些组件、每个组件跑在哪个节点上、组件绑定的参数文件Coff 文件是哪个”Launch 文件描述“要加载哪些 DAG 文件、每个进程跑几个节点”。一个典型的自定义 DAG 文件片段长这样module_config { module_library : /apollo/bazel-bin/cyber/examples/common_component_example/libcommon_component_example.so components { class_name : CommonComponentSample config { name : common_component_sample config_file_path : /apollo/cyber/examples/common_component_example/common_component_sample.pb.txt } } }对应的 Coff 文件里写的是组件的运行时参数和消息绑定关系message_channels: /apollo/test_chatter message_channels: /apollo/test_timer这种做法的好处是换算法参数、换消息通道甚至换整个算法实现只要类名和输入输出消息类型保持一致都不需要重新编译代码改配置重启就行。在 Apollo 这种多模块、多人协作的项目里这个特性非常关键——规划组的同学调整参数不需要等感知组的同学发新包自己拉代码改配置就能联调。3. 一张表看清楚Cyber RT 和 ROSROS1/ROS2到底差在哪3.1 通信模型和传输层对比很多初学者学 Cyber RT 时会犯一个错误把 Cyber RT 跟 ROS 里的一个库比如 roscpp做对比。其实 Cyber RT 的定位更接近 ROS 1 的 ros_comm roscpp 一个轻量调度器的整体它不是一个纯粹的通信库。我把几个关键维度整理成了对照表对比项ROS 1ROS 2Cyber RT底层通信TCPROS/UDPROSDDSFastDDS等自有传输层支持 intra-process、共享内存、网络名称服务roscore 中心化DDS 自动发现Node/Channel 在进程内注册无中心节点调度方式线程回调系统线程调线程回调 Executor用户态协程调度器支持优先级实时性保证无有 QoS 机制但受 DDS 实现影响强调度控制适合确定性执行开发范式Node 自由回调Node 自由回调Component 继承模式强约束语言C/PythonC/PythonC/Python部分工具核心 C可观测性rqt_graph、rosbagrqt_graph、rosbag2cyber_monitor、cyber_recorder、cyber_visualizer从表里能看出ROS 2 的 QoS 机制和 DDS 自动发现确实是巨大进步但 Cyber RT 在调度控制这个维度上走得更远。ROS 2 的 Executor 虽然也在改进但本质还是线程池 回调队列用户缺乏对任务时序的精确控制。Cyber RT 的 Scheduler 则是协程级别的一个 Compoent 实例对应一个协程协程可以被挂起、恢复、按优先级排队这种细粒度的控制对自动驾驶里的模组协同很有价值。3.2 设计哲学差异自由开发 vs 框架约束如果你以前一直写 ROS切换到 Cyber RT 后最强烈的感受是它管得太多了。ROS 给你的是一个自由工具箱——你想创建 10 个 Node、一个 Node 里订阅 100 个话题、回调里随便开线程都没人拦你。项目初期开发效率很高但后期维护、调优、性能分析就全靠个人自律团队里代码风格和模块组织方式容易五花八门。我见过很多 ROS 项目最终每个包的 node 组织方式都不一样新人接手特别痛苦。Cyber RT 走的是另一个方向我规定你按 Component 组织代码规定你继承基类、实现 Init 和 Proc规定你用 DAG 和 Coff 声明依赖。刚开始你会觉得束手束脚但一旦团队规模上来、模块之间需要频繁联调这种约束就会变成优势——每个人写出来的模块结构都是同构的排查问题、替换实现、横向对比性能都方便得多。以我自己的体会这两种设计哲学没有绝对优劣取决于项目阶段和团队规模。三五个人做科研原型ROS 的自由度更合适几十上百人做量产级自动驾驶系统Cyber RT 这种强约束明显更稳。不过如果你是个人学习者Cyber RT 的学习曲线会比 ROS 陡一些因为很多概念是绑定的不能只学通信、跳过调度那样你根本没法理解 Component 为什么是这种形态。3.3 从 ROS 迁移到 Cyber RT 的思维转变这里给准备从 ROS 转过来的朋友一个迁移映射表能省不少理解成本ROS 1 / ROS 2 概念Cyber RT 对应概念差异提示NodeNode但更轻量通常一个进程一个 NodeCyber 里 Node 主要负责创建 Writer/Reader/ServiceTopicChannelChannel 无 QoS 参数底层自动选择传输方式PublisherWriter一般建议在 Init 里提前创建避免运行时动态创建SubscriberReader回调函数里不要做耗时操作会占用协程ServiceService / Client同步请求会阻塞协程谨慎使用launch 文件Launch 文件 DAG 文件Launch 管理进程DAG 管理组件参数服务器参数文件 Proto 配置参数是强类型 proto 消息编译期检查这个表我建议你贴在自己电脑旁边练习 Cyber 时方便对照。但要注意概念映射只是入门工具真正写代码的时候Cyber RT 和 ROS 的底层运行机制差别很大光靠类比是会踩坑的后面我在实操章节会详细讲几个血泪教训。4. 实操跑通一个 Cyber RT 最小示例并顺手踩一遍坑4.1 环境准备和源码获取Cyber RT 可以脱离 Apollo 整个项目独立编译使用这个很多人不知道。我建议入门阶段不要一上来就拉 Apollo 全量代码——那是一个几 GB 级别的大仓库光 Bazel 编译就能让你怀疑人生。单独编译 Cyber RT依赖更少、构建更快能把注意力集中在理解通信机制上。环境方面官方推荐 Ubuntu 18.04 以上我测试过 Ubuntu 20.04 和 22.04 都没问题。编译器要求 GCC 7.5 或以上CMake 3.12还需要 Protobuf 3.x建议直接装 3.6 以上版本。这里有一个很容易踩的坑系统自带的 Protobuf 版本可能跟 Cyber RT 需要的不一致编译时会报出一堆“版本不匹配”的错误。我的建议是单独建一个虚拟环境或者容器用源码安装指定版本的 Protobuf不要污染系统的 Protobuf否则你后面跑其他 ROS 项目时可能会遇到动态库冲突。我自己的开发惯例是# 拉取 Apollo 源码只取需要的版本 git clone https://github.com/ApolloAuto/apollo.git cd apollo git checkout v6.0.0如果你只是想把 Cyber RT 抠出来单独玩国内开发者也有人做了独立维护的版本但我不推荐直接用那些第三方裁剪包还是从官方仓库里摘出cyber目录和它依赖的基础库更靠谱。摘出来后打开cyber目录里面自带CMakeLists.txt可以独立构建。有一点需要提醒Apollo 项目的代码量确实不小拉取和首次编译耗时会比较久。如果你想同时体验 ROS 生态做对照实验ROS 1 的安装可以借助一键安装脚本省去不少麻烦比如“鱼香ROS一键安装”这类社区工具对新手特别友好几分钟就能把 ROS 环境搭好。但 Cyber RT 目前没有类似的一键脚本还是要老老实实源码构建好在 Cyber RT 本身的依赖比 Apollo 全量少很多编译时间可以接受。4.2 用 cyber_recorder 录制和回放 Channel 数据跑通例子之前先熟悉 Cyber RT 自带的三件套工具它们是你在学习和调试阶段最常用的辅助cyber_recorder对标rosbag负责录制、拆分、恢复、回放 Channel 数据。cyber_monitor对标rostopic listrostopic echo实时查看当前 Channel 列表和消息内容。cyber_visualizer对标rviz简化版用于可视化点云、图像等传感数据。如果你是第一次启动 Cyber RT 环境先检查环境变量有没有 sourcesource cyber/setup.bash然后可以用cyber_recorder record -a录制所有 Channel。实际跑的时候这个方法比 ROS 的 rosbag 更直接因为 Cyber RT 的录制文件就是二进制格式回放时不需要额外的类型注册它把消息定义都内嵌在文件里了。这点在跨机器、跨版本迁移数据时非常省心不像 ROS 1 的 bag 换台电脑可能因为消息定义不一致而无法播放。回放用cyber_recorder play -f 文件默认会按实时时间间隔恢复消息发布也可以用-r 2.0加速回放。我第一次回放路测数据的时候为了图快直接设了 10 倍速结果下游规划模块因为输入时间戳跳变产生了误判。这里提醒一下Apollo 的模块之间很多依赖时间戳做逻辑判断回放速度不要乱调涉及多模块联调时最好保持实时或低倍速。4.3 实战演练编译一个最简单的 Talker/ListenerCyber RT 源码里自带了talker和listener的示例位置在cyber/examples/无论你是独立编译 Cyber RT 还是用 Apollo 的 Bazel 构建都可以直接跑。我先带你看核心代码逻辑。Talker 端#include cyber/cyber.h #include cyber/examples/proto/examples.pb.h using apollo::cyber::examples::proto::Chatter; int main(int argc, char* argv[]) { apollo::cyber::Init(argv[0]); auto talker_node apollo::cyber::CreateNode(talker); auto talker talker_node-CreateWriterChatter(/apollo/test_chatter); apollo::cyber::common::Rate rate(1.0); uint64_t seq 0; while (apollo::cyber::Ok()) { auto msg std::make_sharedChatter(); msg-set_timestamp(apollo::cyber::Time::Now().ToNanosecond()); msg-set_seq(seq); talker-Write(msg); rate.Sleep(); } }Listener 端#include cyber/cyber.h #include cyber/examples/proto/examples.pb.h using apollo::cyber::examples::proto::Chatter; void MessageCallback(const std::shared_ptrChatter msg) { AINFO Received message, seq msg-seq() , timestamp msg-timestamp(); } int main(int argc, char* argv[]) { apollo::cyber::Init(argv[0]); auto listener_node apollo::cyber::CreateNode(listener); auto listener listener_node-CreateReaderChatter( /apollo/test_chatter, MessageCallback); apollo::cyber::WaitForShutdown(); }如果你用的是独立的 CMake 构建CMakeLists.txt 里要链接 Cyber RT 的库和头文件我提供一个最小可用的模板供参考cmake_minimum_required(VERSION 3.12) project(cyber_demo) set(CMAKE_CXX_STANDARD 14) find_package(PkgConfig REQUIRED) include_directories(/path/to/apollo/cyber) include_directories(/path/to/apollo/cyber/proto) add_executable(talker talker.cc) target_link_libraries(talker /path/to/apollo/bazel-bin/cyber/libcyber.so /path/to/apollo/bazel-bin/cyber/proto/libcyber_proto.so )注意这里不要再链接系统的 Protobuf因为你用的 Cyber RT 库本身就是绑定特定 Protobuf 版本编译出来的混用很容易崩。编译好之后先启动 listener再启动 talker正常情况下你会在 listener 终端看到不断打印消息。如果想直观看到 Channel 数据用cyber_monitor工具启动后按回车能展开 Channel 看消息字段值这个工具非常适合验证通信是否正常。4.4 常见报错与排查经验实录这一节我想重点写因为我自己在 Cyber RT 上踩过的坑很多都是网上文档里找不到答案的。先整理一个速查表问题现象解决办法Protobuf 版本冲突编译时报libprotobuf.so.x: version ... not found用源码装匹配版本的 Protobuf并确保 LD_LIBRARY_PATH 优先指向你的版本找不到 libcyber.so编译通过、运行报加载库失败检查LD_LIBRARY_PATH是否包含bazel-bin/cyber目录cyber_launch 启动即退出提示Failed to load dag file检查 DAG 文件路径和.so库路径是否正确DAG 里写相对路径时容易出问题协程池满导致组件不响应某个组件偶发不触发 Proc回调里不要做阻塞操作把耗时逻辑丢到专用 Task 里异步执行多机通信失败两个设备之间收不到消息检查共享内存配置跨机时确保网络和 DDS 配置正常这里重点说两个。第一个是 Protobuf 版本冲突几乎所有独立编译 Cyber RT 的人都会遇到。Cyber RT 源码里会拉取特定版本的 Protobuf 作为第三方依赖如果你系统里已经装了别版本的 Protobuf动态链接时就会抓狂。我的做法是单独开一个 Docker 容器在容器里编译运行所有依赖都按 Cyber 的要求锁版本干净利落。第二个是 Proc 不触发表面上看配置、代码都没问题但组件就是不干活。这种情况我排查了整整一天最后发现是我在 Init 里启动了一个线程这个线程里调用了阻塞接口把整个协程调度器的线程资源卡住了。Cyber RT 的调度模型决定了同一优先级的协程共享线程池任何一个协程的阻塞都可能拖累其他协程。理解了这一点之后我给自己定了一条规矩Component 的回调里绝对不出现 sleep、绝对不等待锁、绝对不调同步阻塞服务。这不是代码风格偏好是框架的运行机制决定的硬约束。5. 实际项目里我踩过的坑学习笔记加分项5.1 不要把 ROS 的思维直接套进 Cyber RT我从 ROS 切到 Cyber RT 的初期最大的挫败来源就是想当然地把 ROS 的写法搬过来。比如 ROS 里我习惯在回调里直接做处理、直接发布结果到了 Cyber RT 发现这种写法容易让组件变成“时序黑洞”——因为回调执行不结束调度器就没法把这个协程切出去别的组件即使有更高优先级也只能等着。另一个典型区别是 Writer 的创建时机。ROS 里你可以在程序任意位置创建 Publisher很方便。Cyber RT 里如果你在 Proc 里反复创建 Writer不但效率低还可能触发框架的检查逻辑导致运行时报错。正确的做法是在 Init 阶段把该创建的 Writer、Reader、Service 全部创建好Proc 只负责业务逻辑。还有一个容易被忽略的差异是消息定义。ROS 的 msg 文件和 Cyber 的 proto 文件虽然都是 IDL但 proto 提供更强的类型约束和版本兼容性。写 proto 字段时我会注意给每个字段加上[deprecated true]标记再做演进而不是直接删字段因为 Cyber 的多个模块可能使用不同版本的 proto 定义直接删除字段会导致线上模块解析失败。5.2 多传感器融合时的组件配置技巧如果你用 Cyber RT 做相机和激光雷达融合可以利用 Component 的多消息绑定机制减少通道同步的代码量。我在实际项目中组件的配置是这样的message_channels: /apollo/sensor/camera/front_6mm/image message_channels: /apollo/sensor/lidar/velodyne64/compensator/PointCloud2这种情况下Component 模板需要声明两个消息类型Proc 函数签名也会有对应的两个参数。默认触发策略是所有消息都到了才触发这个对时间同步要求高的融合模块很友好。但要注意如果其中一个传感器掉线Proc 永远不会被触发超时检测只能自己写——我一般会在 Init 里启动一个定时器协程定期检查上一次 Proc 调用的时间戳超过阈值就报警。另外同样一套组件代码可以通过 DAG 文件实例化多份共享同一个.so库。这在多相机处理时特别好用写一个 CameraComponentDAG 里为每个相机挂一个独立实例每个实例的 Coff 文件配置不同的相机 ID 和消息通道。这个设计比 ROS 里为一个相机开一个 Node 要清爽得多。5.3 性能排障先看 Bvar 监控再猜问题Cyber RT 内置了一个叫 Bvar 的监控组件对标的是工程界常用的 Metrics 收集器。你可以用cyber_monitor查看一些关键指标比如协程的运行时间、调度次数、消息延迟分布等。遇到性能问题的时候不要直接猜是通信慢还是算法慢先看 Bvar 数据再定位。我当时遇到过一个问题融合模块的延迟时不时飙升到几百毫秒。常规思路是先怀疑通信层于是加了各种日志打点折腾两天没结果。后来看了 Bvar 的调度器统计发现是某个组件里调用了std::this_thread::sleep_for把线程资源占着不释放间接导致了其他协程排队。换成cyber::common::Rate的异步等待之后延迟曲线一下子就平稳了。从这次排查里我总结了一个经验在 Cyber RT 里性能问题优先往调度层想其次才是通信层。因为通信层的共享内存和 intra-process 机制已经非常高效真正容易出问题的是协程调度被打乱而不是数据传输本身。建议你在开发阶段就把 Bvar 监控挂上数据跑起来之后先看调度指标再决定要不要优化代码。这套笔记后续我还会继续写下一篇打算深入拆解 Cyber RT 的调度器源码把协程切换、任务优先级、负载均衡这几个关键机制讲透。如果你也在从 ROS 转向 Apollo或者正在纠结该学 ROS 还是 Cyber RT欢迎留言交流你的实际场景。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑