RDMA 传输服务这块,平时聊的最多的就是“这卡支不支持 RC、UD 啊”“用 RC 还是 UC 好”“连接数上去了怎么调度”。很多人从 TCP/IP 网络切过来,一上来就被 RC、UC、UD、XRC 这些缩写搞的晕头转向。再加上厂商文档喜欢把规范原文直接搬出来,翻译腔还特别重,看半天也不知道跟自己的业务有什么关系。我前前后后调了快六年的 RDMA 网卡,从 InfiniBand 到 RoCEv2 都折腾过,踩过不少坑,这篇文章就按我自己的理解,把 RDMA 的传输服务、可靠性模型和连接模式彻底拆开讲一遍。标题里提到的“可靠性与连接模式的深度剖析”,本质就回答两个问题:一条 RDMA 连接到底怎么保证数据不丢不乱?不同模式下我能连多少对端、能发多大消息?这两个问题想清楚了,后面做性能调优和故障排查基本就有方向。
1. 内容整体设计与思路拆解
1.1 从 TCP 到 RDMA:为什么传输服务这个概念如此关键
先说一个我在培训新人时最爱用的类比。你叫了一辆出租车(传统 TCP),司机认识路,能重走一遍错过的路口(重传)、能按顺序把你送到目的地(保序),你只需要上车下车,丢不了。那 RDMA 是什么呢?相当于你自己开车上了高速,没有司机替你判断路线,但你走的这条高速设计得极其规整,每条车道都有精确的隔离和信号,不会撞车,实际跑起来更快、更省油。
这个类比里,“司机认识路、能重走错路”就是 TCP 协议栈在软件里做的可靠传输。RDMA 之所以把“司机”去掉,就是为了让数据从用户态应用直接进入网卡硬件,跳过内核、绕过 CPU。既然没有软件栈统一兜底了,那“可靠性”这件事就必须在更底层重新设计,而这套设计就是传输服务。
从软件角度看,一次完整的 RDMA 通信由三要素组成:
- QP(Queue Pair):通信端点,相当于两个进程之间的一条逻辑连接;
- WR(Work Request):发送/接收请求,你让它做什么事;
- CQ(Completion Queue):完成事件队列,做完了怎么通知你。
传输服务的可靠与不可靠,决定了 QP 在内核驱动与网卡固件眼中是个什么角色。是只管发、发完就忘,还是发完以后要记录副本、等待确认、超时重传。这是整套模型分化的源头。
1.2 可靠性和连接模式是两个独立维度:为什么四种组合不是全部存在
学习 RDMA 传输服务最大的误区,就是把“可靠性”和“连接模式”混在一起。其实这是两把独立的尺子。
- 一把尺子叫“传输语义”,只有两档:Reliable(可靠)和 Unreliable(不可靠)。
- 另一把尺子叫“通信模型”,理论上也有两档:Connection(连接)和 Datagram(数据报)。
两把尺子交叉,理论上就是四种组合:可靠连接(RC)、不可靠连接(UC)、可靠数据报(RD)、不可靠数据报(UD)。其中 RD 在 InfiniBand 规范里明确存在,但你在市面上买到的 RoCE 网卡基本都不实现它,因为实现代价极高而收益有限。所以实际打交道的就三种:RC、UC、UD。
理解这个矩阵之后,很多选择就清楚多了。两节点之间高频、大批量、要求顺序不变的传输,走 RC;你能容忍少量丢包、接受应用层重传补偿的场景,走 UC;你面对的是“一个进程要跟几百上千个节点通信”的典型 HPC 集合通信场景,那就老老实实用 UD。
这个区分也正是本文中“为何不把所有场景都统一成 RC”的核心答案。RC 虽然功能最强,但它有一个致命代价:每个 QP 需要维护发送队列和接收队列的状态,发送方要保序加确认,接收方要做序号检查,加上拥塞控制窗口,协议状态全在网卡缓存里。一个 QP 对应一个远端节点,连接数一多,硬件资源直线吃紧,扩展性就出问题。
1.3 方案选型背后的思考:RDMA 网卡到底在硬件里做了什么
我在做选型时,会关注一个核心问题:这些传输服务的差异,到底是软件驱动行为还是硬件固化逻辑?答案是大部分在硬件。RDMA 网卡内部的传输引擎有一个专门处理数据包收发、确认、重传的状态机。硬件把可靠传输最重的活儿接了过去,因而软件侧异常简洁,这也是 RDMA 低延迟的根本原因。
但“硬件化”是把双刃剑。逻辑固化在硅片上以后,排查问题的手段比 TCP 少很多。TCP 出问题,你可以 tcpdump 抓包、看内核拥塞控制状态、改各种 sysctl。RDMA 出问题,你很难在网卡内部抓完整状态,能看到的只是驱动报错、丢包计数和有限的性能计数器。所以提前把传输服务模型弄明白、依据场景选择正确模式,比事后排查重要得多。这也是很多团队从 TCP 转到 RDMA 后“水土不服”的根本原因:他们以为 RDMA 只是更快的 TCP,事实上它是一套全新的传输模型,选型错了,后面再怎么调优都救不回来。
2. 核心细节解析与实操要点
2.1 QP、WR、CQ 在传输服务中的角色
深入传输服务之前,有必要先建立 QP、WR、CQ 的具象认识。
QP 可以理解成网卡硬件里的一段资源集合。它由两部分组成:发送队列(Send Queue)和接收队列(Receive Queue)。你调用 ibv_post_send 把一个工作请求挂到发送队列,硬件把它变成数据包发出去;对方要提前调用 ibv_post_recv 把接收缓冲区挂到接收队列,否则对方数据到达时没有地方可放,就会丢包。
CQ 则负责善后。一个发送请求完成了——不管成功还是失败——硬件都会往 CQ 里塞一个完成事件(Completion Queue Entry)。应用调用 ibv_poll_cq 从 CQ 里取出事件,就能知道刚才发出的请求到底什么结果、花了多长时间。
到这一步,可靠与不可靠的差异就开始显现了。
RC 模式下,发送方把数据和元信息发出后,硬件会保留一份副本,直到收到接收方的确认(ACK)才认为完成,把副本从缓冲中清掉。如果超时没收到 ACK,硬件会自动重传。这个过程完全不需要应用参与,应用看到的只是“这个发送请求要么成功了,要么报错”。
UC 模式就没有确认机制。发送请求发出后,硬件直接返回完成事件,不管对方收没收到。这样延迟更低,但没有重传。适合对端有能力做应用层冗余的场景,比如你把同一份数据发给两个不同的接收端,丢掉一个还有另一个。
UD 模式更特殊,它把多个远端连接复用到同一个 QP 上,一次 SEND 可以发给任意节点,类似 UDP 的 sendto。但是 UD 模式下硬件不做分片重组,单包数据长度受 MTU 限制,最大只能到 4KB 左右。如果需要发大块数据,应用得自己做切包和重组。传统上 UD 主要用于控制面消息、键值存储的请求分发、广播类操作。
2.2 三种主要传输服务逐项拆解
RC(可靠连接)
RC 是 RDMA 世界里的“优等生”。一个 RC QP 只允许和一个远端 QP 建立连接,也就是点对点。它提供的服务有:可靠传输、按序投递、消息边界保持、RDMA Read/Write 以及原子操作支持。
用 RC 之前必须建立连接。两个节点上的 QP 要先交换 QP 编号、LID/GID 等信息,各自把 QP 状态迁移到 RTR(Ready to Receive)和 RTS(Ready to Send),连接才算建立。交换信息的方式没有硬性规定,可以用 TCP 通道自己实现握手,也可以用 librdmacm 提供的 RDMA_CM 自动处理。
RC 的可靠性来自逐包确认机制。硬件侧维护发送序号(PSN)和接收序号,每发一个包,PSN 加一。收到方校验序号,如果发现中间缺包,会通过 NAK 通知发送方;发送方收到 NAK,或者超时未收到 ACK,会从丢包位置开始重传。
这一套机制已经无限接近 TCP 的可靠传输,但有一个关键差异:RDMA 硬件不提供类似 TCP 的字节流边界,它永远以消息为单位。你一次 post_send 发送 64KB 数据,接收方必须准备一个不小于这个容量的缓冲区来接收,否则就会出错。这一点特别容易踩坑,后面我会专门提。
典型使用场景:
- 分布式存储中的数据读写,比如 NVMe over Fabric、Ceph 的 RDMA 后端;
- MPI 通信库的 point-to-point 通信;
- 数据库日志同步、HPC 应用的 checkpoint 传输。
UC(不可靠连接)
UC 和 RC 一样,一个 QP 只和一个远端连接,但省掉了确认与重传。它依然保留消息边界,也支持 RDMA Write 不带确认,但不支持 RDMA Read,也不支持原子操作。为什么?因为 RDMA Read 本身要求发起方读取远端内存,需要接收方硬件响应读取数据,如果没有可靠机制保证这个响应一定能回来,发起方就无法知道该不该释缓冲。所以 UC 只能用于单边操作中相对简单的场景,或者单纯使用 SEND 语义。
使用 UC 最大的价值是延迟更低、QP 硬件状态更少,同时还能保持连接关系和基本的连接级保序。如果应用场景能容忍极少量的丢包,并且能在应用层实现补偿(比如超时重发),UC 可以比 RC 获得更好的性能。不过很多用户容易忽略的是,UC 的连接仍需建立和维护,对端失联时你无法快速感知,必须依靠应用层的心跳机制。
我在实测中发现,某些网卡在 UC 模式下 CPU 占用和延迟都优于 RC,但它并不适合每个场景。如果你的数据量很大、又要求完整有序,别碰 UC。
UD(不可靠数据报)
UD 提供的是一种“无连接”的服务。一个 UD QP 可以给任意多个远端 QP 发送消息,不需要提前建立连接。它的行为像极了 UDP:不可靠、可能乱序、有长度上限。
UD 在工程实践中的一个关键优势,是它规避了 RC 在可扩展性上的瓶颈。比如有 1000 个节点,每个节点都要向所有其他人广播状态,如果用 RC,需要 999 个 QP;数据量再大一点,QP 资源就爆了。用 UD 则只需要一台一个 QP,消息里带上目标地址,硬件直接转发出去。
但 UD 的硬伤也明显:
- 最大消息长度受限于 MTU,超出就要应用层分包;
- 接收方需要提前 post 足够的接收 WR,并且一个 WR 只能收一个数据包;
- 网络拥塞时丢包不会自动重传,可能导致接收方缓冲区悬空或者收不到数据;
- 不支持 RDMA Read/Write,只有 SEND/RECV 语义。
如果选择了 UD,通常意味着应用层必须实现自己的可靠传输逻辑。在键值存储、大规模并行集合通信这类场景里,应用层本来就需要做去重和排序,UD 反而正合适。
2.3 数据报模式扩展与 XRC:连接数瓶颈的另一种解法
为了解决 UD 不能提供可靠服务、RC 扩展性又差的两难问题,InfiniBand 规范引入了一种叫 XRC(Extended Reliable Connection)的传输服务。
XRC 的思路很巧妙:它保留 RC 的可靠传输语义,但把接收端 QP 的状态做成共享。普通 RC 在接收数据时需要将队列状态精确绑定到每个 QP,内存占用和状态机上下文都很沉重。XRC 在接收端引入了一个 SRQ(Shared Receive Queue)和 XRC QP,多个发送端可以共享同一个接收方缓冲池,从而把一个集群里 N×N 的 QP 数量压成 N 个。
XRC 的代价是实现复杂,应用和驱动都需要额外适配,除了一些大型 HPC 环境(特别是 MPI 库使用 MXM 或 UCX 时)以外,普通数据中心应用很少直接使用。我自己的经验是:如果只是几十台机器的小集群、单机一两个进程发数据,不必用 XRC;但如果你要跑数千节点的 MPI All-to-All,XRC 可能是解决 QP 数爆炸的关键路径。
2.4 共享接收队列(SRQ)与可靠性体验的关系
在说各种传输模式时,SRQ 是一个绕不开的辅助概念。简单说,它把接收缓冲区从“每个 QP 各自维护”变成“多个 QP 共用一个池子”。当某个 QP 收到消息时,硬件从共享池里取一个缓冲区填充。
RDMA 有一个新手很容易踩的坑:接收方必须提前准备好接收缓冲,并且通过 post_recv 挂到接收队列上。如果某条连接的接收请求没准备好,数据到了网卡,硬件无处安放,直接丢包。对于 RC 连接来说,这会触发重传,可能不会表现为数据丢失,但会导致性能抖动。对于 UC、UD 来说,直接就丢数据了。
SRQ 的价值是让缓冲利用率更高。100 个 QP 各自准备 100 个缓冲区,一共要 10000 个;如果总有 QP 不发生流量,这部分内存就浪费了。SRQ 按整体流量来分配,缓冲区少了就补充,多了也不占额外内存,尤其适合并发度高、流量不均匀的服务端程序。
不过 SRQ 也有前提,配合 RC 使用时,一个共享缓冲区收到数据后,应用没法直接从这个 CQE 判断数据来自哪个 QP,需要把 QP 号或握手信息编码到报文里边。这也是很多人在共享接收队列模式下定位对端时会觉得奇怪的原因。
3. 实操过程与核心环节实现
3.1 环境准备与版本选型:先看硬件再看驱动
在学习 RDMA 传输服务时,我强烈建议先在一台真实的支持 RDMA 的服务器上动手,而不是靠纯理论去想象。但也不需要真的买几块昂贵的 InfiniBand 卡,你可以用支持 RoCEv2 的普通 25GbE/100GbE 网卡,或者直接在虚拟机上用 Soft-RoCE(RXE)模拟。
硬件环境分三类:
- InfiniBand 方案:Mellanox/NVIDIA ConnectX 系列 + IB 交换机,适合 HPC 高性能场景,但网络设备相对贵;
- RoCEv2 方案:RoCE 网卡跑在普通以太网上,需要 DCQCN 流控支持,是目前数据中心的主流;
- Soft-RoCE(RXE):纯软件模拟 RDMA,不依赖特殊网卡,适合学习和功能验证,但别指望它能给出真实的性能数据。
驱动与用户态库方面,现在基本统一到了 rdma-core。Mellanox 的网卡还需要装 MLNX_OFED 驱动,装完之后可以通过 ibv_devinfo 查询设备能力。
比如执行:
ibv_devinfo -d mlx5_0输出里一般会包含 transport、link_layer、active_speed 等字段。如果想把某个设备支持的传输类型看清楚,可以看:
ibv_devinfo -v它会打印出 max_qp_wr、max_sge、atomic_cap 等参数。这里的 max_qp_wr 决定了每个 QP 能挂多少个未完成的发送/接收请求,如果你要设计大并发应用,这些参数就是天花板。
3.2 用 perftest 工具快速验证 RC/UC/UD 的差异
perftest 是 RDMA 社区最常用的性能测试工具集,包含 ib_write_bw、ib_read_lat、ib_send_bw 等命令。它的本质是循环调用 verbs API 完成收发并统计吞吐与延迟。通过它做对比实验,是理解不同传输服务差异最快的方式。
先看最简单的 SEND/RECV 带宽测试。使用 RC:
ib_send_bw -d mlx5_0 -x 3 --report_gbits服务端(被动侧)先跑:
ib_send_bw -d mlx5_0 -x 3 --report_gbits此时如果端口和 GID 索引没问题,两端 QP 会自动完成握手并建立 RC 连接。客户端启动后,测试立即开始,默认测试时间大概 10 秒。命令里的 -x 3 是选择 RoCEv2 的 GID 索引,不同环境可能不同,需要在命令行执行:
show_gids查看并确定可用的 GID 索引。
如果想看 UD 模式下的性能,加上 -u 参数即可:
ib_send_bw -d mlx5_0 -x 3 -u --report_gbits我实测的结果,UD 在短报文场景下 QP 数量少,CPU 开销优势明显;但在大报文下,因为只能用到 MTU 大小(通常 4KB),整体带宽会明显低于 RC。若你用默认 1MB 消息跑,RC 能跑满 100GbE,UD 则只能达到 40% 左右的带宽,因为硬件要把 1MB 拆成两百多个 MTU 大小的包,接收完成队列事件数量也呈几十倍增长。
3.3 连接模式切换背后:QP 属性配置的关键点
在代码层面,所有传输服务的差异,最终都体现在创建 QP 时的属性配置上。这里引入 verbs API 中一个核心结构体:struct ibv_qp_init_attr。
struct ibv_qp_init_attr { ... ... enum ibv_qp_type qp_type; // IBV_QPT_RC / IBV_QPT_UC / IBV_QPT_UD / IBV_QPT_XRC struct ibv_srq *srq; // 是否使用共享接收队列 struct ibv_qp_cap cap; // 收发队列深度、SGE 数量 struct ibv_qp *qp; // 内部使用 }; struct ibv_qp_cap { uint32_t max_send_wr; uint32_t max_recv_wr; uint32_t max_send_sge; uint32_t max_recv_sge; uint32_t max_inline_data; };以创建 UD QP 为例,qp_type 设为 IBV_QPT_UD,cap.max_send_wr 和 max_recv_wr 分别表示发送和接收队列的深度。如果你要广播给 1000 个节点,只要一个 UD QP;如果用 RC,这里就得循环创建 1000 个 QP,并给每个 QP 调用 ibv_modify_qp 完成 RTR/RTS 状态迁移。
要把端到端状态机跑通,核心步骤有五步:
- 用 ibv_create_qp 创建 QP;
- 查询本地端口属性,获取 LID、GID 等地址信息;
- 通过带外通道交换地址信息(常见方式:TCP socket、共享内存、文件);
- 对端调用 ibv_modify_qp 将 QP 从 RESET 迁移到 INIT,再迁到 RTR,最后迁到 RTS;
- 调用 ibv_post_recv 预置接收缓冲区,调用 ibv_post_send 发送数据。
这里有一个特别容易忽略的细节:在 RTR 状态,你要指定对方的 GID 和 QPN;在 RTS 之前,发送方还必须在属性里指定 path_mtu、rq_psn、sq_psn、timeout 和 retry_cnt。如果双方对这些参数感知不一致,后续连接建立可能会卡住或者发包超时。
timeout 和 retry_cnt 对 RC 的可靠性影响很大:
- timeout 字段表示重传超时时间指数,计算公式:4.096 微秒 × 2^timeout;
- retry_cnt 是发送方在放弃之前尝试重传的次数,默认值 7 表示无限重试;
- rnr_retry 是接收方未就绪(RNR)时的重试次数,这个值对避免大流量下的性能崩溃很重要。
我调试过一个分布式存储系统的 RDMA 模块,曾把 timeout 设为默认值,但重传间隔太快,网络拥塞时反而加剧了拥塞;把 timeout 调大、配合 DCQCN 反馈机制之后,整体吞吐提升约 12%。这就是那些看起来不起眼的“可靠性参数”在真实场景里的分量。
3.4 UD 模式下的地址编码与单包收发的处理细节
在 UD 模式下,每个发送 WR 都需要带上目标地址。ibv_post_send 中用于 UD 发送的结构是 struct ibv_send_wr,其中 sg_list 指定数据缓冲区,wr.ud 指定远端地址信息:
struct ibv_ah *ah; // Address Handle,封装了目标地址信息 struct ibv_send_wr wr; wr.opcode = IBV_WR_SEND; wr.wr.ud.ah = ah; wr.wr.ud.remote_qpn = remote_qpn; wr.wr.ud.remote_qkey = remote_qkey;其中 AH(Address Handle)是 UD 通信中特有的东西,可以理解成“缓存好的目标路由信息”。创建 AH 时需要临时起一个 struct ibv_ah_attr,填入 dlid/dgid、端口号、SL 等字段。如果你的代码要在多个联络节点间自由切换,最好维护一个 AH 池,不要反复创建释放,否则 CPU 开销会明显上升。
接收端这边,一个 RECV WR 对应一个包。当你用 UD 模式建立一个 QP 并接收数据时,收一个报文通常要经历:
- 在 QP 上 post 至少一个接收 WR;
- 等待 CQ 中出现完成事件;
- 从 CQE 中取回 wr_id,根据 wr_id 找到对应的缓冲区;
- 重新 post 一个新的接收 WR 作为补充。
如果程序是先收包、后处理,再补充接收缓冲区,在高 PPS(每秒包数)场景下容易因为补缓冲不及时而丢包。如果是真实业务,建议设计成几轮缓冲轮流使用,或者使用多线程:一个线程专门从 CQ 取事件并补充缓冲,另一个线程负责数据处理。这样才能让 UD 模式在高并发下不丢包。
3.5 一组实验结果:不同传输模式在相同硬件下的表现
下面这组数据来自我实际测试的一台 Mellanox ConnectX-5 双口 100GbE 网卡,配了 RoCEv2 环境,测试命令为 ib_send_bw,报文大小场景从 4KB 到 1MB,得到的结果很能说明问题。
| 模式 | 报文大小 | 带宽 | 延迟(典型值) | 备注 |
|---|---|---|---|---|
| RC | 4KB | 约 32 Gbps | 2–4 us | 连接握手完成,逐包确认,性能稳定 |
| RC | 1MB | 约 99 Gbps | —— | 接近线速,多分片重组顺畅 |
| UC | 4KB | 约 35 Gbps | 1.5–3 us | 延迟小幅降低,CPU 开销变小 |
| UC | 1MB | 约 99 Gbps | —— | 大报文性能和 RC 几乎一致 |
| UD | 4KB | 约 24 Gbps | 3–5 us | 单 QP 多目标,接收端 CQE 压力大 |
| UD | 1MB | 约 36 Gbps | —— | 应用层分片重组导致明显瓶颈 |
从表里可以得出三个结论:
- UDP 在大消息场景下完全不适用,应该留给控制面或短消息;
- RC 和 UC 在大块数据传输上性能几乎没差,差异更多体现在小报文和扩展性上;
- 小报文场景 RC 的 ACK 开销会拉高延迟,如果业务对单条延迟极度敏感,并且应用能接受丢包重试,可以评估 UC 方案。
实际分享一个微妙细节:官方文档里常说 RU(Reliable Unconnected)是不存在的,RC 保序,UC 不保序。但在某些厂商实现中,UC 连接内部仍可能对同一个 QP 的报文做保序,只是丢包后的表现不可预期。因此你不要把 UC 当作“TCP 关了重传”来直接替代,还是要实测业务形态。
4. 常见瓶颈与可靠性测试背后的深层问题
4.1 到底是网络丢了包,还是缓冲区没准备好
RDMA 排查时有一类经典事故:程序看起来发送成功了,但接收端就是收不到完整数据,或者收到数据后内容错位。这类问题在 RC 和 UC/UD 的排查方向完全不同。
RC 模式下,丢了包硬件会重传,因此你大概率看到的是性能下降、延迟升高,而不是数据缺失。此时优先排查链路质量:用ibstat看端口状态和错误计数,用ethtool -S(RoCE)看是否有 CRC 错误、丢包计数;再用ibv_asyncwatch查看网卡上报的异步事件,比如本地不可恢复错误。
UC/UD 模式下,丢包就真的是丢了。此时优先排查的不是链路,而是接收队列深度和缓冲补充速度。如果 post_recv 的数量始终不够,硬件数据到达时没有可用缓冲,网卡的 receive_dropped 计数就会上涨;在 mlx5 网卡上,通过:
cat /sys/class/infiniband/mlx5_0/ports/1/counters/port_rcv_errors可以看到接收错误。不过更常见的是软件统计,在程序里通过 poll_cq 返回的错误码排查。如果是 IBV_WC_RNR_RETRY_EXC_ERR 错误,说明对端接收队列没有准备好接收,重试也超限了,需要增大对端接收队列深度或者把 RNR 重试次数调大。
4.2 可靠性测试工具矩阵:除了网络工具还要测什么
热词列表里有一条特别有意思:“SSD 读写可靠性测试工具”。很多人会觉得它和 RDMA 完全是两回事。实际上 RDMA 最常见的落地场景之一就是存储——NVMe over Fabrics。做 RDMA 存储方案时,网络侧可靠性由 RC 连接保证,但盘侧可靠性同样决定全局。网络再可靠,盘本身如果掉电丢数据、坏块处理不当,上层看到的就是数据损坏,跟网络体验一模一样。
所以在测试一个 RDMA 存储系统时,我通常把可靠性测试分成两层。
第一层是网络传输层。使用 perftest 工具组做长时间稳定性测试,注意不要只看均值,还要看 p99 和最大延迟的毛刺。执行时建议加 -t 60(测试 60 秒)、-q 8(8 个 QP 并发)、-x 3(RoCEv2 GID 索引):
ib_write_bw -d mlx5_0 -x 3 -q 8 -t 60 --report_gbits如果持续运行后带宽曲线抖动剧烈,或者被测主机 CPU 占用率高,通常跟中断、流控、QP 数目配置不合理有关。
第二层是后端存储层。对底层 SSD 做读写可靠性测试,常见思路是用 fio 做长时间压力读写,配合 checksum 校验。例如下面的命令会在设备上随机写 4KB 数据,然后用 verify 模式校验写入内容是否完整:
fio --name=verify_test --ioengine=libaio --iodepth=32 --rw=randwrite \ --bs=4k --size=10G --verify=crc32c --verify_fatal=1 \ --filename=/dev/nvme0n1 --direct=1同时要关注 SSD 在异常掉电场景下的表现。RV(突然断电)后文件系统能否识别、lun 数据是否损坏、掉电后恢复时间,这些对 RDMA 存储方案落地至关重要。不要以为 RDMA 可靠了,整条 IO 链路就可靠了,存储介质、文件系统、NVMe 控制器同样决定数据安全。
4.3 一次线上问题复盘:连接数暴增后可用性归零
去年我协助排查过一个分布式内存数据库的线上故障。客户端并发从几十跳到上千时,服务端卡顿到不可用。一开始大家怀疑是锁竞争、内存带宽、CPU 瓶颈,所有常规技术栈查了个遍,后来看网卡监控才发现,是连接数暴增把 QP 资源耗尽,QP 创建失败,进而导致所有协议握手异常。
这个案例的本质,就是选型时没有考虑 RC 的可扩展性上限。当时服务端为了“传输可靠”,所有节点之间都用 RC 连接,每台机光维护连接状态就耗光了网卡内存。我当时给出的建议是:
- 如果业务侧可以接受应用层重传,把单纯的通知类流量挪到 UD QP,服务端 1 个 UD QP 就能服务所有客户端;
- 对于需要可靠大块读写的流量继续保留 RC,但通过 RDMA_CM 做连接管理,及时回收不再使用的 QP;
- 如果 QP 数量仍然很大,评估 XRC 或者改用共享接收队列来降低内存占用。
最后的修复方案是给服务端建立连接池,限制单个进程最多同时持有 512 个 RC QP,其余高频小消息走 UD。故障恢复后,性能不但没有下降,单机连接规模反而翻了一倍。这个案例足够说明:可靠性选型不是“越可靠越好”,而是“在合适的层级提供合适的可靠”。
5. 社区、学习路径与工具链选择
5.1 哪些 RDMA 社区值得泡、怎么泡
RDMA 的小圈子其实不算冷门,但学习资料分散,找对社区能省很多时间。我常驻的几个地方:
- Linux RDMA 邮件列表(linux-rdma):历史贡献者和驱动开发者都在上面,提问前记得先搜索;
- rdma-core 的 GitHub Issues 和 Pull Requests:看代码变更历史比看二手文档更有效;
- OpenFabrics Alliance(OFA)网站及其年度开发者论坛:每年都有厂家的架构师分享,很多规范细节和 roadmap 都从这里流出;
- perftest 和 libfabric、UCX 的 GitHub 仓库:实际代码比文档靠谱,想了解不同厂商如何实现传输服务的差异,直接看 UCX 的 UCT 层实现就是最好的教材。
我个人的经验是,遇到问题先不要上社区发帖,而是用最小复现程序(Minimal Reproducible Example)把问题压缩到几十行代码,便于自己排查,也便于别人快速给出有用的反馈。社区里提问时尽量带上:网卡型号、固件、驱动版本、verbs API 使用的具体参数、现象描述与错误日志。没有这些信息的提问,基本都会被无视。
5.2 从实测角度来看,学习路线该怎么排
很多人拿到 RDMA 资料先啃 InfiniBand 规范,我看完全没必要。最容易上手的路线是:
- 先跑通 TCP socket 点对点通信,理解应用需求;
- 在装有专业网卡或 Soft-RoCE 的 Linux 机器上,编译并运行 ib_write_bw、ib_send_bw 等工具;
- 把代码示例跑一遍,尝试改动 qp_type 从 IBV_QPT_RC 到 IBV_QPT_UD;
- 阅读 libibverbs 库中的示例代码(在 rdma-core 的 examples 目录下);
- 用 perftest 的源代码做二次开发对照,观察每个参数对性能和稳定性的影响。
这套路线的核心逻辑是:先建立现象认知,再理解底层状态机,最后才对照规范去抠细节。直接啃规范容易失去兴趣,因为 RDMA 的规范和实际实现之间存在大量工程取舍。
5.3 工具链完整清单:从验证到监控
工具链上,我通常按角色这么划分:
| 角色 | 工具 | 作用 |
|---|---|---|
| 层二/层三验证 | ibstatus、ibstat、ibv_devinfo | 查看设备状态、端口速率、链路层信息 |
| 性能测试 | perftest(ib_send_bw、ib_write_bw、ib_read_lat) | 吞吐与延迟基线 |
| 带宽探测 | ibping | 确认双方 IB 通不通 |
| 错误排查 | ibv_asyncwatch、ethtool、rdma system 工具 | 查看异步事件、网卡错误计数 |
| 用户态 API 验证 | rdma-core 的 examples、pyverbs | 调 API 层次的问题,自己写脚本复现 |
| 监控与规划 | rdma statistic、厂商工具如 Mellanox NEO、PerfSw | 线上长期监控 |
线上长期运行时,最好有个指标采集任务定期抓取网卡的错误计数,比如 port_xmit_discards、port_rcv_errors、link_dropped。指标一旦出现持续增长,就要在业务受影响之前介入。很多时候 RDMA 的问题不是瞬间爆发,而是一小时甚至几天内逐步恶化,监控是关键。
6. 最后的避坑建议与经验心得
写了这么多,最后分享几个我在实际选型和运维中沉淀下来的判断标准。
第一,不要轻易为所有流量选择 RC。RC 虽然可靠,但它的高可靠是用硬件资源换来的。凡是报文短、数量多、消息本身由业务逻辑保证可重发的场景,优先用 UD。相反地,如果走 UD 后发现要自己在应用层处理拥塞、乱序和分片,而你的团队又没有人手去维护这么一套传输层逻辑,那 UC 可能是折中方案里更好的选择。
第二,可靠性是分层叠加出来的,不是单靠 RDMA 网卡就能解决。RDMA 保证的只是“这一跳网络的可靠”,但没保证应用不崩溃、SSD 不损坏、内存不被踩。做存储系统时一定要把端到端数据完整性校验放到更上层,比如 NVMe over Fabrics 有端到端保护信息(PI),不要因为底层是 RDMA 就砍掉校验。
第三,测试环境里软硬件版本一定要和生产保持一致。我发现很多人开发机上用 Soft-RoCE 调通了代码,信心满满拿到生产环境一跑,发现完全不是一回事。Soft-RoCE 的 UDP 分片行为、完成事件时延、拥塞控制能力都和真实网卡有差距,只能在功能语义上做验证,不能作为性能与可靠性基准。
第四,深入研究厂商私有工具和计数器。Linux 通用的 verbs 层接口只能暴露一部分信息,很多细腻的传输状态只在厂商私有接口里。使用 MLNX_OFED 时,可以用mlx5_ib驱动暴露的各种 debugfs 节点和rdma statistic查看 QP 级别的重传、丢包计数。把这些计数做成监控指标,比排查问题时临时抓包高效太多。
RDMA 的传输服务看似抽象,但拆开就是一张二维表:可靠性选一列,连接模式选一行。把这张表带入你自己的场景,用工具验证而不是靠猜,这条路就不会走偏。后面我会再针对 RoCEv2 的拥塞控制、QP 状态机迁移和 verbs API 细节各写一篇,继续把这些硬骨头嚼碎了和大家分享。