FastDDS 中 ASYNCHRONOUS 与 SYNCHRONOUS 的区别?

在 ROS 2 中,可以通过环境变量 RMW_FASTRTPS_PUBLICATION_MODE 来设置 FastDDS 的发布模式,有两个可选值:

模式 行为 特点
SYNCHRONOUS(同步) 调用 publish() / write() 时,数据在调用者线程中直接完成序列化并送入传输层发送 调用者线程会被阻塞,直到数据交给传输层;延迟更低,适合对实时性敏感的场景
ASYNCHRONOUS(异步) 调用 publish() / write() 时,数据仅被拷贝到内部发送队列,由一个独立的后台线程(Async Delivery Thread)负责实际发送 调用者线程立即返回,不阻塞;适合大数据量场景,但引入了额外的线程开销和微小延迟

同步 = 自己寄快递,异步 = 把快递放门口,等快递员来取。


为什么 ASYNCHRONOUS 可能会导致 CPU 使用率激增?

异步模式下,FastDDS 会为每个 DomainParticipant 创建一个专用的 Async Delivery Thread,负责从发送队列中取数据并通过传输层(UDP / Shared Memory)发出。

问题出在高频小数据的场景:

  • 异步线程的工作机制是"等待队列 → 被唤醒 → 发送 → 再等待"的循环。
  • 当节点以很高频率(如 200 Hz 的 IMU、编码器数据)持续发布小消息时,队列几乎永远不为空,线程刚处理完一条数据就立刻被下一条唤醒,根本没有机会真正休眠。
  • 此时线程退化为近似忙等(busy-loop),CPU 占用随之飙升。
  • 更糟的是,如果系统中有多个 Participant 都开启了 ASYNCHRONOUS 模式,每个都会各自持有一个这样的线程,CPU 开销会叠加放大。

异步线程是为"偶尔来一单大货"设计的,你让它"每秒送 200 个小包裹",它就累趴了。


如何正确使用 ASYNCHRONOUS 模式?

解决 Fast DDS 异步模式导致的 CPU 激增,不能靠单一手段,而需要一套分层治理策略。核心思路是:让发送线程从"无脑全速空转"变为"按需、限速、高效地工作"

1.启用 Flow Controller

没有流控的异步发送线程就像一个没有油门的车——永远踩到底。

// 1. 创建流控描述符
auto flow_ctrl_desc = std::make_shared<eprosima::fastdds::rtps::FlowControllerDescriptor>();
flow_ctrl_desc->name = "sensor_flow_ctrl";
flow_ctrl_desc->max_bytes_per_period = 80 * 1024 * 1024; // 80 MB/s
flow_ctrl_desc->period_ms = 100;                          // 每100ms一个周期
flow_ctrl_desc->scheduler_policy = eprosima::fastdds::rtps::FlowControllerSchedulerPolicy::FIFO;

// 2. 注册到 Participant
participant_qos.transport().flow_controllers().push_back(flow_ctrl_desc);

// 3. 绑定到 DataWriter
writer_qos.reliable_writer_qos().flow_controller_name = "sensor_flow_ctrl";

Flow Controller 在发送线程内部实现了 Token Bucket / Leaky Bucket 算法。当达到速率上限时,发送线程会主动 sleep 直到下一个周期,而不是自旋等待。CPU 从 100% 降至 20-40% 是常见效果。

调参建议

场景 max_bytes_per_period period_ms 等效带宽
激光雷达 (1.5MB × 20Hz) 35 MB 100 ~280 Mbps
相机图像 (4MB × 10Hz) 50 MB 100 ~400 Mbps
IMU (200B × 500Hz) 1 MB 100 ~80 Mbps
通用保守值 10 MB 100 ~800 Mbps

2.优化发送线程配置

绑核 + 实时调度
publisher_qos.publish_mode().async_thread = {
    .scheduling_policy = SCHED_FIFO,
    .priority = 10,
    .affinity = 0x08  // CPU3
};
  • 减少上下文切换和 Cache Thrashing
  • 避免与业务线程争抢导致双方都变慢
