一、系统架构对比:Apollo的Cyber RT框架 vs Autoware的ROS 2架构

做自动驾驶系统,选对底层框架太重要了。我这些年两个框架都深度用过,今天聊聊它们的架构差异。

先说结论:Apollo的Cyber RT是为自动驾驶量身定做的,Autoware的ROS 2则更通用、生态更丰富。你想想看,一个是从零开始只为无人车设计,另一个是机器人领域的通用方案改过来的,骨子里的基因就不一样。

1.1 模块化设计差异

先看一张整体架构对比图,我手绘的,帮你快速建立认知:

Apollo Cyber RT vs Autoware ROS 2 架构对比 Apollo Cyber RT 调度层 协程调度器 | 优先级队列 | CPU亲和性 通信层 共享内存 | 零拷贝 | 发布/订阅 数据层 Record文件 | 回放 | 数据融合 组件层 Component | 定时触发 | 事件驱动 Autoware ROS 2 节点层 Node | 生命周期 | 参数服务器 通信层 DDS | Topic/Service/Action | QoS 工具层 RViz2 | rosbag2 | tf2 | rqt 包管理 ament | colcon | 包依赖 核心差异:Cyber RT 强调确定性调度 + 零拷贝;ROS 2 强调生态兼容 + 分布式通信

1.2 Apollo Cyber RT 的核心设计

Cyber RT 是百度从零开始写的。我记得2018年第一次接触时,第一反应是「这玩意儿跟ROS完全不一样」。

它的设计哲学是:确定性 + 高性能。说白了,就是让每个模块的执行时间可预测,延迟尽可能低。

Cyber RT 三大核心机制:

  • 协程调度:不是线程,是协程。切换成本极低,能跑几千个协程不卡顿
  • 共享内存通信:数据不拷贝,直接读内存。延迟能压到微秒级
  • Component 模型:每个模块是一个Component,框架帮你管理触发条件

看个实际例子。这是Cyber RT里一个感知模块的写法:

class PerceptionComponent : public apollo::cyber::Component<CameraFrame> {
 public:
  bool Init() override {
    // 初始化模型、分配内存
    AINFO << "Perception component initialized";
    return true;
  }

  bool Proc(const std::shared_ptr<CameraFrame>& msg) override {
    // 每次收到CameraFrame数据,自动触发
    // 注意:这里拿到的msg是共享内存指针,不是拷贝
    ProcessFrame(msg);
    return true;
  }
};

// 注册组件
CYBER_REGISTER_COMPONENT(PerceptionComponent)

你发现没?没有while循环,没有显式的spin。框架帮你搞定了调度。我在项目里最喜欢的就是这个设计——写业务逻辑的人不用操心线程怎么管理。

我的经验:Cyber RT的协程调度在CPU密集型的感知模块里特别香。我曾经把一个激光雷达点云处理模块从ROS迁移到Cyber RT,延迟从15ms降到了3ms。当然,前提是你得理解协程的阻塞问题——协程里千万别做sleep或者同步I/O,否则整个调度器都卡住。

1.3 Autoware ROS 2 的架构特点

ROS 2 走的是另一条路。它继承了ROS 1的生态,但底层换成了DDS(数据分发服务)。

ROS 2 的设计哲学是:分布式 + 标准化。每个节点是独立的进程,通过DDS通信。好处是天然支持多机部署,坏处是——嗯,延迟比共享内存高不少。

ROS 2 四大核心机制:

  • Node 模型:每个功能模块是一个Node,独立进程运行
  • DDS 通信:使用RTPS协议,支持QoS配置(可靠性、历史记录等)
  • 生命周期管理:节点有明确的状态机(未配置→未激活→激活→关闭)
  • 工具链丰富:RViz2可视化、rosbag2录包、tf2坐标变换

看一个Autoware里感知模块的典型写法:

class LidarDetectionNode : public rclcpp::Node {
 public:
  LidarDetectionNode() : Node("lidar_detection_node") {
    // 声明参数
    this->declare_parameter<double>("confidence_threshold", 0.5);
    
    // 创建订阅者
    subscription_ = this->create_subscription<sensor_msgs::msg::PointCloud2>(
        "/input/points", 10,
        std::bind(&LidarDetectionNode::callback, this, std::placeholders::_1));
    
    // 创建发布者
    publisher_ = this->create_publisher<autoware_auto_msgs::msg::DetectedObjects>(
        "/output/objects", 10);
  }

 private:
  void callback(const sensor_msgs::msg::PointCloud2::SharedPtr msg) {
    // 处理点云数据
    auto objects = detect_objects(*msg, confidence_threshold_);
    publisher_->publish(objects);
  }

