1. 从“一次远端写入”看清楚 RDMA 把可靠性放在哪一层
RDMA 传输服务 的可靠性到底靠不靠谱?这是我在刚开始用 InfiniBand 和 RoCE 时最爱问的一句话,后来发现真正要把这个问题讲清楚,不能只看协议名,还得看你用的是哪种连接模式。很多入门材料喜欢把 RDMA 形容成“网卡直接搬内存”,这句话没错,但它让人忽略了一个关键点:从源内存到目标内存这段过程,谁来保证数据不丢、不错、不乱序?你如果把它丢给上层网络协议,RDMA 的延迟优势就没了;如果丢给 CPU,内核旁路就白做了。所以答案是:RDMA 的传输服务本身要承担很大一部分可靠性工作,这就是 RC、UC、RD、UD 这一堆缩写存在的意义。
1.1 传统网络栈的瓶颈其实不在“带宽”,而在“处理”
在说 RDMA 之前,先想想普通 TCP/IP 发一次数据要经过什么。应用把数据写进 socket buffer,内核把用户态数据拷贝到 sk_buff,经过 TCP 分段、IP 路由、网卡驱动,再触发 DMA 到网卡。数据到了对端,又得经过一次 DMA、驱动收包、协议栈处理、内核把数据拷到用户态缓冲区。这中间的每一步都有 CPU 参与,尤其是网卡断包和内存拷贝,只要吞吐一高,CPU 占用率就上去了。
传统网卡发展到后来也支持 TCP 卸载、checksum offload、LRO/GRO 这些特性,本质上都是想帮 CPU 分担协议栈工作。但真正难解决的是“数据路径”问题:数据总是要经过内核,通讯的双方不能直接访问对方内存。RDMA 的做法则直接改变了边界——让网卡(HCA)成为两个端点之间数据搬运的执行者,应用只需要注册一段内存,然后把内存地址、访问密钥告诉网卡,网卡自己把数据拆包、发送、重组,最后 DMA 写到对端应用指定的缓冲区里。
1.2 RDMA 传输层要回答的问题:谁保证可靠
很多人以为有了 RDMA,所有的丢包、重传问题都消失了,实际上不是。RDMA 把可靠性下放到了硬件传输层,但“可靠”是有等级和成本的。
一个 RDMA 消息从发送队列(SQ)发出后,会被切成大小不超过路径 MTU 的报文,每个报文都带传输层头部。接收方收到后要么向上递交,要么根据报文序号判断是不是丢了包。如果这个 RDMA 服务要求可靠交付,发送端的 HCA 就会保存发送状态、等待确认、超时重传;如果这个服务本身不可靠,那 HCA 就像一个只管发射的火箭,数据丢了它也不管,完全交给上层应用去兜底。
所以在选型时,真正的问题不是“我要不要用 RDMA”,而是“我要用哪种 RDMA 传输服务”。这类似 TCP 和 UDP 之间的关系,但又不完全一致,因为 RDMA 的传输服务还有“连接模式”和“数据报模式”两个独立维度。可靠性与连接性不是一一对应的,这才导致新手很容易被 RC、UC、RD、UD 四个名字绕晕。
1.3 你在软件里看到的“QP”,其实就是传输服务的实体
RDMA 编程里,最核心的对象是 Queue Pair(QP)。一个 QP 由发送队列和接收队列组成,应用通过 ibv_post_send 把工作请求(WQE)扔到队列里,HCA 拿到之后自己处理传输。对程序员来说,你创建一个 QP 时指定的 qp_type,几乎就决定了之后所有数据报文会被网卡怎么对待。
QP 类型分别对应不同传输服务:
IBV_QPT_RC:可靠连接IBV_QPT_UC:不可靠连接IBV_QPT_UD:不可靠数据报IBV_QPT_RD:可靠数据报(可选,很多硬件支持有限)
从经验来看,90% 的存储场景用的都是 RC,因为 NVMe over Fabric、分布式存储这类场景要求消息可靠交付,而且需要 RDMA Read/Write 这类语义。但如果你只了解 RC,在规模做大之后会遇到很现实的 QP 数量膨胀问题。这也是为什么我们有必要把四种服务放在一张表里认真看。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. RC/UC/RD/UD 四兄弟放在一起看,可靠性和连接性其实是两条轴
很多资料会把 RC 说成“最可靠”,把 UD 说成“最不可靠”,听起来好像按顺序排列就行。但实际情况不是一维排序,而是两条轴:一条轴是“是否可靠”,另一条轴是“是否面向连接”。UC 既连接又不可靠,UD 既无连接又不可靠,RC 连接且可靠,RD 则是想把可靠和数据报结合起来。先看清这张矩阵,后面才不会用错。
2.1 四个服务的核心差异
| 传输服务 | 连接模型 | 可靠性 | 多对端复用 | 支持的主要操作 | 典型运行代价 |
|---|---|---|---|---|---|
| RC(可靠连接) | 一个 QP 只对应一个远端 QP | 有确认和重传,保序 | 否,对端 QP 数量会膨胀 | Send、RDMA Write、RDMA Read、Atomic | 高,每个 QP 都有收发状态 |
| UC(不可靠连接) | 一个 QP 只对应一个远端 QP | 无确认,不重传 | 否 | Send、RDMA Write | 中,连接状态比 RC 少 |
| RD(可靠数据报) | 一个 QP 理论上可对应多个远端 QP | 有重传,但依赖 EEC 等附加状态对象 | 是,减少 QP 数量 | Send、部分 RDMA 操作需要看具体 HCA | 较高,可靠性状态不归 QP 持有 |
| UD(不可靠数据报) | 一个 QP 可向多个远端 QP 发送 | 无确认,不重传,有丢包可能 | 是,QP 数量最省 | Send | 低,几乎不维护单对端状态 |
要说明一点,RD 在很多商用网卡和软件栈中并不是默认支持,它的设计初衷是用少量 QP 支撑大规模可靠通信。实际项目里我见到更多是用 UD 做控制面,或者用 UD 再叠一层应用层确认来实现自己的可靠消息层。
2.2 RC:典型可靠,但“连接数”也是最大成本
RC 的工作方式和一个 TCP 连接有点像,每个 RC QP 在硬件里维护了对端的 PSN(包序号)、丢包重传状态、接收窗口等。发送端发送数据后,接收端会回 ACK;如果发现丢包、错序或接收端队列不够用,还会触发重传逻辑。RC 能支持 RDMA Write、RDMA Read、Atomic 操作,这一点对存储协议特别关键,因为 NVMe over Fabric 的数据搬运本质上依赖“由目标端主动读取或写入源端内存”的能力。
RC 的另一个优势是“保序”。一条 RC 连接上,消息之间的顺序有明确保证,应用不太需要自己处理乱序。但是性能背后是资源成本:N 台服务器之间做全互联,如果要两两建 RC QP,那就是 N(N-1)/2 这个数量级的连接。一个 QP 在 HCA 内部对应的是一大堆状态,包括 PSN、重传队列、超时参数、MTU、路径信息。连接数量一多,不仅 QP 分配内存变大,连接建立本身也需要在两端交换参数,复杂度会迅速上来。
2.3 UC 和 UD:不可靠服务到底省在哪
UC 是“连接型但不可靠”的服务。它不像 RC 那样需要做复杂 ACK/NAK 和重传,HCA 不用保存太多重发状态,所以单条连接的内存占用比 RC 小。UC 支持 Send 和 RDMA Write,但通常不支持 RDMA Read 和 Atomic,因为后者天然需要请求者维护一种“可靠请求-响应”状态,没有可靠重传就很难做好。
UD 是更彻底的数据报模式。一个 UD QP 可以直接向多个不同远端 QP 发消息,接收端也可以收到来自很多 QP 的消息。UD 最明显的价值是不用为每个对端创建 QP,所以大规模节点间通信很适合用 UD 做广播、服务发现、健康检查这类控制消息。不过 UD 的每条消息通常被限制在单个数据包范围内,没法像 RC 那样把一个很大的消息切成多个带序号的数据包并可靠重组。如果你要用 UD 发送超过 MTU 的数据,软件会直接报长度错误,而不是自动帮你分段。
我在实际调优里见过不少团队想用 UC/UD 换取更低延迟,但前提是上层协议能容忍丢包或者自己处理重试。比如某些高性能计算里的脏位通信、状态同步、监控日志,丢一包数据影响不大,用不可靠服务反而能把硬件负担降下来。
2.4 RD:可靠数据报为什么没有占领世界
RD 的概念很吸引人:一个 QP 可以跟多个对端通信,同时又能获得可靠重传语义。这样既避免 RC 大规模建连,又不用像 UD 那样由应用自己负责重发。
但现实是,RD 的实现远比 RC/UD 复杂。它要把可靠性状态从 QP 里拆出来,放到 EEC(End-to-End Context)这样的对象里,每次发送都要显式或隐式关联到对应的 EEC,HCA 得额外维护一张“远端上下文”的表。硬件支持不统一,软件栈暴露出来的接口也绕,导致 RD 在很多厂商网卡上只作为“标准里有但不是拿来就用”的备选项。真正追求大规模低延迟的人,更多是采用 UD + 上层 ACK,或者用 XRC 这类扩展方案绕开痛点。因此我的观点是:可以让 RD 停留在概念理解层面,但在项目选型时先确认你手上的网卡和驱动到底支持到什么程度,不要想当然。
3. 可靠连接里的硬核机制:PSN、ACK/NAK、RNR 与重试上限
RC 能提供可靠传输,靠的是一整套硬件协议状态机。这部分不像应用代码那么直观,但它直接决定了什么时候你会看到“死等”“超时”“错误 completion”,所以很值得展开。
3.1 PSN 和 ACK 是可靠性的地基
RDMA 的可靠服务和 TCP 的序号机制有异曲同工之处。RC 发送的每个报文都有一个包序号 PSN,发送方启动 QP 时会确定一个初始 PSN,之后每发送一个包含新消息的报文,PSN 递增。接收方则维护一个期望收到的 PSN,用来判断当前收到的报文是否是重复包、是否跳号。
当接收方成功收到数据并完成 DMA 写入,会根据策略返回 ACK。RC 使用的是累积确认机制,接收方不需要对每个包单独回 ACK,可以等多收到几个包后一次性确认前面的所有包。这样设计是为了减轻 ACK 风暴对带宽的影响,但代价是发送方需要保存足够多的发送状态来支持“如果超时,需要把之前所有未被确认的报文重新发送一遍”。
如果接收方发现收到的 PSN 不是期望值,或者校验失败,理论上会触发 NAK 或直接丢包。遇到这种情况,发送方的 HCA 会根据参数决定是重传某个包还是直接报错。这里最容易被忽略的一点是:RDMA 的“可靠”,并不是“一定送达”,而是“要么送达,要么用错误 completion 明确告诉你它没能送达”。应用代码如果在发完请求后不检查 completion 错误,就会出现数据已经丢在你不知道的地方,程序却还在傻等后续状态这种怪现象。
3.2 RNR 不是丢包,而是“接收端还没准备好”
在排障现场最常见的可靠性误判,是把所有超时都当成网络丢包。有一次我在压测一个基于 RC 的服务,发送端不断报 RNR Retry Exceeded,一开始我也以为是网络问题,后来看 HCA 计数器才发现大量 rnr_nak_rcvd。
RNR 的全称是 Receiver Not Ready。它表示接收方已经把报文物理收到了,但是接收队列里没有足够的 Receive WQE 来承接这个消息。类比一下,就是对方电话铃响了,但因为电话线那头没有人接听。RC 服务里,接收端遇到这种情况会返回 RNR NAK,发送端不会立刻认定链路故障,而是等一个 RNR 超时计时器,然后再重试。如果重试次数超过了 rnr_retry 上限,连接就会出现错误,应用层看到的就是“RNR Retry Exceeded”。
很多初学者写的 RDMA 接收程序只在一开始 post 了一两个 Receive buffer,一旦收发速率上来,接收队列瞬间被耗光,RNR 就开始狂涨。解决思路也很简单:预先在接收队列里放足够多的 Receive WQE,并保证有线程持续补 post_recv,让接收队列永远“有人值班”。但需要注意,并不是队列越深越好,队列太深会占用大量内存并拉长完成事件的处理路径,具体深度要靠压测来定。
3.3 错误计数器和重试参数:这些才是排障的“仪表盘”
RC 连接在创建 QP 时会传入若干路径参数,比如 timeout、retry_count、rnr_retry。实际换算单位挺绕人,有一个简洁经验:timeout 的值是指数化的,4 对应约 4096ms,每个单位大致是 2 的幂乘一个基础时间片。你不需要背公式,但必须知道这些参数不是越大越好。retry_count 设置过小,偶发拥塞会让连接直接失败;设置过大,一旦真的丢包,系统会在用户无感知的情况下反复重传,把故障时延拖得很长。
我排障时习惯看两组数据:一组是 Linux 下 /sys/class/infiniband/<device>/ports/<port>/counters/ 下的计数器文件,另一组是 ibstat 输出里的收发错误和丢弃计数。与你直接相关的主要有:
rnr_nak_rcvd:对端发来 RNR NAK 的次数,说明接收队列深度或补缓冲逻辑有问题duplicate_request:对端收到重复请求,通常是 ACK 丢失后重传导致的packet_seq_err:包序号错误,严重时说明网络上存在丢包或乱序,需要检查链路层和拥塞配置
这些计数器比带宽数字诚实得多。我见过一块“看起来一切正常”的卡,实际 packet_seq_err 已经涨了三万多,只不过应用因为重传成功所以没报错误。所以压测时建议测完吞吐和时延之后,顺手把这一串计数器打点记录一遍,形成基线,后面再出问题就有对照了。
4. QP 状态机与建连握手:连接模式不是网络意义上的长连接
很多人学 RDMA 时会下意识拿它和 TCP socket 对比:本地 listen,对端 connect,然后
