RDMA全称是Remote Direct Memory Access,中文翻译为远程直接数据存取。其核心理念就是通过RDMA硬件不经操作系统直接进行数据的传输,从而加快数据传输的性能。我使用的是InfiniBand RDMA[本文剩下地方RDMA都指的是InfiniBand],我从网上找到一个关于建立Server/Client通信的一个tutorial [see reference for copyright],虽然短,但是基本上关键点都进行了说明,这篇blog是我自己的根据tutorial完成后,对原文的翻译和自己的理解。

Idea

RDMA是一种双向Point to Point的数据传输方式。RDMA可以不经操作系统而直接传输数据,并且消耗非常小的CPU资源[zero-copy transfers]。其主要目的是为了在数据中心[data center]建立最高效的数据传输方式。连接通过建立一个两个application之间的channel完成,这个channel的两端[Endpoints]是Queue Pairs[QP]。每个endpoint都有一个Host Channel Adapter[HCA]来建立并维护和host rdma device的连接。一个QP包含两个Work Queue[WQ],一个是Send Queue[SQ],另一个是Receive Queue[RQ]。发送Work Request[WR]中,是基本的传递消息的方式。一旦一个WR被放到WQ里,其本质就是要在channel传输消息或者是执行一些函数。

Zero-copy describes computer operations in which the CPU does not perform the task of copying data from one memory area to another. This is frequently used to save CPU cycles and memory bandwidth when transmitting a file over a network. Zero-copy是CPU不需要把数据从一个地方拷贝到另一个地方,比如在内核层直接将文件内容传送给网络Socket,避免应用层数据拷贝,减小IO开销,提高传输效率

举两个例子来具体说明一下RDMA的工作流程:

Example 1: build communication

两个endpoint需要建立连接,具体来说是两个应用,一个本地应用[local application/sending application],一个远程应用[remote application/receiving application]。远程应用首先使用Post Receive Request verb建立一个或多个Receiving Working Request[RR],并将之放入自己的QP的RQ中,本地应用则使用Post Send Request verb来建立一个或多个Sending Working Request[SR],并放入自己的QP的SQ中。每个在SQ中的SR都是一个要发送到remote application的信息。发送的操作通过verbs中的SEND来完成,需要注意的是send application进行SEND操作时只知道接受方的QP的RQ。所以当远程应用的RQ收到对方的信息,就会将一个完成消息放入Completion Queue,并告知application对应的request已经完成[失败的话,也会发送完成消息,通知application]。

Example 2: transfer data

Infiniband RDMA还定义了 RDMA WRITE/ RDMA READ操作用来进行数据传输。

假设一个文件系统[file system],这里作为local application发送数据,主要是写一组数据到一个存储设备上[stroage device],这里可以看作是remote application。假设像Example 1那样,已经建立一个channel用作信息传输。此时,文件系统会生成一个data block,并将它放入一个在本地通过HCA注册的buffer中(会返回一个key,用于查找)。文件系统建立一个SEND operation,这个SEND包含了这个buffer地址和对应的key。同时,文件系统将一个RR放入RQ因为不久就会收到一个来及remote application返回的ack信息。

remote application(也就是存储设备)会在自己的RR里放一个RECEVICE operation用来接受这个消息,接受到消息后,就使用RDMA READ operation获得buffer的地址和key,这样存储设备就可以知道从file system的哪个位置获得数据(通过地址和key),从而来获得数据然并写入本地。操作完成后会返回一个ending status message给file system。READ WRITE的操作类似,首先在本机HCA的地址空间里,注册一段可用地址并返回key,通知等待写入的节点可以写的位置,然后就可以进行WRITE操作。

Design

Basic

首先这里使用的是Infiniband RDMA及其API,然后其中用到的rdma_cma.h是对infiniband的api的再封装,编译时需要link libibverbs和librdmacm。用RDMA建立连接,类似与使用socket:在server端建立listening socket,然后通过client端连接server。一旦建立连接,client和server都可以使用send()和recv()方法来传输数据。

RDMA的操作具体特点有:

  1. 不只可以使用send和recv两种方法,直接的read和write也是RDMA主要的功能。

  2. 所有的操作都是异步的,比如一个请求发送后,会在一段时间后收到完成通知。

  3. 在应用层,没有数据会被缓冲进内存。 Receives have to be posted before sends, 用于执行发送请求的内存在请求完成之前也不能被修改。

  4. 用于send/receive的内存空间必须预先注册。

建立RDMA的最关键步骤是建立queue pair(consisting of a send and receive operations, respectively)completion queue(we receive notification that our operations have completed)。一个连接的两端也就是server和client都有一个queue pair和一个completion queue,但是在一个application中多个queue pair对应一个completion queue。

建立连接的主要步骤是:

  1. 建立protection domain, completion queue和send-receive queue pair,其中queue pair,completion queue和memoryregistation都需要在这个protection domain中进行。

  2. 确定queue pair的address

  3. 使用该address和其他节点进行通信

  4. queue pair的状态在ready-to-receive (RTR)和ready-to-send (RTS)之间不断转换,用来完成send/receive的操作。

其中最后一个步骤,通过一个event-driven的connection manager来管理不同的event。

Server

