第一次在 ROS2 里真正意识到 DDS 存在的,通常不是啃文档的时候,而是ros2 topic list明明两个节点都在跑却互相看不见、或者一帧几 MB 的点云传起来像幻灯片的时候。ROS2 把通信层整个交给了 DDS,而它默认用的那套实现就是Fast-DDS(老名字叫 Fast RTPS,eProsima 家的)。平时用 apt 装好的 ROS2,Fast-DDS 是跟着一起装进来的,你几乎感觉不到它的存在;可一旦要改传输方式、要调 QoS、要把多机通信压到极致延迟,就绕不开一件事——自己把 Fast-DDS 的环境搭一遍,并且让 ROS2 用上你自己编的那一份。
这篇是我把 Fast-DDS 从源码编到能跑通 HelloWorld、再接管 ROS2talker/listener的完整记录,中间踩过的坑、版本对齐的细节、XML 配置里那些"少一个字符就静默失效"的地方,我都会写清楚。内容偏进阶,但每一步我都尽量说清"为什么这么干",只要能看懂cmake --build和source setup.bash,跟着走就能跑通。
1. 先把层次理清楚:Fast-DDS 站在 ROS2 的哪个位置
1.1 RMW 是 ROS2 给自己留的换挡杆
很多人对 ROS2 的理解停在"话题、服务、动作"这一层,以为publish()就是直接往网络里发数据。实际上这个调用往下走要穿过好几层:ROS2 客户端库(rclcpp/rclpy)→ RMW 接口(rosidl 生成的消息类型 + rmw 抽象层)→ 具体 DDS 实现的适配包 → DDS 库本体 → 传输层(UDP / 共享内存)。
RMW全称是 ROS Middleware Interface,它存在的唯一目的就是让上层的 rclcpp 不绑定任何一家 DDS。你写的代码只认rclcpp::Publisher,至于底下是 Fast-DDS、CycloneDDS 还是别的商业实现,靠一个环境变量RMW_IMPLEMENTATION决定。这个设计的好处是换中间件不改代码,坏处是——出问题的时候你不知道该骂哪一层。我见过太多人去 GitHub 上给 ROS2 提 issue,结果最后发现是自己装的 DDS 版本和 rmw 适配包对不上。
所以搭 Fast-DDS 环境之前,先把这条链路在脑子里画出来。你现在要干的事,本质上是替换掉倒数第二层,并且要保证倒数第三层(适配包)能跟它握手成功。这也是为什么单纯编一个 Fast-DDS 出来,装完什么都不改,ROS2 还是走系统自带的那份,你会以为"白干了"。
1.2 Fast RTPS、Fast DDS、rmw_fastrtps_cpp 到底是不是一回事
这三个名字是新手最容易混淆的地方,我列一下它们的关系:
| 名字 | 是什么 | 什么时候会碰到 |
|---|---|---|
| Fast RTPS | Fast-DDS 的旧名,2.x 之前基本都叫这个 | 看老教程、老 CMake 变量(FASTRTPS_*)时 |
| Fast-DDS | 同一个库的现用名 | 官方仓库、新文档、fastdds命令行工具 |
| rmw_fastrtps_cpp | ROS2 侧的适配包,链接 Fast-DDS | ROS2 源码里的ros2/rmw_fastrtps仓库 |
关键在于,改名的同时环境变量也改了。早期版本用FASTRTPS_DEFAULT_PROFILES_FILE指定 XML 配置文件,较新的版本改成了FASTDDS_DEFAULT_PROFILES_FILE,并且为了兼容还认旧名字。这就导致一个非常典型的坑:你从某篇两年前的文章里抄了旧变量名,在一套新版本上跑,配置文件根本没加载,但程序一点报错都没有,你只会觉得"这个配置项好像没生效"。我建议你把这两个名字都记着,写完先去日志里确认一眼加载路径。
1.3 什么时候才真的需要自己动手编
不是所有人都需要走源码编译这条路。如果你的目标只是"让 ROS2 跑起来",apt 装完就完事了。真正需要自己搭一套独立 Fast-DDS 环境的,通常是下面几类情况:
- 需要用某个还没进发行版的新特性(比如新的传输方式、新的发现机制),系统包版本太老。
- 要开统计模块(Statistics)、要接 DDS 层的观测工具做性能分析,需要打开编译期开关。
- 想做跨版本对比,同一台机器上并存两份不同版本的 Fast-DDS 做 A/B 测试。
- 需要自定义 XML profile 并且要用到编译期才生效的宏。
如果只是想改改 QoS、调调传输,其实 apt 装的那份 + XML profile 就够了,不用折腾编译。这一点先说清楚,免得有人为了改个history depth就花一下午编库。
2. 版本对齐:这一步错了,后面全是无用功
2.1 ROS2 发行版和 Fast-DDS 大致是怎么对应的
每个 ROS2 发行版都会锁一个 DDS 版本范围,官方的二进制包就是按这个范围编的。你把版本对错,轻则配置项不认,重则编译出来的libfastdds.so和适配包符号对不上,一跑就 segfault。
| ROS2 发行版 | 默认 RMW | Fast-DDS 大致版本 |
|---|---|---|
| Foxy | rmw_fastrtps_cpp | 2.x 早期 |
| Galactic | rmw_cyclonedds_cpp | 2.x |
| Humble | rmw_fastrtps_cpp | 2.6 上下 |
| Jazzy 及之后 | rmw_fastrtps_cpp | 2.14 上下,逐步迁到 3.x |
这张表只是给你一个方向感,真正靠谱的做法是查本地:
dpkg -l | grep -i fastrtps dpkg -s ros-humble-fastrtps 2>/dev/null | grep -i version拿到版本号之后,再去 Fast-DDS 仓库里 checkout 对应的 tag 分支。别偷懒用main,main永远在往前跑,你今天的编出来的东西明天就可能有 API 变动。
2.2 依赖清单:哪些能 apt 拿,哪些必须自己编
Fast-DDS 本体不算大,但它的依赖链有点绕,核心是这么几块:
- Fast-CDR:序列化库,Fast-DDS 的数据编解码全靠它。版本必须和 Fast-DDS 匹配,不能随便拿一个。
- foonathan_memory:内存池实现。带内存池的 2.x 版本这是硬依赖,缺了直接找不到头文件。它一般通过
foonathan_memory_vendor这个壳仓库拉取并编译。 - Asio:网络 I/O。可以走系统包,也可以用仓库里
thirdparty/目录自带的。 - TinyXML2:读 XML profile 用的。
- OpenSSL:只在你要开安全传输(TLS/Security)时才需要。
我的习惯是:能走-DTHIRDPARTY=ON用仓库自带源码的,就尽量用自带的。因为系统包版本的 Asio、TinyXML2 在不同 Linux 发行版上差异不小,用自带的能省掉一大半"在我机器上能编,在你机器上编不过"的问题。代价是编译时间会长一点,首次全量编译十几分钟很正常。
2.3 二进制和源码混装:症状比报错更恶心
这是我最想强调的一条。你 apt 装了一整套 ROS2,里面已经有一个libfastrtps.so躺在那儿了。然后你编了一份新的装到~/fastdds/install。这时候机器上就有两份同名的动态库。
会发生什么?取决于LD_LIBRARY_PATH的顺序和二进制里的 RPATH。典型的症状是:
- 编译期一切正常,链接期也正常,跑起来行为却和你的 XML 配置对不上。
ldd看某个可执行文件,发现链的是/opt/ros/...下的那一份,不是你刚装的。- 更阴的是部分链接你的、部分链接系统的,程序能起来,但某些功能随机失效。
排查方法很直接,编完先ldd一遍:
ldd ~/fastdds/install/bin/HelloWorldExample | grep -i -E "fastdds|fastcdr|foonathan"输出的路径如果不是你安装目录,说明LD_LIBRARY_PATH没生效或者被别的东西覆盖了。这一步一定要在跑任何测试程序之前做掉,不然你会花大量时间去怀疑别的东西。
3. 源码编译 Fast-DDS 的完整链路
3.1 工作空间布局
我自己用的是"源码一个目录、编译产物分离开"的布局,方便后面删干净重来:
~/fastdds/ ├── src/ # 三个仓库的源码 │ ├── foonathan_memory_vendor/ │ ├── Fast-CDR/ │ └── Fast-DDS/ ├── build/ # 中间产物,随时可删 └── install/ # 最终安装前缀mkdir -p ~/fastdds/src && cd ~/fastdds/src git clone https://github.com/eProsima/foonathan_memory_vendor.git git clone https://github.com/eProsima/Fast-CDR.git git clone https://github.com/eProsima/Fast-DDS.git克隆完之后记得git checkout到和前面查到的版本匹配的 tag,三个仓库的版本是有对应关系的,不要各选各的。
3.2 依赖顺序:先编谁后编谁
顺序搞反是新手最常见的卡点。正确的链路是:
- foonathan_memory_vendor→ 产出
foonathan_memory。 - Fast-CDR→ 依赖上一步装的东西,产出
libfastcdr。 - Fast-DDS→ 同时依赖上面两个。
每一层编完都要install到同一个前缀,然后在下一层的 CMake 里把这个前缀加到CMAKE_PREFIX_PATH,否则下一层会去找系统的包,编出来就串了。
cd ~/fastdds/src/foonathan_memory_vendor mkdir build && cd build cmake .. -DCMAKE_INSTALL_PREFIX=~/fastdds/install -DBUILD_SHARED_LIBS=ON cmake --build . -j$(nproc) && cmake --install .Fast-CDR 同理,只是多带一个-DCMAKE_PREFIX_PATH=~/fastdds/install。这个变量看着不起眼,但它决定了 CMake 能不能找到你刚装的那个 foonathan。
3.3 Fast-DDS 本体的 CMake 参数逐条说
到了最关键的一步:
cd ~/fastdds/src/Fast-DDS mkdir build && cd build cmake .. \ -DCMAKE_INSTALL_PREFIX=~/fastdds/install \ -DCMAKE_PREFIX_PATH=~/fastdds/install \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_SHARED_LIBS=ON \ -DTHIRDPARTY=ON \ -DCOMPILE_EXAMPLES=ON \ -DCOMPILE_TOOLS=ON cmake --build . -j$(nproc) cmake --install .几个参数值得单独讲:
CMAKE_BUILD_TYPE=Release:默认不写是没优化的,DDS 这种带大量模板和序列化的库,Debug 版跑起来性能差一个量级,测速之前一定要 Release。THIRDPARTY=ON:用仓库自带的第三方源码,减少环境差异。COMPILE_EXAMPLES=ON:会编出 HelloWorld 之类的示例,这是你验证环境最省事的工具,一定要开。COMPILE_TOOLS=ON:会编出fastdds命令行工具,后面排查很有用。
编完之后安装目录下大致会有bin/、lib/、include/。ls install/bin看一眼有哪些可执行文件,不同版本名字会略有差别,以你实际看到的为准。
3.4 环境变量与生效验证
export FASTDDS_HOME=~/fastdds/install export LD_LIBRARY_PATH=$FASTDDS_HOME/lib:$LD_LIBRARY_PATH export PATH=$FASTDDS_HOME/bin:$PATH注意:
LD_LIBRARY_PATH只影响当前 shell 及其子进程,写进~/.bashrc之前先想清楚——如果你后面还要用 apt 装的 ROS2,这个顺序会改变它链接的库,可能把本来正常的 ROS2 弄坏。我的做法是写成一个单独脚本source_fastdds.sh,需要的时候再 source。
验证三部曲:ldd看链接路径、fastdds --help看工具能不能跑、跑一遍 HelloWorld 看能不能收发。前两步过了,第三步基本不会有问题。
4. 用 HelloWorld 把环境跑通
4.1 发布端与订阅端怎么起
Fast-DDS 示例里最经典的就是一对 HelloWorld。大致是HelloWorldExample(发布端)和HelloWorldExampleSubscriber(订阅端),不同版本命名可能是HelloWorldExamplePublisher之类的变体,装完ls install/bin确认一下真实名字。
开两个终端,分别 source 环境脚本,然后一个跑发布一个跑订阅。正常情况下订阅端会开始打印收到的消息,发布端会打印匹配到订阅者的日志。这一对程序的价值在于:它完全不经过 ROS2,纯 DDS 层。如果它跑不通,说明你的 Fast-DDS 环境本身有问题,跟 ROS2 一点关系都没有。这样就能把故障范围一刀切开。
4.2 怎么确认走的是你编的那一份
光看到消息收到还不够,得确认它链的是你的库。在程序运行中另开一个终端:
cat /proc/$(pgrep -f HelloWorldExample | head -1)/maps | grep -i fastdds看到你install/lib下的路径,才算真的验证通过。这个方法比ldd更硬,因为它反映的是进程实际加载的映射。
4.3 常见报错速查
| 现象 | 大概率原因 | 处理方向 |
|---|---|---|
启动报找不到libfastcdr.so | 前缀没进LD_LIBRARY_PATH | 检查环境变量,重开终端再试 |
编译期找不到foonathan_memory | 上一层没装或前缀没传 | 补CMAKE_PREFIX_PATH重编 |
| 两端都起来了但收不到消息 | 发现机制没配对 | 见第 8 节的排查链路 |
| 运行中随机崩 | 两份库混链 | ldd+maps双重确认 |
| XML 配置不生效 | 命名空间或变量名写错 | 见第 5 节 |
5. XML Profile:把配置从代码搬到文件
5.1 那个必须一字不差的命名空间
Fast-DDS 的 XML 配置文件里,根节点的命名空间是硬编码校验的。写错一个字符,它不会报错,只会静默地当成没配置。这是我这几年遇到的最有欺骗性的一个问题。
<?xml version="1.0" encoding="UTF-8"?> <dds xmlns="http://www.eprosima.com/XMLSchemas/fastRTPS_Profiles"> <profiles> <participant profile_name="my_participant" is_default_profile="true"> <rtps> <name>my_participant</name> </rtps> </participant> </profiles> </dds>注意命名空间里那个fastRTPS是老名字留下的,即使现在库叫 Fast-DDS 了,这个字符串也还是这个样子。用编辑器的时候小心别被自动补全改掉了。
5.2 让配置真正生效的三个环境变量
光有文件没用,得告诉程序去哪读:
export FASTDDS_DEFAULT_PROFILES_FILE=/path/to/profile.xml # 老版本用这个,两个都设上不会有坏处 export FASTRTPS_DEFAULT_PROFILES_FILE=/path/to/profile.xml # ROS2 场景下,让 rmw 从 XML 里读 QoS export RMW_FASTRTPS_USE_QOS_FROM_XML=1最后那个变量特别值得说。ROS2 默认会用自己的一套 QoS 覆盖你在 XML 里写的 QoS 配置,所以你会觉得"我明明写了可靠传输,怎么还是 best effort"。打开RMW_FASTRTPS_USE_QOS_FROM_XML=1之后,XML 里的 QoS 才真正管用。这个开关不打开,前面配的 QoS 全是白写。
5.3 一份能直接改的 profile 骨架
<profiles> <transport_descriptors> <transport_descriptor> <transport_id>udp_transport</transport_id> <type>UDPv4</type> <sendBufferSize>1048576</sendBufferSize> <receiveBufferSize>1048576</receiveBufferSize> </transport_descriptor> </transport_descriptors> <participant profile_name="participant_profile" is_default_profile="true"> <rtps> <useBuiltinTransports>false</useBuiltinTransports> <userTransports> <transport_id>udp_transport</transport_id> </userTransports> </rtps> </participant> <data_writer profile_name="reliable_writer"> <qos> <reliability><kind>RELIABLE</kind></reliability> <history><kind>KEEP_LAST</kind><depth>20</depth></history> </qos> </data_writer> </profiles>useBuiltinTransports置为 false 再挂userTransports,是接管传输层的标准姿势。不这么写,内置传输会一直生效,你的配置等于叠加上去,行为会变得难以预测。
6. 传输层:共享内存和 UDP 该怎么选
6.1 共享内存不是万能加速器
Fast-DDS 内置了共享内存(SHM)传输,同主机内大消息走 SHM 确实能省掉内核网络栈的拷贝,延迟和 CPU 占用都会好看很多。但它有几个前提条件,不满足会静默回退到 UDP,你会以为它在用 SHM,其实没有:
- 必须在同一台物理机(或者说能访问同一块共享内存区域)。
- 消息大小不能超过共享内存段的上限。段大小默认值在不同版本里不一样,小到几百 KB 也有,十几 MB 也有,大点云很容易挤爆。
- 依赖
/dev/shm,容器里如果没给它足够的空间,直接就退化了。
判断有没有真的走 SHM,我一般用两条:一是开统计模块看传输层计数,二是干脆用环境变量关掉 UDP 排除干扰:
export FASTDDS_BUILTIN_TRANSPORTS=UDPv4这个变量是我排查问题最爱用的一个,因为它能在不改代码、不改 XML的前提下,把内置传输限制成某一种,快速二分定位问题出在哪条传输上。
6.2 大消息场景该动哪几个参数
传大消息(点云、图像、大 JSON)的时候,最常见的两个现象是"丢包"和"卡顿"。要动的主要是这几个:
| 参数 | 作用 | 调大之后的代价 |
|---|---|---|
sendBufferSize/receiveBufferSize | 收发缓冲区 | 每连接多占内存 |
maxMessageSize | 单条消息上限 | 需要下游也能接住 |
heartbeatPeriod | 心跳间隔 | 调太小会增加无谓流量 |
maxInitialPeersRange | 初始发现范围 | 影响发现开销 |
我的经验是先动缓冲区,再动消息大小,最后才碰心跳。因为缓冲区不够是最直观的丢包来源,而心跳参数调错了会让重传逻辑变得很诡异,不容易回退。
6.3 多网卡机器上的经典故障
一台机器上插了有线、无线、USB 网卡、Docker 虚拟网桥,这是机器人开发机的常态。Fast-DDS 默认会把发现报文往所有接口上发,看起来是好事,实际会带来两个问题:一是流量到处跑,二是发现阶段的响应来自非预期网段,导致发布者和订阅者互相认不全。
解决办法是在 XML 里给传输描述加白名单,只留干活的那张网卡:
<interfaceWhiteList> <address>192.168.1.100</address> </interfaceWhiteList>或者干脆用ROS_LOCALHOST_ONLY=1只在回环上通信,做单机调试时非常省心。顺带说一句,ROS_DOMAIN_ID越大,占用的 UDP 端口段越高,同一网段里多人共用网络时,提前分配好 domain id 能少很多莫名其妙的干扰。
7. 让 ROS2 真正用上你编的 Fast-DDS
7.1 只换环境变量是不够的
这是全文最重要的一个结论:你编了一份新的 Fast-DDS,但 ROS2 的rmw_fastrtps_cpp是 apt 装的,它链接的是系统那份。你只改LD_LIBRARY_PATH,属于"碰运气式接管",能不能生效取决于 RPATH 和加载顺序,而且版本一旦有 ABI 差异就会崩。
想干净地接管,只有一条路:把rmw_fastrtps也一起从源码编,放进同一个工作空间,让 colcon 按依赖顺序构建。这样编出来的适配包会明确链接你那份 Fast-DDS。
mkdir -p ~/ros2_dds_ws/src && cd ~/ros2_dds_ws # 把 rmw_fastrtps 以及它依赖的 rosidl 相关包放进来 colcon build --symlink-install source install/setup.bash编完之后验证:
export RMW_IMPLEMENTATION=rmw_fastrtps_cpp ros2 run demo_nodes_cpp talker再开一个终端跑listener。同时用ldd确认talker进程实际链接的是你的库。这一步确认过了,才算真正接管成功。
7.2 观测工具要配合着用
接管之后,光靠ros2 topic list能看的东西太少了。我的习惯是三层工具一起用:
- ROS2 层:
ros2 topic info /话题名 -v,能直接看到 QoS 和发布/订阅端点的匹配数量,这是判断"为什么收不到"最快的一招。 - DDS 层:
fastdds命令行工具,fastdds --help看看有哪些子命令。fastdds shm可以观察共享内存的使用情况,段清不干净的时候重启相关进程。 - 系统层:
ss -unp看 UDP 端口占用,ip -s link看网卡有没有丢包。
三层对照着看,基本上没有定位不下来的问题。单看一层,尤其是只看 ROS2 层,很容易得出错误结论。
7.3 统计与日志的开关
要真做性能分析,得打开统计模块。这个一般是编译期开关加运行时 XML 配置的组合,编之前把对应的 CMake 选项打开。日志方面,Fast-DDS 的日志量可以非常大,日常开发建议用RMW_IMPLEMENTATION换回默认实现对照着测,怀疑是 DDS 层问题时再打开详细日志。
提示:日志动不动几百 MB,别一直在生产环境开着。我的做法是在 XML 里单独配一份"调试用 profile",需要的时候切换文件路径,而不是改代码。
8. 一条完整的排查链路:两端收不到消息怎么办
这个问题我遇到太多次了,索性把排查顺序固化下来,从下往上找,比乱试效率高得多。
第一步,确认 DDS 层本身是通的。用第 4 节的 HelloWorld 示例,不经过 ROS2 直接收发。通了就说明 Fast-DDS 环境没问题,故障在 ROS2 侧;不通就停在这儿,先解决库和环境的问题。
第二步,确认两端在不在同一个 domain。ROS_DOMAIN_ID不一致是最常见的原因,尤其多人共用一个网络环境时。这个变量只要有一边没设或者设错,就是彻底看不见。
第三步,确认发现机制能找到对方。ROS2 较新版本有ROS_AUTOMATIC_DISCOVERY_RANGE这类变量,取值可以限制成同网段、仅本机、甚至关闭。如果它被设成了仅本机,跨机通信必然失败。用ros2 doctor --report把当前环境一次性打出来看,比一个个echo快得多。
第四步,确认网络层没被拦。DDS 的发现依赖多播,很多防火墙默认拦多播。临时关掉防火墙测一次,能立刻区分是配置问题还是网络问题。
第五步,确认 QoS 兼容。发布端是 best effort、订阅端要 reliable,这对组合是匹配不上的,端点数量看着有,消息就是不来。用ros2 topic info -v一看就清楚。
第六步,怀疑缓存。ROS2 有守护进程会缓存节点信息,改完环境变量没重启守护进程,看到的是旧状态。ros2 daemon stop再重新跑命令,能排掉一批"幽灵问题"。
这套顺序的价值在于每一步都能把问题范围砍掉一半,而不是在同一层里反复试。我早期最常犯的错就是跳过第一步,直接去调 XML,最后发现库本身都没编对。
最后分享一个我自己踩过的坑:编完 Fast-DDS 之后我习惯性地把LD_LIBRARY_PATH写进了~/.bashrc,结果过了一阵子去跑一个旧的 ROS2 项目,编译期就报了一堆奇怪的模板错误,查了半天才想起来是环境变量污染导致的。现在我所有这类"会改变全局链接行为"的配置,一律写成独立的 source 脚本,用完unset或者直接开新终端。省下的调试时间,远比多敲两行命令要多。