在基于 RDMA 的多机数据分发和接收场景中,数据的传输主要依赖于以下几个步骤和角色:
- 机器 A(发送方):通过 RDMA 将数据直接写入远程机器的内存中。
- 机器 B(接收方):接收数据,处理数据后将其转发或分发至其他机器(如机器 C)。
- 机器 C(接收方):从机器 B 接收数据。
RDMA 的特点是发送方和接收方不需要通过 CPU 完成数据传输,而是通过 RDMA NIC(网络接口卡)直接传输到远程内存。因此,操作主要集中在内存注册、队列对(QP)的操作、传输完成通知等步骤上。
时序图
下图展示了三台机器(A、B 和 C)之间的数据分发时序图,其中机器 A 向机器 B 发送数据,机器 B 将数据处理并转发到机器 C。
时序步骤:
- 内存注册:机器 A 和 B 在 RDMA 开始前都需要将其要使用的内存区域注册给 RDMA NIC。
- 队列对(QP)创建:机器 A 和 B 分别创建发送和接收队列对。
- 连接建立:机器 A 和 B 建立 RDMA 连接,确保它们可以直接访问彼此的内存。
- 数据传输(RDMA Write):机器 A 将数据直接写入机器 B 的内存(通过 RDMA Write 操作),数据传输无需操作系统干预。
- 完成通知:机器 B 的 RDMA NIC 通知其接收队列数据已经到达。
- 数据处理:机器 B 对接收到的数据进行处理。
- 数据转发(RDMA Send):机器 B 将数据通过 RDMA 发送到机器 C。
- 完成通知:机器 C 接收到数据后,RDMA NIC 通知其数据已经到达。
- 处理完成:机器 C 完成数据的进一步处理。
时序图(简要步骤)
机器 A 机器 B 机器 C
| | |
|<-------内存注册/队列对创建------>| |
| | |
|-------------连接建立----------->| |
| | |
|-------RDMA Write 发送数据------>| |
| | |
| <完成通知> |
| | |
| <数据处理> |
| | |
|---------RDMA Send 数据转发------>| |
| | |
| <完成通知> |
| | |
| <数据处理完成> |
| | |
时序解释:
-
内存注册:所有参与 RDMA 传输的机器都需要提前注册它们的内存,提供给 RDMA NIC,以便进行远程内存访问。
- 调用
ibv_reg_mr()
来注册内存区域。
- 调用
-
队列对(QP)创建:机器 A 和 B 各自创建
Queue Pair (QP)
,用于处理 RDMA 操作(例如发送、接收操作)。- 调用
ibv_create_qp()
创建队列对。
- 调用
-
连接建立:机器 A 和 B 之间建立 RDMA 连接,确保它们可以访问彼此的内存。此时,机器 A 可以发起数据传输。
- 通过
ibv_modify_qp()
将 QP 状态修改为RTS (Ready to Send)
。
- 通过
-
数据传输(RDMA Write):机器 A 通过 RDMA Write 直接将数据写入机器 B 的内存,绕过操作系统和 CPU 的干预。机器 B 此时无需主动操作。
- 机器 A 调用
ibv_post_send()
发起 RDMA Write 操作。
- 机器 A 调用
-
完成通知:机器 B 的 RDMA NIC 收到数据并存储在内存中,通知其接收队列操作完成。
- 机器 B 在接收完成后,会有一个
completion event
,通过轮询ibv_poll_cq()
检查接收完成。
- 机器 B 在接收完成后,会有一个
-
数据处理:机器 B 对接收到的数据进行处理,如必要的解析或修改。
-
数据转发(RDMA Send):机器 B 将处理后的数据通过 RDMA Send 操作发送到机器 C。机器 C 通过其接收队列接收数据。
- 机器 B 调用
ibv_post_send()
向机器 C 发送数据。
- 机器 B 调用
-
完成通知:机器 C 收到数据后,RDMA NIC 通知其数据已成功接收。机器 C 可以处理数据。
- 机器 C 同样通过
ibv_poll_cq()
检查发送和接收的完成状态。
- 机器 C 同样通过
-
处理完成:机器 C 处理收到的数据并完成整个数据传输过程。
回调和事件响应
在整个过程中,RDMA 的大部分操作都是异步完成的,因此需要通过事件轮询或中断的方式检查传输是否完成。这些事件通常通过 Completion Queue (CQ) 来通知发送或接收操作的完成。
struct ibv_wc wc;
while (ibv_poll_cq(cq, 1, &wc)) {
if (wc.status == IBV_WC_SUCCESS) {
printf("RDMA operation completed successfully\n");
} else {
printf("RDMA operation failed with status %d\n", wc.status);
}
}
小结
- 内存注册和队列对创建是 RDMA 操作的前提。
- RDMA Write 可以实现机器之间直接的数据传输,发送方直接将数据写入接收方的内存。
- 完成通知 是由 RDMA 的硬件通过 Completion Queue (CQ) 机制来异步通知传输的状态。
- 时序图中的基本动作包括内存注册、QP 创建、数据传输、完成通知和数据处理,形成了一个闭环的数据传输模型。
补充1: 如何实现RDMA
RDMA(Remote Direct Memory Access)简介
RDMA(远程直接内存访问)是一种技术,允许一台计算机直接访问另一台计算机的内存,而无需通过操作系统内核的干预。RDMA 的优点在于:
- 低延迟:绕过内核和网络栈,减少了数据传输的延迟。
- 高吞吐量:因为数据传输无需 CPU 参与,所以 CPU 可以处理其他任务,提供了更高的吞吐量。
- 低 CPU 占用率:CPU 不需要参与数据传输的管理,大大减少了系统开销。
RDMA 在高性能计算、分布式存储系统、大规模数据中心等领域有广泛的应用,特别是对延迟敏感且需要高带宽的应用,如:
- 高性能计算(HPC)
- 分布式数据库
- 存储系统(如 NVMe over Fabrics)
- 大规模数据中心网络(如 RoCE, iWARP)
RDMA 的三种主要协议
- InfiniBand (IB):用于高性能计算网络,提供了非常高的带宽和低延迟。
- RDMA over Converged Ethernet (RoCE):在数据中心网络中使用,以太网技术实现 RDMA。
- iWARP:基于标准 TCP/IP 协议栈实现的 RDMA,主要用于传统数据中心网络。
RDMA 的原理
RDMA 的核心是通过硬件支持直接将数据从发送方的内存传输到接收方的内存,不经过 CPU 或操作系统干预。具体流程如下:
-
内存注册:应用程序将内存缓冲区注册到网络适配器(NIC),允许它直接访问内存。这个过程确保了内存的物理地址不会改变,并提供给 RDMA NIC。
-
建立连接:RDMA 的通信双方通过
QP
(Queue Pair, 队列对)建立连接。每个QP
包括发送和接收队列。 -
数据传输:
- 发送方在本地内存中准备数据,并向 NIC 提交传输请求。
- NIC 将数据直接传输到接收方的内存,而无需经过操作系统内核。
-
完成通知:数据传输完成后,NIC 向应用程序发出完成事件。
RDMA 实现的方式
RDMA 通信依赖于硬件支持,比如带有 RDMA 功能的 NIC(如 Mellanox 的 ConnectX 系列)。RDMA 提供了几种操作方式:
- Send/Receive:基本的消息传递模式,发送端主动将数据传送到接收端。
- RDMA Read/Write:远程内存读写,发送端可以直接访问接收端的内存,类似于共享内存的操作。
RDMA 通信代码示例
以下是一个简化的基于 libibverbs
的 RDMA 编程模型。libibverbs
是 Linux 下的 RDMA 库,提供了直接访问 InfiniBand 硬件的接口。
1. 设置 RDMA 资源
#include <infiniband/verbs.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct rdma_context {
struct ibv_context *context;
struct ibv_pd *pd;
struct ibv_mr *mr;
struct ibv_cq *cq;
struct ibv_qp *qp;
char *buffer;
size_t size;
};
// 初始化 RDMA 资源
void init_rdma_context(struct rdma_context *ctx, size_t size) {
// 获取设备列表
struct ibv_device **dev_list = ibv_get_device_list(NULL);
struct ibv_device *device = dev_list[0];
// 打开设备
ctx->context = ibv_open_device(device);
// 创建保护域
ctx->pd = ibv_alloc_pd(ctx->context);
// 分配缓冲区
ctx->buffer = (char *)malloc(size);
ctx->size = size;
// 注册内存区域
ctx->mr = ibv_reg_mr(ctx->pd, ctx->buffer, ctx->size, IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE);
// 创建完成队列
ctx->cq = ibv_create_cq(ctx->context, 1, NULL, NULL, 0);
// 创建队列对 (Queue Pair)
struct ibv_qp_init_attr qp_init_attr = {
.send_cq = ctx->cq,
.recv_cq = ctx->cq,
.cap = {
.max_send_wr = 1,
.max_recv_wr = 1,
.max_send_sge = 1,
.max_recv_sge = 1
},
.qp_type = IBV_QPT_RC
};
ctx->qp = ibv_create_qp(ctx->pd, &qp_init_attr);
// 设置队列对的状态
struct ibv_qp_attr attr = {
.qp_state = IBV_QPS_INIT,
.port_num = 1
};
ibv_modify_qp(ctx->qp, &attr, IBV_QP_STATE | IBV_QP_PORT);
}
2. RDMA 发送数据
void rdma_send(struct rdma_context *ctx, char *data, size_t len) {
memcpy(ctx->buffer, data, len);
// 构建 Send Work Request
struct ibv_sge sge = {
.addr = (uintptr_t)ctx->buffer,
.length = len,
.lkey = ctx->mr->lkey
};
struct ibv_send_wr wr = {
.next = NULL,
.sg_list = &sge,
.num_sge = 1,
.opcode = IBV_WR_SEND
};
struct ibv_send_wr *bad_wr;
ibv_post_send(ctx->qp, &wr, &bad_wr);
}
3. RDMA 接收数据
void rdma_recv(struct rdma_context *ctx) {
struct ibv_wc wc;
// 等待接收完成
while (ibv_poll_cq(ctx->cq, 1, &wc)) {
if (wc.status == IBV_WC_SUCCESS) {
printf("Received message: %s\n", ctx->buffer);
}
}
}
4. 清理资源
void cleanup_rdma_context(struct rdma_context *ctx) {
ibv_destroy_qp(ctx->qp);
ibv_destroy_cq(ctx->cq);
ibv_dereg_mr(ctx->mr);
ibv_dealloc_pd(ctx->pd);
ibv_close_device(ctx->context);
free(ctx->buffer);
}
小结
- RDMA 提供了高效的内存到内存传输能力,绕过 CPU 和操作系统的干预。
- 通过 libibverbs 库,我们可以直接与 RDMA 硬件交互,实现高性能、低延迟的网络通信。
- RDMA 的应用场景广泛,尤其在高性能计算和大规模分布式系统中。
这个示例代码展示了如何使用 RDMA 进行数据传输,包括资源初始化、数据发送和接收操作。
补充2: 注册与队列对创建细则
为了更好地解释 RDMA 的内存注册与队列对(Queue Pair,QP)的实现原理,我们需要从 RDMA 的核心操作细节和具体协议机制入手。主要包括 内存注册(Memory Registration)、队列对的配对(QP Matching),以及在网络中如何传递数据的机制。
1. 内存注册(Memory Registration)
在 RDMA 中,内存注册是必不可少的一步。它的目的是将用户空间的内存区域直接暴露给 RDMA 硬件,使得 NIC 能够直接访问这些内存而无需操作系统的干预。操作系统需要将用户空间内存页映射到物理内存地址,并将物理地址提供给 RDMA 硬件。这个过程是通过 注册内存区域(Memory Region, MR) 来完成的。
内存注册的过程:
-
分配缓冲区:应用程序在用户空间中分配一块内存区域用于 RDMA 传输。
示例代码:
char *buffer = malloc(size);
-
注册内存:应用程序将缓冲区通过 ibverbs API 注册给 RDMA NIC。NIC 通过物理地址访问这块内存区域。
示例代码:
struct ibv_mr *mr; mr = ibv_reg_mr(pd, buffer, size, IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE);
pd
是 保护域(Protection Domain),用于隔离不同的 RDMA 资源(比如内存、QP)。IBV_ACCESS_LOCAL_WRITE
和IBV_ACCESS_REMOTE_WRITE
是内存的访问权限,允许本地和远程写操作。
-
生成 RDMA 地址句柄:注册完成后,RDMA 会返回一个句柄(即
mr
)。这个句柄包含了内存区域的逻辑地址(lkey)和远程地址(rkey),它们会被用在后续的数据传输中。
RDMA 数据结构(协议头)示例:
在 InfiniBand 或 RoCE 中,RDMA 数据包包含了内存地址的描述信息,具体如下:
- QP Number: 队列对编号,用于唯一标识 RDMA 连接。
- Memory Region: 注册的内存区域标识(
lkey
/rkey
),允许远程访问该区域。 - Remote Address: 远程内存的目标地址。
这些信息是 RDMA 数据包的一部分,它们被封装在网络层的协议头中,如 RoCE 使用的 Ethernet 帧,或者 InfiniBand 使用的 InfiniBand 协议头。
2. 队列对(Queue Pair,QP)的实现和配对原理
**队列对(QP)**是 RDMA 通信的基本单位。每个 QP 包含了两个队列:
- 发送队列(Send Queue,SQ):用于发送 RDMA 请求。
- 接收队列(Receive Queue,RQ):用于接收远程主机发送的数据。
队列对的建立流程:
-
创建保护域(PD):保护域用于将内存和 QP 进行分组,确保它们属于同一个上下文。所有的 QP 和 MR 都依赖于 PD。
struct ibv_pd *pd = ibv_alloc_pd(context);
-
创建完成队列(CQ):CQ 用于存放操作完成后的通知。当 RDMA 操作完成后,CQ 会记录操作的状态和结果。
struct ibv_cq *cq = ibv_create_cq(context, cq_size, NULL, NULL, 0);
-
创建队列对(QP):创建队列对,关联发送队列和接收队列,指定所需的操作模式(如 RDMA Read/Write,Send/Receive)。
struct ibv_qp_init_attr qp_init_attr = { .send_cq = cq, .recv_cq = cq, .cap = { .max_send_wr = 16, .max_recv_wr = 16, .max_send_sge = 1, .max_recv_sge = 1 }, .qp_type = IBV_QPT_RC // Reliable Connection }; struct ibv_qp *qp = ibv_create_qp(pd, &qp_init_attr);
-
队列对状态转移:QP 的状态会从
INIT
(初始化)转移到RTR
(Ready to Receive),然后再转移到RTS
(Ready to Send)。在状态转移的过程中,应用程序会提供远程机器的 QP 编号、LID(在 InfiniBand 中)和 GID(在 RoCE 中)。struct ibv_qp_attr attr = { .qp_state = IBV_QPS_RTR, .path_mtu = IBV_MTU_1024, .dest_qp_num = remote_qp_num, .rq_psn = 0, .ah_attr = { .is_global = 0, .dlid = remote_lid, .sl = 0, .src_path_bits = 0, .port_num = 1 } }; ibv_modify_qp(qp, &attr, IBV_QP_STATE | IBV_QP_DEST_QPN);
-
QP 配对与连接:在配对过程中,发送方和接收方会交换它们的 QP 编号、地址信息(如 IP 或 LID),以及内存区域的
rkey
和远程地址。通过这种方式,发送方可以访问接收方的内存。
配对原理(协议交互)
队列对的配对主要是通过交换 QP Number
和地址信息来完成的。这一过程通常依赖于 连接管理协议(如 RDMA CM) 或者传统的 TCP/IP 连接来传递这些信息。
例如,RDMA CM(Connection Manager)协议会在 RDMA 连接建立的过程中,通过 TCP 连接传递以下信息:
- QP 编号(Queue Pair Number)
- 内存注册的地址和
rkey
- 网络地址(如 GID 或 IP)
这些信息通过网络传输后,双方的 QP 就可以进行 RDMA 读写操作。
3. RDMA 数据传输过程
一旦队列对建立完成,内存注册成功,RDMA 通信的核心步骤就是数据传输。RDMA 的通信操作分为几种类型:
- RDMA Write:发送方直接将数据写入接收方的内存区域,不需要接收方的 CPU 参与。
- RDMA Read:发送方从接收方的内存区域读取数据。
- Send/Receive:消息传递模式,发送方将数据发送到接收方,接收方必须准备好接收缓冲区。
发送数据(RDMA Write 示例)
struct ibv_sge sge = {
.addr = (uintptr_t)buffer,
.length = size,
.lkey = mr->lkey
};
struct ibv_send_wr wr = {
.next = NULL,
.sg_list = &sge,
.num_sge = 1,
.opcode = IBV_WR_RDMA_WRITE,
.send_flags = IBV_SEND_SIGNALED,
.wr.rdma.remote_addr = remote_addr,
.wr.rdma.rkey = remote_rkey
};
struct ibv_send_wr *bad_wr;
ibv_post_send(qp, &wr, &bad_wr);
这里的核心是:
wr.rdma.remote_addr
是接收方的内存地址。wr.rdma.rkey
是接收方内存区域的rkey
,表示可以访问的远程内存。
接收数据(轮询 CQ)
struct ibv_wc wc;
int num_entries = ibv_poll_cq(cq, 1, &wc);
if (num_entries > 0 && wc.status == IBV_WC_SUCCESS) {
printf("RDMA operation completed\n");
}
通过轮询 完成队列(CQ) 来检测 RDMA 操作是否完成。
总结
- 内存注册:通过
ibv_reg_mr
注册用户空间内存,使得 RDMA NIC 能够直接访问物理内存。 - 队列对(QP):RDMA 通信的核心单位,包含发送和接收队列,用于发起数据传输操作。
- 配对机制:通过交换 QP 编号、地址和内存区域的
rkey
实现远程内存访问,配对过程依赖于 RDMA CM 或传统 TCP/IP 连接。 - RDMA 数据传输:使用 RDMA Write/Read 进行数据传输,绕过操作系统和 CPU,直接在机器之间传输数据。