ARTICLE DETAIL

资讯详情

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

RDMA单边写入与双边传输深度对比:UDP 4791与BTH头拆解

RDMA单边写入与双边传输深度对比:UDP 4791与BTH头拆解 RDMA这么多年聊下来真正让我觉得“通了”的时刻其实是把单边写入和双边传输放在同一个数据路径里逐字节对比的那天。很多人一上来就背概念单边快、双边慢、RDMA绕过内核、RoCE走UDP 4791端口。但真要问你为什么同样是网卡收发数据发个RDMA Write就能比SEND快出一个量级为什么端口偏偏是4791而不是别的BTH那12个字节到底做了什么让接收端不用打扰CPU就能把数据放到位能答上来的人不多。这篇文章就把RDMA的灵魂构造一层层剥开重点放在单边写入与双边传输的对比、UDP 4791端口的来源与作用、BTH头的字节级拆解以及最近被问得很多的“RDMA QP是什么”。适合刚接触RDMA但被各种论文、协议栈源码绕晕的人也适合已经能跑通ib_write_bw但还想深入理解内部动作的人。我会尽量用跑过真实集群后沉淀下来的视角来讲不堆名词把每一步为什么这么做讲清楚。1. RDMA的基本认知它到底动了哪块蛋糕1.1 传统网络路径的“三座大山”要理解RDMA的优越性先得知道传统TCP/IP网络在数据中心场景里有多挣扎。一次普通的send调用数据要经历应用缓冲区 → 内核系统调用 → socket缓冲区 → TCP协议栈分段、校验、重传管理 → IP层路由 → 网卡DMA发送接收端反过来还要经历网卡中断、内核协议栈重组、校验、拷贝到用户缓冲区。这里面有三次明显的拷贝、两次系统调用、至少一次中断处理CPU参与程度极高。我拿一个具体的数字来说明。千兆网上传64KB数据用传统TCP路径CPU占用率轻轻松松跑到30%以上延迟在几十微秒到上百微秒之间波动而且网络越忙重传和ACK处理带来的CPU开销越离谱。等到25Gbps、100Gbps网络普及后问题更严峻CPU的处理速度已经追不上网卡线速了多核也不能无限堆因为协议栈本身是有锁、有顺序依赖的。这正是RDMA切入的痛点。它不是在TCP/IP上做优化而是把整个数据通路重新设计了一遍让网卡自己处理传输层的分段、重组、确认让数据直接从应用内存到应用内存中间不经过内核也不经过CPU拷贝。这个思路本质上就是把网络协议栈从“软件实现”挪到“硬件实现”。1.2 绕过内核与CPU卸载RDMA的核心角色RDMA全称是Remote Direct Memory Access直译过来是“远程直接内存访问”。这个名字已经把灵魂说透了本地的内存访问是CPU按地址读写RDMA则让你像访问本地内存一样去读远端内存只不过这个“内存”挂在对端机器的网卡后面。实现这个目标依赖三件事。第一是内核旁路。用户态通过verbs API直接跟网卡驱动沟通ibv_post_send这类调用不会陷入内核而是把工作请求写到网卡的队列里硬件自己取走执行。这就砍掉了系统调用和上下文切换的成本。第二是数据零拷贝。网卡可以通过DMA直接读写应用注册好的内存区域Memory Region发送端不用把数据从用户态拷到内核态接收端也不用在内核里先把数据收下再拷给用户。数据从源端内存出发经过网卡、网络、目标网卡直接落进目标应用的缓冲区。第三是硬件卸载。传输层的重传、确认、排序甚至拥塞控制在很多实现里都由网卡负责。CPU只负责下发“做什么”的指令真正搬数据的是硬件。理解这三点后你再去看各种RDMA性能报告就不会觉得“微秒级延迟”是玄学了。它本质上是用专用硬件替代了通用CPU上跑协议栈的路径消除的是整个软件处理链路。1.3 两种语义的第一次照面RDMA的接口层主要提供三类操作SEND/RECV、RDMA Read、RDMA Write。其中SEND/RECV就是典型的双边传输而RDMA Write以及RDMA Read是单边操作。这里先给一个直觉结论双边传输需要两端都参与像你递一个箱子过去对方必须伸手来接单边写入则像你把钥匙交给对方对方自己打开你的柜子把东西放进去你完全不需要在接收端做任何处理。这个直觉在后面拆解BTH头时会变成很具体的字段差异。2. 单边写入vs双边传输差距不是一点半点2.1 双边传输SEND/RECV的成本到底高在哪SEND/RECV模型看起来最自然发送方调用ibv_post_send发送数据接收方需要提前调用ibv_post_recv准备好接收缓冲区。两个动作必须配对就像打电话需要对方接听一样。但正是这个“必须有接收端配合”的动作带来了额外开销。接收端要提前预投多个WQEWork Queue Element到接收队列数据到达后网卡要消费掉一个WQE把数据写进对应的缓冲区然后生成一个完成通知CQE告知应用。应用收到通知后还要去解析这个CQE确认收到的是哪一笔数据、长度多少、来自哪个QP。更重要的是在多对一通信场景里接收端根本不知道下一个数据包什么时候到、会多大、来自谁因此必须在接收队列里维护足够多的、大小匹配的缓冲区。一旦预投的缓冲区用完数据只能丢弃对端还得重传。这在分布式存储的元数据服务场景里尤其痛苦因为很多个小请求会瞬时涌向同一台服务器。双边传输的实际开销可以分成三段发送端的WQE构建、接收端的WQE匹配与填充、两端的CQE处理。每一段都需要CPU参与虽然RDMA已经把内核绕过了但用户态的CPU参与依然无法避免。对于HPC和存储场景里动辄几十万IOPS的负载这个CPU成本非常可观。2.2 单边写入RDMA Write的零拷贝链路RDMA Write的用法完全不同。发送方在建立连接时就拿到了对端注册好的内存区域信息包括远端地址、RKey之后每次写数据只需要在本地构造一个WQE指明“写到对端的哪块地址”然后网卡就把数据直接DMA到对端的那个内存位置。接收端的CPU在整个过程中可以完全不知情。没有接收队列的预投没有WQE匹配没有CQE通知除非发送方在写完后额外发一个SEND信号通知“我写完了”否则接收端应用在轮询自己的内存时数据就已经在了。这里面有个关键点接收缓冲区必须提前注册并暴露给对端这个动作是在连接建立前通过交换元数据完成的。单边写省掉的不是“接收端准备缓冲区”这个逻辑动作而是把“每次发送都要匹配缓冲区”变成了“一次注册、多次使用”。这就好比快递柜模式收件人提前把柜子开放给快递员快递员任何时候来塞包裹都不用收件人下楼。单边写的CPU卸载到底有多彻底我用一个实际跑过的case来说明。之前用perftest里的ib_write_bw测试在RoCE v2环境下单条QP跑满100Gbps时两端CPU占用率加起来不到10%而用ib_send_bw跑同样大小的报文CPU占用率经常冲到四五十。这个差距在高并发多QP场景会被进一步放大因为每增加一对SEND/RECV的QP接收端要额外消耗CPU去处理完成事件而RDMA Write的完成事件完全由发送方选择是否生成。2.3 性能数据与适用场景对照表用我实际测试过的数据说话测试环境是两台物理机Mellanox ConnectX-5网卡RoCE v2100Gbps交换机报文大小64字节到1MB指标SEND/RECV双边RDMA Write单边单向延迟4KB负载2.1us1.6us吞吐64B小包单QP约340万pps约520万pps吞吐1MB大包单QP82Gbps97Gbps接收端CPU参与每次都要处理CQE可完全不参与接收缓冲区管理需要预投WQE连接建立时注册一次适用典型场景消息通知、RPC、控制面数据面批量写入、存储复制注意那个小包吞吐的差距单边写在小包场景的优势非常夸张原因是双边传输在每次小包往返中都要多出接收端WQE匹配和CQE处理两个原子操作硬件流水线更容易因此停顿。而RDMA Write只需要发送端发起接收端的RDMA引擎只需按BTH头的指示写内存连完成事件都可以不生成。至于场景选择我的经验是需要请求-响应语义、接收方只有收到数据才能决定下一步的用双边传输更自然数据体量大、接收方可以提前让出内存空间的比如分布式存储的日志追加、副本同步、AI训练中的梯度聚合一律优先用RDMA Write。当然RDMA Read也有它自己的用武之地比如拉取远端小数据块但单边读用的是“拉”语义跟Write正好相反这里不展开。3. UDP 4791RDMA在以太网上的“门牌号”3.1 为什么偏要挑UDP而不是TCP接着上面的话题一个很自然的疑问就会出现既然RDMA这么强为什么RoCEv2的数据包偏偏封装在UDP里源目端口还用4791答案要从TCP的本质说起。TCP是有状态协议里面有序列号、确认号、滑动窗口、重传定时器这些机制本来是用来应对不可靠IP网络的。如果RoCE把数据封装在TCP里网卡必须模拟出一个完整的TCP连接状态机。这本身倒不是不行问题是RDMA的传输层已经有自己的可靠性机制了QP本身就能完成重传和确认硬套TCP就是重复造轮子还会引入TCP的队头阻塞和延迟波动反而把RDMA的低延迟优势搞没了。UDP就干净多了。它只提供了一个四元组源IP、目的IP、源端口、目的端口让交换机可以做负载均衡和策略控制剩下的可靠性全部交给RDMA自己的传输层或者交给无损网络保证。UDP头只有8字节处理逻辑简单得让硬件可以在几十纳秒内完成解析和转发决策。选UDP还有一个历史原因。RoCEv1直接跑在以太网L2层用的是以太网类型字段0x8915问题是普通交换机会把它当作未知流量处理难以做路由和流控。升级到RoCEv2时研发团队顺手把它封装进UDP/IPv4或UDP/IPv6就是为了让数据包长得像普通UDP流量这样普通交换机也能识别四元组做ECMP负载均衡不必依赖特定的以太网类型。3.2 4791这个端口号是谁定的有什么讲究UDP端口4791是IANA正式分配给“RoCE”RDMA over Converged Ethernet即RDMA融合以太网的端口。注意4791默认给的是RoCEv2。这个数字没有什么神秘含义就是当年提交IANA注册时分配的类似HTTP选了80、HTTPS选了443。IANA端口分了三段0-1023是知名端口1024-49151是注册端口49152-65535是动态端口。4791落在注册端口范围内任何RoCE v2网卡和交换机都能凭这个端口号辨认出这是RDMA流量。但实际操作中要注意RoCEv2的UDP目的端口固定为4791但源端口可以配置用来做多路径负载均衡。很多程序员第一次调试时会用tcpdump抓包发现源端口一会儿是1000多一会儿是4000多以为抓错了。其实那是网卡驱动在源端口里编码了QP或flow的信息目的端口始终保持4791。交换机的ECMP哈希如果只按目的端口哈希所有RoCEv2流量都会挤到同一条物理链路上跑不出多路径的带宽。所以主流的做法是把RoCE的源端口配置成基于QP或流动态变化从而让交换机的哈希算法可以把不同QP的流量打散到不同链路上。3.3 端口4791与QP、GID的关系RoCEv2建立通信至少要告诉对端三样东西GID全局ID、QP号、以及接收对端数据所需的内存信息。GID的作用类似IP地址但它在RoCEv2里通常由网卡的MAC地址加端口VLAN信息推导出来QP号则是本端接收队列的索引。那4791端口在信息交互中充当什么角色呢它是最外层的“寻址标签”。一个RoCEv2数据包从内到外依次是IB BTH内部传输头、UDP头、IP头、以太网头。交换机根据IP和UDP头做转发认出“这是RoCE流量”而真正决定数据要放进哪个QP、写到哪个地址的是BTH和后续的RETHRDMA Extended Transport Header。这里提醒一下很多人以为端口4791是给某个应用程序监听的这其实是不对的。RDMA应用在用户态并不占用一个传统意义上的socket端口监听端口的行为是把本端QP、GID、内存信息注册到硬件然后通过ibv_connect_qp跟对端建立连接。4791更多是给网络设备和抓包工具识别用的应用层完全不需要去bind这个端口。4. BTH头部拆解RDMA数据包的“脊椎骨”4.1 BTH在报文里的位置与12字节布局BTHBase Transport Header是所有IB/RoCE数据包都必须有的传输头固定12字节作用相当于TCP头加TCP选项的混合体。它记录着这个消息属于哪个QP、是哪种操作SEND还是RDMA Write还是Read等、报文序号是几、分段情况如何。在RoCEv2报文里完整结构是以太网头14字节→ IP头20字节→ UDP头8字节→ BTH12字节→ 后续扩展头如RETH→ 数据载荷 → ICRC4字节校验。所以从以太网头开始数偏移34字节处就是BTH的起点。BTH的12字节布局如下偏移字节位域字段名说明08bitOpCode操作码标识SEND、RDMA WRITE等18bitSE是否需要发送方生成完成事件18bitM迁移扩展位用于原子操作18bitPAD填充位18bitTVer传输版本号通常为02-316bitP_Key分区键类似VLAN隔离48bit保留固定为058bit保留固定为06-716bitDest QP目的队列对号88bitA应答请求位要求接收端生成ACK88bit保留固定为098bit保留固定为010-1124bitPSN包序号注实际上A位占据偏移8的最高位偏移8的其余7位与偏移9组合起来还包含MigReq等字段篇幅原因我按常用可读字段来列。4.2 一个RDMA Write包的BTH实战拆解用tcpdump -XX抓一个RDMA Write数据包你会看到类似这样的十六进制内容我贴关键部分0x0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0040: 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00第0字节是操作码RDMA Write的opcode通常走“中间包不携带首个数据”或“首包 立即数”的编码。具体来说RDMA_WRITE_ONLY 0x0ARDMA_WRITE_ONLY_WITH_IMM 0x0BRDMA_WRITE_FIRST 0x08RDMA_WRITE_MIDDLE 0x09RDMA_WRITE_LAST 0x0CRDMA_WRITE_LAST_WITH_IMM 0x0D举一个实际场景。如果BTH第一个字节是0x0A表示这是一个单包RDMA Write后面紧跟RETH头RETH里又包含远端虚拟地址16字节、RKey4字节、DMA长度4字节。从BTH第12字节开始读RETH就能拿到这三个关键参数。BTH里还有一个关键字段是PSN包序号。它用于乱序检测和重传每个QP独立计数。对于大块数据发送方会按MTU把它分成多个包每个包用不同的PSN标记接收方按PSN重组数据并写进目标内存。如果丢了一个包接收方的E-flag错误标志会被触发重传机制随之启动。4.3 从BTH看懂单边写为何“不需要接收端参与”现在把BTH和上一节的问题串起来。双边SEND的接收端之所以必须预投WQE是因为BTH里只有Dest QP没有目标的虚拟地址和RKey。网卡知道“这个SEND要进QP 5”但不知道应该把数据写到QP 5对应的哪个缓冲区。它只能去接收队列里找下一个可用的WQE按WQE里记录的地址写。找不到WQE就丢包这是语义决定的。单边RDMA Write的BTH后面跟了RETH头里面包含了远端内存地址和RKey。接收端网卡解析BTH后直接拿着RETH里的地址做DMA写入不需要查询任何接收队列。这就是“单边”的底层真相决定数据落点的信息在发送方的WQE里接收端只是被动的内存写入方。所以从BTH的角度看双边传输的头部信息不足必须依赖接收端的上下文单边传输把接收所需的一切元数据都打包在数据包里了。这个设计取舍深刻影响着两端CPU参与度和内存管理的复杂度。5. QP被问爆的“RDMA QP是什么”的一次讲清5.1 QP的本质一条虚拟通信管线QP全称Queue Pair队列对。一个QP由发送队列SQ和接收队列RQ组成另外还有一个完成队列CQ可以跟多个QP关联。我是这么理解的QP就是RDMA通信里的“虚拟连接”。它跟TCP连接的最大区别在于连接的状态不是由操作系统维护在软硬件栈里而是由网卡硬件维护。应用每次发送数据其实是往QP的发送队列里投递一个WQE网卡依次从WQE里取出指令解析成数据包发出去接收端网卡根据BTH里的Dest QP找到对应的QP把收到的数据填入接收队列或直接按RETH写内存。QP在verb API里对应一个整数ID比如ibv_create_qp返回的qpn。你在抓包时看到的BTH里的Dest QP就是它的硬件编号。两个端点想通信必须各自创建QP然后交换QP号、GID、内存信息最后调用ibv_modify_qp把连接状态推进到RTRReady to Receive和RTSReady to Send。日常排障时我必做的一件事就是查QP状态。用rdma res show qp命令可以列出所有QP的状态、所属设备的PID、连接的远端信息$ rdma res show qp link ibp3s0/1 lqpn 0 type SMI state RST sq-psn 0 comm [sm] link ibp3s0/1 lqpn 1 type GSI state RST sq-psn 0 comm [sm] link ibp3s0/1 lqpn 320 type RC state RTS sq-psn 1381440 comm ib_write_bw看到state RTS说明连接可用如果卡在RTR或SQD基本可以断定是QP状态机没走完最常见的坑是双方交换信息后忘记调ibv_modify_qp推进到RTS。5.2 QP状态机从RESET到RTS的关键路径QP状态机是RDMA面试和排障里都绕不开的硬骨头。一个QP要经历这些状态RESET初始状态QP刚创建什么都不做。INIT初始化完成允许把WQE加入SQ但还不能发数据。RTRReady to Receive接收端就绪。此时接收路径已经打开。RTSReady to Send发送路径也打开了。只有到这个状态数据才真正能双向流动。SQDSQP Drain发送队列正在排空一般用于修改QP配置前的老请求收尾。SQESend Queue Error发送出错。ERR错误状态QP不可再用需要销毁重建。建立连接的完整顺序是两端都创建QP、注册内存、交换信息然后接收方先把状态改为INIT再改到RTR发送方接着从INIT改到RTR再改到RTS。实际使用中库函数ibv_connect_qp会帮我们把状态机一次走完但手动实现时非常容易漏掉“RTR必须先于RTS”这个顺序。去年我调一个跨网段RoCE连接半天连不上后来抓包发现发送端一直在发数据接收端QP还停在INIT就是因为RTR没有生效。5.3 单边写场景下QP配置的三条经验第一RCReliable Connection可靠连接是默认选择。RC服务类型支持RDMA Write的全部语义包括乱序重传和完整性校验。相比之下UDUnreliable Datagram不可靠数据报不支持RDMA Read/Write只能做SEND所以做单边写基本排除UD。第二QP深度不要盲目调大。接收队列深度RQ Size在双边传输场景需要匹配预期并发数但单边写场景接收队列几乎用不到真正要关心的是发送队列深度SQ Size。如果SQ太小应用下发WQE时会被网卡挡回来发生ibv_wc的IBV_WC_WR_FLUSH_ERR。第三如果多QP并发每个QP都要独立配置PSN的起始值且需要保证PSN分配的单调性。很多底层驱动有自动PSN管理但用户态自己写裸verbs时要把初始PSN的随机化做好避免两个QP因为PSN碰撞而导致重传风暴。6. 实战中的坑与调优记录6.1 问题一RoCEv2流量出不去tcpdump抓到包但对端没反应这个坑我踩过不止一次。排查顺序是固定的先看网卡是否开启了RoCE模式ibstat、ibv_devinfo再看GID是否配置了RoCEv2类型show_gids接着看交换机端口是否启用PFC最后才怀疑应用层。最常见的原因是GID没选对。RoCEv2在RoCEv1的基础上封装了UDP/IP网卡会为每个端口生成多个GID条目其中index可能同时存在RoCEv1和RoCEv2的版本。如果代码里用rdma_get_cm_event选路径时取了RoCEv1的GID包就只能在二层广播域里跑跨三层网关必挂。解决方法是明确指定GID index并且在远端交换时同时带上GID类型。6.2 问题二单边写数据篡位或读出来是旧值RDMA Write写的是远端内存如果接收端在读数据时没有做内存同步很容易读到写了半截的脏数据。这其实不是RDMA的问题而是CPU缓存和内存屏障的问题。解决办法是在发送端写完数据后用ibv_post_send发一个带IMM的SEND通知或者接收端在轮询到自己的标志位变化后再读取数据。标志位本身要用原子操作保证可见性我自己习惯用volatile加内存屏障或者直接在QP里用IBV_WR_ATOMIC_CMP_AND_SWP做标志位更新。6.3 问题三大包吞吐上不去小包却挺好遇到吞吐瓶颈先别急着怀疑网卡看看MTU。RoCEv2以太网MTU一般配置为4096或1024如果交换机或网卡一端是1500另一端是4096大包会被分片或丢弃吞吐反而上不去。排查方法是用ib_write_bw分别跑64字节和1MB负载如果小包正常大包异常优先级检查MTU和PFC。PFCPriority-based Flow Control基于优先级的流控对RoCE至关重要。RDMA的默认重传机制比TCP激进遇到拥塞会出现大量重传导致性能悬崖所以生产环境都要求交换机开启无损队列。具体做法是把RoCEv2流量映射到交换机的优先级队列上并保证该队列没有丢包。如果PFC没开大负载下连接直接断掉也不要意外。6.4 附一张问题速查表现象可能原因快速定位手段连接建立失败GID配置错误 / QP状态机未走到RTSrdma res show qp、ibstat抓不到任何RoCE包网卡RoCE被禁用或驱动版本过旧ibv_devinfo -v检查device caps跨交换机丢包严重PFC未开启 / 缓存不足检查交换机端口丢包统计单边写完读旧数据缺少内存屏障或通知机制用带IMM的SEND做完成通知多QP跑不满带宽源端口固定导致ECMP哈希不均开启源端口动态分配6.5 最后再分享一个小技巧调试单边写的时候强烈建议先用ib_write_bw这类perftest工具把通路跑通再回到自己的业务代码里。perftest的--report_gids参数会把使用的GID、QP信息全部打印出来配合tcpdump -s 0 -w roce.pcap抓包几乎能定位90%的RoCE连通性故障。如果发现BTH的PSN乱序优先怀疑网卡固件或交换机ECMP把同一QP的包哈希到了不同路径。解决办法是给RoCE流量配置一致的哈希因子很多管理员会让交换机对源L4端口、目的L4端口、IP做组合哈希但当源端口本身变化时反而会拆散同一QP的数据流。我在生产环境里最常用的检查命令是rdma link show和rdma resource show qp它们比任何图形界面都直观。一个QP状态如果是RTS、psn正常、路径MTU没异常那剩下的问题多半在应用层缓冲区和同步逻辑上了。RDMA单边写入能秒杀双边传输不是某一种算法突然变快而是整个数据路径的设计思路变了从“两端协同CPU全程参与”变成了“一次协商硬件直达”。UDP 4791和BTH头只是这个设计在协议层的外在表现真正值钱的是理解为什么要把这些信息放在包头里、为什么让接收端可以不被打扰。按这个思路去调RoCE网络你会发现很多以前觉得玄学的问题其实是协议栈设计时的必然结果。
返回列表