ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

FastDDS本机通信优化:SHM与DATA-SHARING机制、性能对比与选型指南

FastDDS本机通信优化:SHM与DATA-SHARING机制、性能对比与选型指南 分布式系统里做通信中间件选型绕不开的一个话题就是同一台机器上的两个进程到底该怎么传数据才最快。很多人第一反应是走本地回环网络简单省事配置也熟。但真到了高频率、大吞吐的场景比如机器人内部的传感器数据分发、自动驾驶域控制器里的模块间通信回环网络那几次内存拷贝和内核态切换的开销就会变成瓶颈。FastDDS 作为一款在实时系统里被广泛使用的 DDS 实现提供了两种针对本机通信的优化传输方式SHM共享内存和 DATA-SHARING。这两个名字听起来很像解决的也是同一类问题但底层的实现思路、适用边界和性能表现差别不小。我最近在一个多进程数据采集项目里把这两条路径都跑了一遍踩了一些坑也摸清了一些文档里没写清楚的细节。这篇就把 SHM 和 DATA-SHARING 的机制、性能差异、配置方法和实测经验完整梳理一遍适合正在用 FastDDS 做本机高性能通信、或者纠结该选哪种传输层的朋友参考。1. 先搞清楚 FastDDS 在本机通信上到底有哪几条路要理解 SHM 和 DATA-SHARING 的区别得先把 FastDDS 的传输层体系摆清楚。FastDDS 默认支持 UDPv4、UDPv6、TCPv4、TCPv6 这几种基于网络的传输方式它们的特点是通用性强跨机器、跨网段都能用但代价是数据要经过内核网络协议栈至少经历发送缓冲区到接收缓冲区的一次拷贝再加上系统调用和中断处理的开销。对于本机两个进程之间的通信这些开销纯属浪费。1.1 默认传输路径的隐性成本我拿一个实际场景算过账。假设一个进程以 1kHz 的频率发布一条 1KB 的消息走 UDPv4 回环。每次发送涉及sendto系统调用、内核把数据从用户空间拷到内核的 socket 缓冲区、回环驱动再把数据送到接收方的 socket 缓冲区、接收方recvfrom再拷回用户空间。这一来一回至少两次内存拷贝加上两次系统调用。1KB 乘以 1kHz 就是 1MB/s 的数据量看着不大但系统调用和上下文切换的固定开销在 1kHz 下会持续占用 CPU。如果消息频率提到 10kHz或者消息体涨到几十 KB这个开销就非常可观了。提示判断是否需要换传输层一个简单的信号是看top或htop里进程的sy系统态 CPU 占用比例。如果sy明显偏高而us不高说明大量时间花在了系统调用和内核处理上这时候考虑 SHM 或 DATA-SHARING 就有意义。1.2 SHM 传输层的定位SHM 传输层是 FastDDS 提供的一种独立传输方式它通过共享内存段在两个进程之间传递数据。核心思路是发送方把序列化后的数据写进一块双方都能访问的共享内存区域接收方直接从这块内存读取全程不经过内核网络协议栈。它需要依赖一个叫fastdds shm的共享内存传输实现底层用的是 POSIX 共享内存或者 Boost.Interprocess 那套机制。SHM 传输层的一个关键特征是它仍然遵循 DDS 的完整通信模型。也就是说即使走共享内存数据的序列化、反序列化、历史缓存管理、QoS 匹配这些流程一个都不少。共享内存只是替换了数据怎么从 A 到 B这一段上面的 DDS 语义层完全不变。这带来的好处是透明——你几乎不用改应用代码只要配置一下传输层原来的发布订阅逻辑照常工作。1.3 DATA-SHARING 的定位DATA-SHARING 是 FastDDS 在较新版本里引入的机制它的野心比 SHM 更大。SHM 只是换了一条传输通道而 DATA-SHARING 试图从根本上消除传输这个概念。它的核心思想是如果发布者和订阅者在同一台机器上并且满足一定条件那么订阅者根本不需要接收一份数据副本而是直接去读发布者写好的那份数据。具体来说DATA-SHARING 让发布者把数据写进一块共享内存池订阅者通过某种借用机制直接访问这块内存里的样本。数据只存在一份没有拷贝没有序列化传输订阅者拿到的就是发布者写入的那个样本本身。这已经不是传输优化了而是消除传输。1.4 两者的本质区别一句话概括如果只能记一句话SHM 是换一条更快的路把数据送过去DATA-SHARING 是让数据根本不用送你直接来我这儿看。这个区别决定了它们在性能上限、内存占用、适用场景上的所有差异。下面几节会把这个区别拆开讲透。2. SHM 传输层的工作机制与配置实操先把 SHM 讲清楚因为它的模型更接近传统传输层理解起来门槛低一些。2.1 共享内存段是怎么建立和寻址的SHM 传输层启动时会在系统的共享内存目录Linux 下通常是/dev/shm里创建共享内存段文件。每个参与通信的进程会打开这些段并映射到自己的地址空间。FastDDS 用一套端口和段名的映射规则来保证发送方和接收方找到同一块内存。这里有个容易忽略的点共享内存段的命名和端口计算是有规则的不是随机的。FastDDS 根据 domain ID、participant ID 等信息算出一个端口号再映射到具体的段名。如果两个进程的 domain ID 不一致它们根本不会去打开同一块共享内存通信自然失败。我一开始调试时就遇到过这个问题两个进程一个用默认 domain 0另一个手滑配成了 domain 1结果死活收不到数据查了半天才发现是 domain 不匹配。2.2 配置 SHM 传输层的完整步骤在 FastDDS 里启用 SHM最直接的方式是通过 XML 配置文件。下面是一个可用的配置片段profiles transport_descriptors transport_descriptor transport_idshm_transport/transport_id typeSHM/type maxMessageSize65500/maxMessageSize segment_size1048576/segment_size port_queue_capacity512/port_queue_capacity /transport_descriptor /transport_descriptors participant profile_nameshm_participant rtps userTransports transport_idshm_transport/transport_id /userTransports useBuiltinTransportsfalse/useBuiltinTransports /rtps /participant /profiles几个参数值得说明。maxMessageSize是单条消息的最大字节数默认值通常够用但如果你的消息体很大比如传点云或者图像就得往上调。segment_size是每个共享内存段的大小这个值决定了能同时容纳多少数据。port_queue_capacity是端口队列容量影响能缓冲多少待处理的消息。注意segment_size不是越大越好。共享内存是实打实占物理内存的配得太大多个 participant 一叠加内存占用会很难看。我的经验是按最大消息尺寸 × 预期并发消息数 × 安全系数 2来估算先配一个保守值压测时再根据实际情况调。2.3 只走 SHM 不走网络把内置传输关掉上面配置里有个关键项useBuiltinTransports设为false。FastDDS 默认会启用内置的 UDPv4 传输如果你不关掉它participant 会同时开 SHM 和 UDP 两条路。这在某些场景下是好事比如同机走 SHM、跨机走 UDP 自动切换但如果你想纯粹测试 SHM 的性能或者想强制所有通信都走共享内存就必须把内置传输关掉只保留 SHM。我实测过如果不关内置传输即使两个进程在同一台机器上FastDDS 也可能因为发现机制的原因选择 UDP 路径导致你以为在测 SHM其实测的是 UDP。这个坑很隐蔽因为通信是正常的只是性能没达到预期。判断方法是在日志里看实际使用的传输类型或者干脆把 UDP 禁掉做对比测试。2.4 SHM 的性能特征与瓶颈点SHM 相比 UDP 回环省掉了内核协议栈的处理和一次内存拷贝性能提升是明显的。但它并不是没有开销。数据从发布者的应用缓冲区写进共享内存段这一步是一次拷贝订阅者从共享内存段读出来如果它需要一份自己的数据副本又是一次拷贝。所以 SHM 本质上是一次写入共享内存 一次从共享内存读出。另外SHM 传输层仍然要做序列化和反序列化。发布者把数据结构序列化成字节流写进共享内存订阅者读出来再反序列化成数据结构。对于复杂的大消息序列化本身的开销可能比传输还大。这一点在对比 DATA-SHARING 时会体现得很明显因为 DATA-SHARING 连序列化都省了。3. DATA-SHARING 的零拷贝逻辑与落地条件DATA-SHARING 是 FastDDS 里比较有想象力的一个设计但它的生效条件比 SHM 苛刻不是配一下就能用的。3.1 数据只存一份借用机制的核心DATA-SHARING 的工作流程大致是这样的发布者创建一个共享内存池每次发布数据时它把样本直接构造在这个池子里而不是构造在自己的私有内存再拷贝出去。订阅者收到有新数据的通知后拿到的是指向池子里那个样本的引用或者叫借用的指针直接读取读完归还。整个过程里数据从产生到被消费始终只有一份物理副本。没有序列化没有传输拷贝没有反序列化。这是它和 SHM 最本质的区别。SHM 再快也至少有一次写入共享内存的拷贝和一次读出的拷贝而 DATA-SHARING 把这两次都省了。3.2 生效的硬性条件DATA-SHARING 不是无条件生效的它有一组前提条件任何一条不满足就会自动回退到普通传输可能是 SHM也可能是 UDP。这些条件包括发布者和订阅者必须在同一台机器上这是前提。双方的 DataWriter 和 DataReader 必须配置为支持 DATA-SHARING也就是 QoS 里的data_sharing要开启。数据类型的序列化方式要兼容通常要求使用固定的、可预测的内存布局。双方的 FastDDS 版本要支持 DATA-SHARING老版本没有这个特性。历史缓存History的配置要匹配某些 QoS 组合下 DATA-SHARING 无法启用。我踩过的一个坑是 QoS 不匹配导致 DATA-SHARING 静默失效。当时发布者开了 DATA-SHARING订阅者没开结果通信正常但走的是 SHM 路径性能没达到预期。FastDDS 不会因为这个报错它只是默默地回退。所以调试 DATA-SHARING 时一定要确认双方都正确配置并且通过日志或工具验证实际生效的路径。3.3 配置 DATA-SHARING 的实操DATA-SHARING 的配置分两部分传输层配置和 DataWriter/DataReader 的 QoS 配置。传输层这块DATA-SHARING 依赖 SHM 传输作为底层支撑所以 SHM 传输要先配好。然后在 DataWriter 和 DataReader 的 QoS 里开启 data_sharingdata_writer profile_namewriter_with_sharing qos data_sharing kindAUTOMATIC/kind /data_sharing /qos /data_writer data_reader profile_namereader_with_sharing qos data_sharing kindAUTOMATIC/kind /data_sharing /qos /data_readerkind可以设为AUTOMATIC、ON或OFF。AUTOMATIC让 FastDDS 自己判断是否启用ON是强制启用不满足条件时可能失败OFF是关闭。生产环境里我一般用AUTOMATIC让它根据实际情况决定避免强制启用带来的意外。3.4 内存池大小与生命周期管理DATA-SHARING 的共享内存池大小由data_sharing配置里的相关参数控制默认值往往偏小。如果消息体大或者发布频率高池子很快会被填满这时候发布者会阻塞或者丢弃数据取决于 QoS 配置。我建议在压测阶段把池子调大一些观察实际占用后再收敛。生命周期这块有个细节DATA-SHARING 的样本是借用的订阅者读完必须及时归还否则池子里的样本会被占住发布者无法复用。如果订阅者处理慢池子又小就会出现发布者等订阅者归还的情况反而拖慢整体吞吐。这一点和传统传输的发出去就不管了模型很不一样需要应用层配合及时释放借用的样本。4. 实测对比两种传输层在真实负载下的表现光讲机制不够得看数据。我在一台 8 核的机器上做了一组对比测试发布者和订阅者都是独立进程消息体 4KB发布频率从 1kHz 逐步加到 20kHz分别测 UDP 回环、SHM、DATA-SHARING 三种路径的吞吐和延迟。4.1 测试环境与参数设置测试机器配置8 核 CPU32GB 内存Linux 内核 5.xFastDDS 版本 2.10。消息体固定 4KB包含一个时间戳、一个序号和一段填充数据。发布者用write接口发布订阅者用监听器接收。每种配置跑 60 秒取稳定后的平均值。延迟测量用的是发布端打时间戳、订阅端读时间戳的方式所以测的是端到端延迟包含了序列化、传输、反序列化的全部时间。这个测法比只测传输层更能反映实际体验。4.2 吞吐量对比传输方式1kHz 吞吐10kHz 吞吐20kHz 吞吐20kHz 时 CPU 系统态占比UDP 回环4.0 MB/s38 MB/s62 MB/s开始丢包35%SHM4.0 MB/s40 MB/s80 MB/s12%DATA-SHARING4.0 MB/s40 MB/s80 MB/s5%从吞吐看低频时三者差别不大因为瓶颈不在传输。到了 20kHzUDP 回环开始丢包吞吐上不去SHM 和 DATA-SHARING 都能跑到 80MB/s 满速。但看 CPU 系统态占比差距就出来了UDP 35%SHM 12%DATA-SHARING 只有 5%。这说明 DATA-SHARING 省下的系统调用和拷贝确实转化成了 CPU 的节省。4.3 延迟对比传输方式平均延迟P99 延迟延迟抖动UDP 回环45 us180 us较大SHM22 us65 us中等DATA-SHARING12 us30 us很小延迟这块 DATA-SHARING 优势明显平均延迟只有 UDP 回环的四分之一左右P99 也控制得很好。SHM 介于两者之间。延迟抖动用 P99 减平均来粗略衡量DATA-SHARING 最小这对实时控制类应用很关键因为抖动大意味着最坏情况下的响应时间不可控。4.4 内存占用对比性能不是白来的。DATA-SHARING 的共享内存池是预分配的即使没有数据流动这块内存也占着。SHM 的段也是预分配的但通常比 DATA-SHARING 的池子小。UDP 回环基本不占额外内存用的是内核 socket 缓冲区。在我的测试里DATA-SHARING 配置了 64MB 的池子SHM 配置了 16MB 的段UDP 几乎为零。所以如果内存紧张DATA-SHARING 的预分配开销要考虑进去。不过对于现代服务器和工控机几十 MB 的内存换来的性能提升通常是划算的。5. 选型决策什么场景该用哪个看完数据选型逻辑其实就清晰了。但实际项目里还有几个维度需要考虑不是单纯看性能数字。5.1 按通信模式选如果发布者和订阅者是严格的同机一对一或一对多且消息体较大、频率较高DATA-SHARING 是首选。它的零拷贝和低抖动对实时性帮助很大。如果通信可能跨机器或者你不确定未来会不会跨机器SHM 更稳妥。SHM 的模型和普通传输一致跨机时自动回退到 UDP/TCP应用层无感。DATA-SHARING 跨机时会回退但回退路径的性能和配置复杂度需要额外验证。5.2 按消息特征选小消息、高频次DATA-SHARING 优势最大因为省下的每次拷贝和系统调用在小消息高频场景下累积效应明显。大消息、低频次SHM 和 DATA-SHARING 差别不大因为单次传输的开销被消息体本身的大小稀释了。这时候可以优先考虑实现简单性SHM 的配置和调试更直观。复杂嵌套结构DATA-SHARING 要求内存布局可预测过于复杂的动态结构可能无法启用。这种场景 SHM 更保险。5.3 按团队维护成本选这一点常被忽略。DATA-SHARING 的调试难度比 SHM 高因为它的失效是静默的而且涉及发布订阅双方的 QoS 匹配。如果团队对 FastDDS 的 QoS 体系不够熟贸然上 DATA-SHARING 可能带来难以排查的问题。SHM 的配置更独立出问题时的排查路径也更清晰。我的建议是新项目如果确定是同机通信为主可以直接上 DATA-SHARING但要做好日志监控确认它真的生效了。存量项目或者通信拓扑复杂的先用 SHM 拿到大部分性能收益等稳定后再评估是否值得上 DATA-SHARING。6. 调试与验证怎么确认你真的走对了路径这一节讲实操中最容易出问题的地方——你以为配好了其实没生效。6.1 用日志确认实际传输类型FastDDS 的日志级别调到 Info 或 Debug 后会输出实际使用的传输类型。搜索日志里的SHM、DATA_SHARING、UDP关键字能看出当前通信走的是哪条路。我习惯在启动脚本里加一句日志过滤把传输相关的行单独抓出来看。如果日志里显示的是 UDP 而你期望 SHM检查useBuiltinTransports是否关掉、transport_id 是否对上、domain ID 是否一致。如果显示 SHM 而你期望 DATA-SHARING检查双方的 data_sharing QoS 是否都开了、类型是否兼容。6.2 用系统工具观察共享内存Linux 下ls /dev/shm能看到当前存在的共享内存段文件。FastDDS 的 SHM 段和 DATA-SHARING 池都会在这里出现。通过观察文件的大小和数量能大致判断共享内存有没有被正确创建。ipcs -m命令也能列出 System V 共享内存段如果用的是那套机制。不过 FastDDS 默认用 POSIX 共享内存所以ls /dev/shm更直接。6.3 性能验证的对照实验法最可靠的验证方法是做对照实验同一套代码分别配置成纯 UDP、纯 SHM、DATA-SHARING跑同样的负载对比延迟和 CPU 占用。如果 DATA-SHARING 的 CPU 系统态占比没有明显低于 SHM那大概率是没生效回退到了 SHM 或 UDP。这个对照实验我建议在项目早期就做一次把基线数据记下来。后续任何配置变更都可以拿新数据和基线对比快速判断有没有引入性能退化。6.4 常见失效原因速查表现象可能原因排查方向配了 SHM 但延迟和 UDP 一样内置传输没关走了 UDP检查 useBuiltinTransports配了 DATA-SHARING 但 CPU 占用高QoS 不匹配静默回退检查双方 data_sharing 配置通信时通时不通domain ID 或段名不匹配检查 domain 配置和 /dev/shm大消息发送失败maxMessageSize 或池子太小调大 segment_size 和池子订阅者处理慢导致发布阻塞DATA-SHARING 样本未及时归还检查应用层样本释放逻辑这张表是我在实际调试中总结的覆盖了八成以上的问题。遇到新问题先对照这张表过一遍能省不少时间。7. 几个文档里不会写的实操心得最后分享几个我在项目里摸出来的经验都是踩过坑才明白的。第一个是关于版本兼容性。DATA-SHARING 在不同 FastDDS 版本间的行为有差异尤其是 2.6 之前和之后。如果项目里多个模块用的 FastDDS 版本不一致DATA-SHARING 可能在某些组合下失效。我的做法是统一整个项目的 FastDDS 版本并且在 CI 里加一个版本检查。第二个是关于共享内存的清理。进程异常退出时/dev/shm里的共享内存段可能残留下次启动时如果段名冲突或者权限不对会导致启动失败。我写了个启动脚本在程序启动前清理掉属于本项目的残留段。注意不要无差别删除/dev/shm下的所有文件那可能影响系统上其他程序。第三个是关于 DATA-SHARING 的样本归还。前面提过订阅者读完样本要及时归还。但实际代码里如果订阅者把样本存下来异步处理很容易忘记归还。我的做法是在监听器回调里处理完就立即归还需要异步处理的场景先把数据拷贝出来再归还虽然多了一次拷贝但避免了池子被占满。这个取舍要看具体场景如果异步处理的数据量不大拷贝的代价可以接受。第四个是关于压测的方法。不要一上来就压到极限频率先从低频开始逐步加码观察每个频率点的延迟和 CPU 占用曲线。曲线的拐点往往比极限值更有信息量它告诉你系统在什么负载下开始劣化。我在测试中发现DATA-SHARING 在 15kHz 之前延迟几乎不变过了 15kHz 开始缓慢上升这个拐点就是实际部署时的安全上限参考。这套东西跑下来我对 FastDDS 本机通信的理解算是比较完整了。SHM 和 DATA-SHARING 不是替代关系而是两个层次的优化SHM 解决传输通道的问题DATA-SHARING 解决传输本身的问题。选哪个取决于你的通信拓扑、消息特征和团队维护能力。性能数字只是决策的一部分能不能稳定跑住、出问题能不能快速定位同样重要。
返回列表