4. RDMA通信模型:通道语义与内存语义、可靠连接与非可靠连接

好,咱们今天聊点硬核的。RDMA的通信模型,说白了就是它怎么把数据从A搬到B,以及搬的过程中出了错怎么办。

我个人习惯把这一章分成两个维度来看:语义模型连接类型。语义模型解决的是“数据怎么被访问”,连接类型解决的是“通信靠不靠谱”。这两个维度组合起来,就构成了RDMA通信的完整图景。

4.1 通道语义 vs 内存语义

先说说语义。你想想看,传统的TCP/IP通信,数据从应用层一路往下,经过内核、协议栈、网卡,再发出去。接收端反过来。这个过程叫“通道语义”——数据像水流过管道一样,经过一个个中间节点。

通道语义(Channel Semantics)

  • 发送端:应用调用 send(),数据从用户缓冲区拷贝到内核缓冲区,再拷贝到网卡
  • 接收端:网卡收到数据,拷贝到内核缓冲区,应用调用 recv() 再拷贝到用户缓冲区
  • 特点:数据路径长,CPU参与多,延迟高

我在项目中遇到过,用通道语义做10Gbps的流量,CPU占用率直接飙到60%以上。说白了,CPU全在干拷贝的活,没空干正事了。

内存语义(Memory Semantics)

RDMA真正厉害的地方来了。内存语义允许一端直接读写另一端的内存,就像读写本地内存一样。不需要中间拷贝,不需要CPU参与。

  • RDMA Read:读取远端内存中的数据
  • RDMA Write:将本地数据写入远端内存
  • 原子操作:对远端内存做原子性的比较交换(CAS)或取加(FetchAdd)

核心区别:通道语义需要双方都参与(send/recv配对),内存语义可以单方面完成(一方读写,另一方完全不知情)。

嗯,这里要注意:内存语义虽然快,但需要提前注册内存区域(MR,Memory Region),并且远端必须暴露自己的内存地址。安全性上要小心。

4.2 可靠连接 vs 非可靠连接

连接类型解决的是“数据丢了怎么办”。RDMA提供了四种传输服务,但最常用的是两种:可靠连接(RC)非可靠连接(UC)

特性 可靠连接(RC) 非可靠连接(UC)
数据确认 有ACK/NACK 无确认
重传机制 硬件自动重传 无重传
保序性 严格保序 不保序
适用场景 存储、数据库、分布式锁 视频流、实时数据、可容忍丢包
连接数 QP数量多(每对连接一个QP) QP数量少(可复用)

我曾经在一个分布式存储项目里,一开始图省事用了非可靠连接。结果线上跑起来,偶尔丢一个包,整个数据块就坏了。排查了两天才发现是UC的锅。后来全部改成RC,问题消失。所以我的建议是:除非你明确知道丢包无所谓,否则一律用RC

避坑指南:我曾经见过有人用UC做数据库同步,结果数据不一致,查了三天才发现是丢包导致的。UC的丢包率虽然低(通常低于10^-12),但在大规模集群里,每天丢几个包是常态。

4.3 两种维度的组合

语义模型和连接类型可以组合出四种模式。但实际生产中,最常用的是两种:

  • RC + 内存语义:高性能存储、分布式数据库、AI训练
  • UC + 通道语义:视频流、日志收集、监控数据

我个人习惯在项目初期先用RC+内存语义,因为性能最好。等系统稳定了,再根据瓶颈分析,看能不能换成UC来节省QP资源。

4.4 核心逻辑图

下面这张图帮你理清整个通信模型的关系:

RDMA通信模型 语义模型 通道语义(Channel) 内存语义(Memory) 数据如何被访问 连接类型 可靠连接(RC) 非可靠连接(UC) 数据如何保证可靠 常用组合 RC + 内存语义 存储、数据库、AI训练 UC + 通道语义 视频流、日志、监控 选择组合时,先问自己:能接受丢包吗?需要低延迟吗?

我的经验:刚开始学RDMA时,别纠结于所有组合。先掌握RC + 内存语义这一种,它能覆盖80%以上的高性能场景。等用熟了,再去看UC和通道语义的细节。

4.5 实际代码片段

下面是一个RC模式下使用内存语义做RDMA Write的简化示例:

// 初始化QP(Queue Pair)
struct ibv_qp_init_attr qp_attr;
memset(&qp_attr, 0, sizeof(qp_attr));
qp_attr.qp_type = IBV_QPT_RC;  // 可靠连接
qp_attr.send_cq = cq;
qp_attr.recv_cq = cq;
qp_attr.cap.max_send_wr = 16;
qp_attr.cap.max_recv_wr = 16;
qp_attr.cap.max_send_sge = 1;
qp_attr.cap.max_recv_sge = 1;

struct ibv_qp *qp = ibv_create_qp(pd, &qp_attr);

// 注册内存区域(MR)
struct ibv_mr *mr = ibv_reg_mr(pd, buffer, buffer_size, 
                                IBV_ACCESS_LOCAL_WRITE | 
                                IBV_ACCESS_REMOTE_WRITE);

// 构造SGE(Scatter/Gather Element)
struct ibv_sge sge;
sge.addr = (uint64_t)buffer;
sge.length = buffer_size;
sge.lkey = mr->lkey;

// 构造WR(Work Request)
struct ibv_send_wr wr;
memset(&wr, 0, sizeof(wr));
wr.wr_id = 0;
wr.opcode = IBV_WR_RDMA_WRITE;  // RDMA Write操作
wr.send_flags = IBV_SEND_SIGNALED;
wr.wr.rdma.remote_addr = remote_addr;  // 远端内存地址
wr.wr.rdma.rkey = remote_rkey;         // 远端内存密钥
wr.sg_list = &sge;
wr.num_sge = 1;

// 提交WR到QP
struct ibv_send_wr *bad_wr;
ibv_post_send(qp, &wr, &bad_wr);

这段代码里,最关键的是 IBV_QPT_RCIBV_WR_RDMA_WRITE。前者告诉网卡“我要可靠连接”,后者告诉网卡“我要直接写远端内存”。

注意remote_addrrkey 必须提前通过带外方式(比如TCP连接)交换。RDMA本身不负责地址发现。

4.6 总结

好了,这一章的核心就这些。记住三句话:

  • 通道语义:像传统网络,需要双方配合
  • 内存语义:直接读写远端内存,CPU不参与
  • 可靠连接:硬件保证不丢包,适合关键业务

你想想看,RDMA之所以能成为高性能网络的标配,靠的就是内存语义带来的零拷贝,和可靠连接带来的硬件级保障。这两个特性缺一不可。

公众号:蓝海资料掘金营,微信deep3321