调整线程休眠策略(如果 Fast DDS 版本支持)

某些版本允许配置异步线程的等待策略:

// 优先使用条件变量阻塞等待,而非自旋
// 具体参数名取决于 Fast DDS 版本,查阅对应文档

3.减少无效工作量

合并小消息 / 增大批次

高频小消息是 CPU 杀手(每条消息 = 1次唤醒 + 1次序列化 + 1次系统调用)。

  • 应用层打包:将多个 IMU 样本合并为一个数组再发布
  • 增大 History Depth:允许发送线程一次取出多条样本批量处理
  • 使用 SHM 传输:同机通信走共享内存,跳过序列化+Socket 开销
// 共享内存传输(同机零拷贝)
descriptor.transport_configurations().push_back(
    std::make_shared<SharedMemTransportDescriptor>()
);
选择合适的 QoS
当前配置 问题 优化方向
RELIABLE + KEEP_ALL + 大队列 发送线程永远有活干,永不休眠 改为 KEEP_LAST(N),N 取合理值
BEST_EFFORT + 无流控 虽不重传但全速发送 加 Flow Controller
大 History + 慢消费者 队列堆积,发送线程持续满载 减小 History 或加速消费者

4.评估是否真的需要异步

消息频率 < 100Hz 且 单包 < 2KB → 切回 SYNCHRONOUS

同步模式没有后台线程,CPU 开销天然更低。

分离关键与非关键数据流
  • 控制指令 → 同步 Writer(低 CPU、确定性延迟)
  • 传感器大数据 → 异步 Writer + Flow Controller
  • 日志/诊断 → 异步 Writer + 更低优先级 + 更严流控

不同 Writer 可以有不同的发布模式和流控策略,不必一刀切。


5.系统与构建层面

措施 说明
Release 构建 Debug 版 Fast DDS 的发送线程 CPU 可能是 Release 的 5-10x
isolcpus isolcpus=3 防止 OS 将其他任务调度到发送线程专用核
IRQ 亲和性 将网卡中断迁离发送线程所在核心
关闭超线程 HT 兄弟核共享执行单元,导致性能不可预测
NUMA 对齐 确保发送线程、网卡、内存在同一 NUMA 节点
升级 Fast DDS 新版本持续优化异步线程调度逻辑

遇到异步 CPU 激增时:

  • 是否启用了 Flow Controller?
  • 是否是 Debug 构建?
  • 发送线程是否绑核?是否与业务线程隔离?
  • History 深度是否过大导致队列永远不满?
  • 是否存在高频小消息未合并?
  • 是否所有 Writer 都开了异步?低频 Writer 能否切回同步?
  • 网卡中断是否打到了发送线程所在核心?
  • QoS策略是否值得调整?

异步模式的 CPU 代价本质上是用计算换取吞吐和解耦。Flow Controller 是你为这笔交易设定的"预算上限"。没有预算上限的异步发布,就是在用无限 CPU 购买你可能并不需要的额外性能。先加流控,再做优化,最后考虑架构调整。


踩坑记录:外部系统通过 UDP 向 ROS 导航发数据,Domain ID 必须一致!

当外部系统(如上位机、PLC、仿真器)通过 UDP 将目标点等数据发送给 ROS 2 导航栈时,双方的 DDS Domain ID 必须一致,否则节点之间根本无法互相发现,数据发了也白发。

  • ROS 2 侧通过环境变量 ROS_DOMAIN_ID 设置(默认值为 0,Linux 下可用范围 0–101、215–232)。
  • 外部系统侧的 DDS 配置中也要设置相同的 Domain ID。
  • 可以先用 ros2 node list / ros2 topic list 确认当前域内是否能看到对方节点,看不到大概率就是 Domain ID 对不上。

同一物理网络上的多机通信还需要确保多播(multicast)可用,或者配置好 DDS 的 unicast discovery peers。

觉得有用的话,点个关注叭!

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