Server主要用来接受client端发送来的消息,具体分为一下几个步骤:

  1. 建立event channel用来接受rdmacm event,比如connection-request和connection-established notification

  2. 绑定一个address

  3. 建立listener并且返回对应的端口和地址(之前绑定的)

  4. 等待connection-request

  5. 一旦收到request,建立protection domain, completion queue和queue pair。最重要的就是completion queue和queue pair。queue pair包含send queue(SQ)和receive queue(RQ),send queue中存储用户的send request(是一个会被发送到SQ中的Work Request,描述了需要传输的数据有多大、数据的目标位置以及传输的方式 (具体的操作码会确定传输方式)),同样的receive queue中存储用户的receive request(是一个会被发送到RQ中的Work Request。它描述了应该把到来的需要写的数据写在哪里。 需要注意的是,一个RDMA写操作会消耗一个RR)。completion queue是一个先进先出的队列,用于存储已经完成的work request信息。

  6. 接受connection-request

  7. 等待connection建立

  8. 允许operation

Client

Client端的主要步骤和server端相似

  1. 建立event channel用来接受rdmacm event,这里的event主要是server响应事件,端比如address-resolved,route-resolved

  2. 建立connection identifier,用来建立连接

  3. 确定一个peer address,这个地址用来把之前建立的connection identifier绑定到一个本地RDMA设备

  4. 建立protection domain, completion queue和queue pair

  5. 确定一条用来连接的链路

  6. 正式连接

  7. 等待server响应,然后建立连接

  8. 允许operation

Implementation && code

Server

按照Design提到8个步骤进行实现,server进行异步操作所以使用一个event-loop进行event管理。

  • 建立event channel
struct rdma_event_channel *ec = NULL;
ec = rdma_create_event_channel();
  • 绑定一个address
struct sockaddr_in addr = NULL;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;

这里主要完成了sockaddr的初始化工作,这里sin_family为AF_INET似乎是一个必选项,不能进行其他赋值。

  • 建立listener并返回port和address
struct rdma_cm_id *listener = NULL;
rdma_create_id(ec, &listener, NULL, RDMA_PS_TCP);
rdma_bind_addr(listener, (struct socketaddr *)&addr);

初始化一个rdma_cm_id类型的listen, rdma_create_id()创建一个位于ec的listener,RDMA_PS_TCP说明我们需要一个connection-oriented和reliable queue pair(通过TCP协议)。

  • 等待connection-request

因为所有操作都是异步的(asynchronous), 所以我们使用一个event-loop来接受rdmacm的request,返回ack并进行处理。具体RDMA事件类型有很多,网上可以查到,主要用到的就是RDMA_CM_EVENT_CONNECT_REQUEST,RDMA_CM_EVENT_ESTABLISHED,RDMA_CM_EVENT_DISCONNECTED

int on_event(struct rdma_cm_event *event) {
	int r = 0;
	if(event->event == RDMA_CM_EVENT_CONNECT_REQUEST) {
		r = on_connect_request(event->id);
	}
	else if(event->event == RDMA_CM_EVENT_ESTABLISHED) {
		r = on_connection(event->id->context);
	}
	else if(event->event == RDMA_CM_EVENT_DISCONNECTED) {
		r = on_disconnect(event->id);
	}
	else {
		die("one event: unkonw event.\n");
	}
	return 0;
}
  • 一旦收到request,建立protection domain, completion queue和queue pair

建立连接主要就是分配protection domain,然后建立completion queue和queue pair用于connect,主要由on_connect_request函数执行,其中包括build_context,build_qp_attr(),register_memory(),post_receives(),rdma_accept()。其中我们建立一个context struct来存储所有这些变量。

//just key code
struct context {
	struct ibv_context *ctx;
	struct ibv_pd *pd;
	struct ibv_cq *cq;
	struct ibv_comp_channel *comp_channel;
	pthread_t cq_poller_thread;
}	

static struct context *s_ctx = NULL;
s_ctx = (struct context *)malloc(sizeof(struct context));
s_ctx-> = ibv_alloc_pd(s_ctx->ctx);
//create completion channel first and then completion queue
s_ctx->comp_channel = ibv_create_comp_channel(s_ctx->ctx);
s_ctx->cq = ibv_create_cq(s_ctx->ctx, 10, NULL, s_ctx->comp_channel, 0);
  • 接受connection-request

使用completion channel来接受request completion的通知,在建立连接中,一旦收到completion的通知,就证明我们建立连接的操作已经完成。

  • 等待connection建立

由on_connection()函数负责,一旦收到RDMA_CM_EVENT_ESTABLISHED,就证明链接诶已经建立,然后可以发送send request()和接受receives request()

Client

  1. 建立event channel用来接受rdmacm event

和server类似先建立event channel。

struct rdma_event_channel *ec = NULL;
ec = rdma_create_event_channel(); 
  1. 建立connection identifier,用来建立连接

使用rdma_cm_id声明一个conn,用来建立连接,使用event channel

struct rdma_cm_id *conn = NULL;
rdma_create_id(ec, &conn, NULL, RDMA_PS_TCP);
  1. 确定一个peer address
struct addrinfo *addr;
rdma_resolve_addr(conn, NULL, addr->ai_addr, TIMEOUT_IN_MS);
  1. 建立protection domain, completion queue和queue pair

一旦从server端收到RDMA_CM_EVENT_ADDR_RESOLVED的event,client就会建立pd,cq,qp。建立的方式和server一样。

  1. 确定一条用来连接的route

从server端收到RDMA_CM_EVENT_ROUTE_RESOLVED的event后,就会确定一条route,然后发送连接请求。等待server端的确认信息,就可以建立connection

int on_route_resolved(struct rdma_cm_id *id) {
	struct rdma_conn_param cm_params;
	printf("route resovled. \n");
	memset(&cm_params, 0, sizeof(cm_params));
	rdma_connect(id, &cm_params);
}

Reference:

[1] Building an RDMA-Capable Application with IB Verbs. Tarick Bedeir, Schlumberger, 2010.