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时,别纠结于所有组合。先掌握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_RC 和 IBV_WR_RDMA_WRITE。前者告诉网卡“我要可靠连接”,后者告诉网卡“我要直接写远端内存”。
注意:remote_addr 和 rkey 必须提前通过带外方式(比如TCP连接)交换。RDMA本身不负责地址发现。
4.6 总结
好了,这一章的核心就这些。记住三句话:
- 通道语义:像传统网络,需要双方配合
- 内存语义:直接读写远端内存,CPU不参与
- 可靠连接:硬件保证不丢包,适合关键业务
你想想看,RDMA之所以能成为高性能网络的标配,靠的就是内存语义带来的零拷贝,和可靠连接带来的硬件级保障。这两个特性缺一不可。
公众号:蓝海资料掘金营,微信deep3321