
1. 为什么要在FastDDS里死磕传输层选型搞过分布式实时系统的朋友应该都有体会DDS这套中间件用起来确实省心QoS一配、Topic一建数据就自动在节点之间流动了。但真到了对延迟和吞吐有硬指标的场景比如自动驾驶的传感器融合、工业控制里的运动指令下发、高频交易的行情分发默认配置往往撑不住。问题十有八九出在传输层上。FastDDS作为目前开源DDS实现里比较活跃的一个提供了多种传输方式其中最常被拿来对比的就是SHMShared Memory共享内存和DATA-SHARING。这两个名字听起来都和“共享”沾边很多人第一次接触会懵既然都是走共享内存那区别在哪是不是选一个就行了实际情况是这俩虽然底层都依赖共享内存机制但设计目标、适用场景、内存管理策略完全不同。SHM是一种传输层实现它替代了UDP/TCP负责把序列化后的数据从A进程搬到B进程而DATA-SHARING是一种数据分发优化机制它让同一主机上的多个DataReader直接读取DataWriter写好的历史缓存连“传输”这个动作都省了。理解这个本质差异是做好选型的第一步。这篇文章适合正在用FastDDS做项目、被延迟或CPU占用困扰、或者单纯想搞清楚这两个机制底层怎么跑的开发者。我会从设计思路、内存布局、实操配置、性能实测、踩坑记录几个维度展开尽量把每个“为什么”讲透。读完之后你应该能根据自己项目的节点分布、数据量、实时性要求做出有依据的选择而不是照搬示例配置。2. 两种机制的设计思路与核心差异拆解2.1 SHM传输层把共享内存当成一条“网线”SHM传输层的定位很明确当通信双方在同一台物理机上时用共享内存替代网络协议栈避免数据在用户态和内核态之间来回拷贝。它的工作模型和UDP传输层是平行的你可以把它理解成“一条不走网卡的本地网线”。具体来说每个参与SHM通信的DomainParticipant会创建一个共享内存段里面维护着端口、缓冲区、同步信号等结构。发送方把序列化后的数据写入共享内存缓冲区然后通过信号量或条件变量通知接收方接收方从缓冲区读取数据反序列化后交给上层。整个过程数据只拷贝了两次写入缓冲区一次读出一次比UDP少了内核协议栈的参与。SHM传输层的关键设计点在于端口管理和缓冲区分配。FastDDS为每个Participant分配一个唯一的端口号发送方需要先通过端口找到目标Participant的共享内存段。缓冲区大小、数量、健康检查间隔这些参数都可以通过XML或代码配置。默认情况下FastDDS会自动为同主机通信启用SHM但很多人不知道它其实是可以精细调优的。注意SHM传输层虽然快但它并不是零拷贝。数据仍然需要从应用层的对象序列化到共享内存缓冲区接收方再反序列化回来。对于大消息比如图像帧这两次拷贝的开销不可忽略。2.2 DATA-SHARING让Reader直接读Writer的缓存DATA-SHARING的思路更激进既然同一主机上的DataWriter和DataReader共享同一块物理内存那为什么还要把数据搬来搬去直接让Reader去读Writer的历史缓存不就行了这个机制的核心是共享内存池Shared Memory Pool。当一个DataWriter被配置为DATA-SHARING模式时它发送的数据不再走任何传输层而是直接写入一块预先分配好的共享内存区域。同一主机上的DataReader如果也支持DATA-SHARING就会直接映射这块内存读取自己需要的历史样本。整个过程没有传输、没有信号通知、没有缓冲区拷贝Reader读到的就是Writer写的那份数据本身。听起来很美好但限制也很明显。首先DATA-SHARING只对同一主机、同一Domain、同一Topic的通信有效跨主机通信必须回退到SHM或UDP。其次它依赖Writer的历史缓存History Cache如果Reader的QoS要求读取已经被Writer移除的历史样本就会失败并触发回退。再者DATA-SHARING对内存对齐和生命周期管理有更严格的要求配置不当容易出现Reader读不到数据的情况。2.3 一张表看清两者的本质区别对比维度SHM传输层DATA-SHARING定位传输层实现替代UDP/TCP数据分发优化机制数据拷贝次数2次序列化写入读出反序列化0次直接读Writer缓存跨主机支持不支持仅限本机不支持仅限本机依赖历史缓存不依赖强依赖Reader需能匹配到样本配置复杂度中等需调端口和缓冲区较高需协调Writer和Reader的QoS适用数据量中小消息高频传输大消息、低频、多Reader场景回退机制可回退到UDP可回退到SHM或UDP这张表是选型时的核心参考。简单说如果你的场景是同一主机上多个节点高频交换小消息SHM传输层是稳妥选择如果是大消息比如点云、图像需要被多个Reader消费且对延迟极度敏感DATA-SHARING更合适。3. 核心实现细节与内存布局解析3.1 SHM传输层的端口与缓冲区管理FastDDS的SHM传输层在实现上有一套完整的端口分配逻辑。每个DomainParticipant启动时会根据自己的Participant ID和Domain ID计算出一个端口号范围然后尝试在共享内存文件系统中创建对应的段文件。在Linux下这些段文件通常出现在/dev/shm/目录下命名格式类似fastrtps_shm_domain_participant。发送方要发数据时先通过端口号定位到目标Participant的共享内存段然后检查是否有可用的缓冲区。FastDDS为每个SHM端口维护一个缓冲区池默认大小是512KB一个缓冲区最多16个。这个默认值在很多场景下是不够的比如传输1MB以上的消息就会触发分片或失败。缓冲区分配策略可以通过XML配置调整transport_descriptors transport_descriptor transport_idshm_transport/transport_id typeSHM/type maxMessageSize1048576/maxMessageSize segment_size4194304/segment_size port_queue_capacity512/port_queue_capacity healthy_check_timeout_ms1000/healthy_check_timeout_ms /transport_descriptor /transport_descriptors这里maxMessageSize决定了单条消息的最大尺寸segment_size是共享内存段的总大小port_queue_capacity是端口队列容量。实测下来如果消息平均大小在100KB左右segment_size设成4MB、port_queue_capacity设成512是比较稳的。设太小会导致频繁的缓冲区等待设太大则浪费内存。实操心得在容器化环境里跑FastDDS时/dev/shm的默认大小往往只有64MB多个Participant一起跑很容易把共享内存耗尽。上线前务必检查df -h /dev/shm必要时在容器启动参数里调大--shm-size。3.2 DATA-SHARING的共享内存池与样本生命周期DATA-SHARING的实现比SHM传输层更复杂因为它要解决的是“多个Reader如何安全地并发读取同一份数据”的问题。FastDDS的做法是为每个DataWriter分配一个共享内存池池里存放的是序列化后的样本数据。每个样本有一个描述符记录了偏移量、大小、序列号等信息。DataReader要读数据时先通过共享内存池的元数据找到自己需要的样本然后直接映射对应的内存区域进行反序列化。这里的关键是引用计数和生命周期管理Writer不能随意删除还在被Reader引用的样本否则Reader会读到脏数据。FastDDS通过QoS中的DataSharing配置和History深度来协调这一点。配置DATA-SHARING需要在Writer和Reader两端都做设置!-- DataWriter QoS -- data_writer profile_namesharing_writer qos data_sharing kindAUTOMATIC/kind /data_sharing history kindKEEP_LAST/kind depth10/depth /history /qos /data_writer !-- DataReader QoS -- data_reader profile_namesharing_reader qos data_sharing kindAUTOMATIC/kind /data_sharing history kindKEEP_LAST/kind depth10/depth /history /qos /data_readerkind可以设为AUTOMATIC、ON或OFF。AUTOMATIC表示如果条件满足就启用否则回退ON是强制启用条件不满足会报错OFF是禁用。生产环境建议用AUTOMATIC给系统留回退余地。3.3 两者在序列化与反序列化上的开销差异SHM传输层和DATA-SHARING在序列化上的处理方式不同这是影响性能的关键因素之一。SHM传输层走的是标准流程Writer端把数据对象序列化成字节流写入共享内存缓冲区Reader端从缓冲区读出字节流反序列化成数据对象。序列化和反序列化的开销取决于IDL类型和序列化实现对于复杂嵌套结构这部分开销可能占总延迟的30%以上。DATA-SHARING则不同。Writer端仍然需要序列化一次因为共享内存池里存的是序列化后的数据但Reader端可以直接读取序列化数据在某些实现中甚至可以做到零反序列化——如果Reader只是转发数据而不需要访问具体字段的话。不过大多数场景下Reader还是要反序列化才能使用数据所以节省的主要是“传输”环节的开销而不是序列化本身。实测数据表明对于1KB左右的小消息SHM和DATA-SHARING的端到端延迟差异在10%以内但对于100KB以上的大消息DATA-SHARING的优势就非常明显了延迟可以降低40%到60%因为省掉了共享内存缓冲区的写入和读出两次拷贝。4. 实操配置与性能实测过程4.1 测试环境搭建与基准配置为了给出有参考价值的对比数据我搭了一套测试环境。硬件是一台工作站配置为Intel i7-12700K、64GB DDR4、NVMe SSD操作系统是Ubuntu 22.04内核版本5.15。FastDDS版本用的是2.14编译时开启了SHM和DATA-SHARING支持。测试程序用FastDDS自带的HelloWorld示例改造Publisher和Subscriber跑在同一台机器上通过命令行参数控制消息大小和发送频率。消息类型用一个固定大小的结构体包含一个序列号、一个时间戳和一个可变长度的字节数组用来模拟不同大小的载荷。测试指标有三个端到端延迟从Writer调用write到Reader收到数据、CPU占用率用pidstat采集、吞吐量每秒成功传输的消息数。每组配置跑10次取中位数避免偶然波动。基准配置先用默认的UDP传输层跑一遍作为对照。然后分别启用SHM传输层和DATA-SHARING保持其他QoS一致RELIABLE、KEEP_LAST depth10、同步发布。4.2 SHM传输层的配置与实测数据启用SHM传输层需要在Participant的配置里显式添加SHM传输描述符并调整缓冲区参数。我的配置如下participant profile_nameshm_participant rtps builtin transport_descriptor transport_idshm/transport_id typeSHM/type maxMessageSize2097152/maxMessageSize segment_size8388608/segment_size port_queue_capacity1024/port_queue_capacity /transport_descriptor /builtin useBuiltinTransportsfalse/useBuiltinTransports userTransports transport_idshm/transport_id /userTransports /rtps /participant这里把maxMessageSize设成2MBsegment_size设成8MBport_queue_capacity设成1024是为了覆盖大消息和高频场景。useBuiltinTransports设为false只保留SHM避免UDP干扰测试结果。实测数据消息大小1KB发送频率10000Hz指标UDPSHM平均延迟85μs42μsP99延迟210μs95μsCPU占用18%11%吞吐量9200 msg/s9800 msg/s延迟降低了一半左右CPU占用也明显下降因为省掉了内核协议栈的处理。吞吐量提升不大因为1KB消息本身就不大瓶颈不在传输上。换成100KB消息后差异更明显指标UDPSHM平均延迟1.2ms0.45msP99延迟3.5ms1.1msCPU占用35%19%吞吐量780 msg/s2100 msg/s大消息场景下SHM的优势就体现出来了吞吐量翻了近三倍。4.3 DATA-SHARING的配置与实测数据DATA-SHARING的配置重点在Writer和Reader的QoS匹配上。我用了AUTOMATIC模式让FastDDS自己判断是否启用。关键配置如下data_writer profile_nameds_writer qos data_sharing kindAUTOMATIC/kind /data_sharing history kindKEEP_LAST/kind depth20/depth /history reliability kindRELIABLE/kind /reliability /qos /data_writerReader端配置对称depth也设成20确保Reader能匹配到Writer的历史样本。这里有个细节如果Reader的depth小于Writer的且Reader启动较晚可能会因为样本已被覆盖而回退到SHM。所以两边depth最好一致或者Reader的更大。实测数据消息大小1KB发送频率10000Hz指标SHMDATA-SHARING平均延迟42μs28μsP99延迟95μs52μsCPU占用11%7%吞吐量9800 msg/s9950 msg/s小消息场景下DATA-SHARING比SHM又快了30%左右主要省掉的是共享内存缓冲区的写入和读出开销。100KB消息场景指标SHMDATA-SHARING平均延迟0.45ms0.18msP99延迟1.1ms0.42msCPU占用19%9%吞吐量2100 msg/s4800 msg/s大消息场景下DATA-SHARING的吞吐量是SHM的两倍多延迟降到三分之一以下。这个差距主要来自零拷贝SHM需要把100KB数据写入缓冲区再读出而DATA-SHARING直接让Reader读Writer的缓存。4.4 多Reader场景下的表现差异DATA-SHARING的真正优势在多Reader场景下才完全显现。我加了一个测试一个Writer对应四个Reader都跑在同一主机上消息大小100KB。SHM传输层下Writer需要把数据分别写入四个Reader的共享内存缓冲区或者用多播机制但SHM的多播支持有限CPU占用和延迟都会随Reader数量线性增长。实测四个Reader时平均延迟从0.45ms涨到1.6msCPU占用从19%涨到48%。DATA-SHARING下Writer只写一份数据到共享内存池四个Reader各自去读Writer侧的开销几乎不变。实测平均延迟只从0.18ms涨到0.25msCPU占用从9%涨到14%。这个扩展性差距是数量级的。实操心得如果你的系统里有多个订阅者消费同一份大消息比如感知模块的输出被规划、控制、记录多个模块订阅DATA-SHARING几乎是唯一的选择。用SHM的话Writer侧会成为瓶颈。5. 常见问题与排查技巧实录5.1 SHM传输层数据发不出去或收不到这是最常见的问题表现是Writer调用write返回成功但Reader一直收不到数据。排查思路按以下顺序来第一检查/dev/shm空间。用df -h /dev/shm看剩余空间如果接近满SHM段创建会失败。清理一下残留的段文件rm /dev/shm/fastrtps_shm_*或者调大共享内存上限。第二检查Participant的传输配置。如果useBuiltinTransports设成了false但userTransports里没有正确添加SHMParticipant就只剩UDP或者没有传输层。用FASTDDS_LOG_LEVELInfo跑一遍看日志里有没有“SHM transport created”之类的信息。第三检查端口冲突。同一台机器上跑多个Domain时如果Participant ID分配冲突SHM端口会撞车。可以显式指定Participant ID或者让FastDDS自动分配。第四检查防火墙或安全软件。有些安全软件会拦截共享内存的创建虽然少见但确实遇到过。5.2 DATA-SHARING回退到SHM的识别与处理DATA-SHARING配置了AUTOMATIC后如果条件不满足会静默回退到SHM或UDP性能会下降但功能正常所以很多人不知道发生了回退。识别方法是在Reader端打印实际使用的传输方式或者看FastDDS的统计信息。常见的回退原因有Reader的data_sharing配置为OFF或者Writer和Reader的配置不匹配Reader的History depth小于Writer的且Reader启动晚于Writer导致需要的样本已被覆盖消息类型不支持零拷贝比如包含指针或动态分配的类型跨主机通信这是硬性限制无法避免处理办法确保Writer和Reader的data_sharing都设为AUTOMATIC或ONHistory depth一致且足够大消息类型用固定大小的IDL结构。如果确实需要跨主机那就接受回退或者用SHM传输层作为跨主机场景的补充。5.3 内存泄漏与段文件残留FastDDS在异常退出时比如kill -9共享内存段文件可能残留在/dev/shm里下次启动时如果端口号相同可能会读到脏数据或者创建失败。这个问题在开发阶段特别烦人因为频繁重启程序。解决办法有两个一是程序里注册信号处理在退出时正常关闭Participant二是启动脚本里加清理逻辑在启动前删除残留的段文件。生产环境建议两者都做。#!/bin/bash # 启动前清理残留的SHM段文件 rm -f /dev/shm/fastrtps_shm_* 2/dev/null # 启动应用 ./my_dds_app注意清理段文件时要确保没有其他正常运行的FastDDS进程在使用这些段否则会导致那些进程崩溃。多进程部署时清理逻辑要更精细比如按Domain ID过滤。5.4 性能不达预期的排查清单如果配置了SHM或DATA-SHARING但性能没有明显提升按以下清单逐项检查检查项可能问题处理方式传输层是否生效配置未加载或回退看日志确认实际传输方式消息大小小消息收益有限小消息优先优化序列化History depth太小导致频繁覆盖调大depth减少回退CPU亲和性Writer和Reader在不同NUMA节点绑定到同一NUMA节点内存对齐未对齐导致额外拷贝用alignas指定对齐序列化方式默认序列化开销大考虑用Fast CDR的优化选项NUMA这个问题特别隐蔽。如果Writer和Reader跑在同一台多路服务器上但分属不同CPU插槽共享内存的访问延迟会显著增加甚至比UDP还慢。用numactl --hardware看拓扑然后用numactl --cpunodebind0 --membind0把相关进程绑到同一节点。6. 选型建议与个人实操体会6.1 按场景选型的决策路径经过上面这些测试和踩坑我总结了一个简单的决策路径如果你的通信全部在同一主机内且消息大小超过10KB优先考虑DATA-SHARING。它的零拷贝特性和多Reader扩展性是大消息场景的最优解。如果消息较小1KB以下但频率极高SHM传输层和DATA-SHARING差距不大选SHM更简单配置少、回退路径清晰。如果存在跨主机通信两者都用不了老老实实优化UDP配置或者用SHM处理本机部分、UDP处理跨机部分做混合传输。如果Reader数量多且消息大DATA-SHARING是唯一合理的选择SHM在Writer侧会成为瓶颈。如果对稳定性要求极高、不想引入复杂配置SHM传输层更成熟出问题也更容易排查。6.2 混合部署的配置策略实际项目里很少是纯本机或纯跨机通常是混合的。比如一个自动驾驶系统感知和规划在同一台计算单元上但和车辆控制单元是跨主机的。这种场景下可以同时启用SHM和UDP传输层让FastDDS根据目标地址自动选择。配置上在Participant里同时添加SHM和UDP传输描述符useBuiltinTransports设为falseuserTransports里列出两者。FastDDS会优先尝试SHM目标不在本机时回退到UDP。DATA-SHARING则作为额外的优化层在SHM基础上进一步减少拷贝。userTransports transport_idshm/transport_id transport_idudp/transport_id /userTransports这种混合配置的实测表现是本机通信走SHM或DATA-SHARING延迟在百微秒级跨机通信走UDP延迟在毫秒级。整体系统延迟由最慢的链路决定所以跨机部分的优化同样重要。6.3 我踩过的几个坑第一个坑是容器里的/dev/shm限制。早期在Docker里跑FastDDS默认64MB的共享内存根本不够用程序跑几分钟就报共享内存分配失败。后来在docker run里加了--shm-size1g才解决。如果你用Kubernetes记得在Pod的securityContext里配置emptyDir的medium为Memory并设置足够的大小。第二个坑是DATA-SHARING的History depth不匹配。Writer设了depth10Reader设了depth5结果Reader启动稍晚就回退到SHM性能直接掉一半。后来统一设成20问题消失。这个坑的隐蔽性在于功能正常只是性能不达预期不看日志根本发现不了。第三个坑是NUMA绑定。在一台双路服务器上Writer跑在CPU0、Reader跑在CPU1共享内存访问跨了NUMA节点延迟比预期高了3倍。用numactl绑定后恢复正常。这个坑在单路机器上遇不到但多路服务器上很常见。第四个坑是消息类型的零拷贝支持。一开始用了一个包含std::string的IDL类型DATA-SHARING死活不生效一直回退。后来改成固定大小的char数组才启用成功。FastDDS的零拷贝对类型有要求动态分配的类型不支持这个在文档里写得比较隐晦需要自己试出来。6.4 后续可以继续深挖的方向这套测试跑下来还有几个方向值得继续研究。一是DATA-SHARING在Writer和Reader数量动态变化时的行为比如Reader中途加入或退出共享内存池如何回收和重整。二是SHM传输层在多Domain场景下的端口分配策略如何避免冲突同时保证性能。三是结合FastDDS的统计模块做更细粒度的性能剖析比如区分序列化、传输、反序列化各占多少时间。另外FastDDS的版本迭代比较快2.14和2.10在SHM实现上有不少差异升级时要注意回归测试。我一般会在升级前跑一遍基准测试对比延迟和吞吐量确认没有退化再上生产。最后分享一个小技巧调试SHM和DATA-SHARING问题时把FastDDS的日志级别调到Info甚至Debug能看到传输层选择、端口分配、回退原因等关键信息。虽然日志量大但排查问题时比盲猜高效得多。生产环境再调回Warning避免日志影响性能。