  rclcpp::Subscription<sensor_msgs::msg::PointCloud2>::SharedPtr subscription_;
  rclcpp::Publisher<autoware_auto_msgs::msg::DetectedObjects>::SharedPtr publisher_;
};

注意:ROS 2的回调函数里,拿到的msg是拷贝过来的。如果你处理的是大尺寸数据(比如高清图像、密集点云),内存拷贝的开销不容忽视。我曾经在一个项目中,仅仅因为把图像从sensor_msgs转成cv::Mat,就多花了8ms。解决办法?用shared_ptr传递,或者用zero-copy的DDS配置。

1.4 通信机制深度对比

这是两个框架最核心的差异点。我直接上对比表:

对比维度 Apollo Cyber RT Autoware ROS 2
底层协议 自研共享内存 + Socket DDS (Data Distribution Service)
数据传输 零拷贝(共享内存指针传递) 序列化拷贝(默认)
延迟典型值 10-100 μs(同进程) 100-1000 μs(同机)
通信模式 发布/订阅 + 服务调用 发布/订阅 + 服务 + 动作
QoS支持 有限(可靠性、优先级) 丰富(可靠性、持久性、延迟预算等)
跨机通信 需要额外配置 原生支持(DDS天生分布式)
调试工具 cyber_monitor, cyber_recorder ros2 topic, ros2 bag, RViz2

看到这个表,你可能会问:「那到底选哪个?」

我的建议是这样的:

  • 如果你做的是量产项目,对延迟和确定性要求极高——选Cyber RT。它的协程调度和零拷贝在实车上表现确实好。
  • 如果你做的是研发验证,需要快速迭代、用各种现成工具——选ROS 2。生态太丰富了,随便一个算法包都能直接用。
  • 如果你需要多车协同或者车路协同——ROS 2的DDS天然支持分布式,省去很多网络编程的麻烦。

避坑指南:我曾经在一个项目中,团队坚持用ROS 2做感知融合,结果发现点云数据在节点间传来传去,延迟累积到了50ms。后来我们改成了进程内通信(用composition的方式把多个节点放到一个进程里),延迟降到了12ms。所以,别被「分布式」这个词迷惑了——同机通信时,进程内通信永远比进程间通信快。

1.5 模块化设计的实际差异

说完了通信,再聊聊模块化。这个差异其实更影响日常开发。

Cyber RT的模块化:

  • 每个功能是一个Component,框架管理生命周期
  • Component之间通过Channel(通道)连接
  • 调度由框架统一管理,开发者不用管线程
  • 模块配置通过DAG文件(有向无环图)描述

ROS 2的模块化:

  • 每个功能是一个Node,独立进程或组合进程
  • Node之间通过Topic/Service/Action连接
  • 每个Node有自己的线程池,开发者需要管理回调
  • 模块配置通过launch文件和参数服务器

举个例子。在Cyber RT里,你要加一个障碍物预测模块,只需要写一个Component,然后在DAG文件里声明输入输出通道:

# dag文件示例
module_config {
  module_library : "libprediction_component.so"
  components {
    class_name : "PredictionComponent"
    config {
      name : "prediction"
      readers {
        channel: "/perception/obstacles"
      }
      writers {
        channel: "/prediction/predicted_obstacles"
      }
    }
  }
}

而在ROS 2里,你需要写一个Node,然后在launch文件里启动它:

# launch文件示例
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='prediction',
            executable='prediction_node',
            name='prediction_node',
            parameters=[{'confidence_threshold': 0.5}],
            remappings=[
                ('/input/obstacles', '/perception/obstacles'),
                ('/output/predicted', '/prediction/predicted_obstacles')
            ]
        )
    ])

你看出区别了吗?Cyber RT的DAG文件是静态配置,启动时框架帮你把整个数据流图建好。ROS 2的launch文件是动态启动,每个节点独立运行。前者更可控,后者更灵活。

我的个人习惯:在做原型验证时,我倾向于用ROS 2,因为改一个参数不用重新编译。但在做量产部署时,我更喜欢Cyber RT的DAG配置——整个系统的数据流一目了然,调试起来特别方便。

1.6 小结

两个框架没有绝对的好坏。Cyber RT在性能和确定性上占优,ROS 2在生态和灵活性上胜出。选哪个,取决于你的项目阶段和团队背景。

如果你是从零开始做自动驾驶,我建议先学ROS 2——资料多、社区活跃、上手快。等你把算法验证通了,再考虑迁移到Cyber RT做性能优化。

嗯,这一章就聊到这儿。下一章我们深入看看两个框架的调度机制——那个才是真正拉开差距的地方。


可可精品资料库