RDMA通过kernel-bypass和协议栈offload两大核心技术,实现了远高于传统TCP/IP的网络通信性能。尽管RDMA的性能要远好于TCP/IP,但目前RDMA的实际落地业务场景却寥寥无几,这其中制约RDMA技术大规模上线应用的主要原因有两点:
- 主流互联网公司普遍选择RoCE(RDMA over Converged Ethernet)作为RDMA部署方案,而RoCE本质上是RDMA over UDP,在网络上无法保证不丢包。因此RoCE部署方案需要额外的拥塞控制机制来保证底层的无损网络,如PFC、ECN等,这给大规模的上线部署带来挑战。而且目前各大厂商对硬件拥塞控制的支持均还不完善,存在兼容性问题。
- RDMA提供了完全不同于socket的编程接口,因此要想使用RDMA,需要对现有应用进行改造。而RDMA原生编程API(verbs/RDMA_CM)比较复杂,需要对RDMA技术有深入理解才能做好开发,学习成本较高。
为了降低应用程序的改造成本,决定研发一个RDMA通信库,该通信库直接基于ibvebrs和RDMA_CM,避免对其他第三方库的调用。
本文主要对rdma编程的事件通知机制进行归纳总结。
传统socket编程中通常采用IO复用技术(select、poll、epoll等)来实现事件通知机制,那么对于rdma是否可以同样基于IO复用技术来实现事件通知机制?答案是完全可以。
1. RDMA_CM API(For Connection)
在rdma编程时,可以直接通过RDMA_CM API来建立RDMA连接。
对rdma_create_id函数进行分析,其主要创建了rdma_cm_id对象,并将其注册到驱动中。
int rdma_create_id(struct rdma_event_channel *channel,
struct rdma_cm_id **id, void *context,
enum rdma_port_space ps)
{
enum ibv_qp_type qp_type = (ps == RDMA_PS_IPOIB || ps == RDMA_PS_UDP) ?
IBV_QPT_UD : IBV_QPT_RC;
ret = ucma_init(); //查询获取所有IB设备,存放在cma_dev_array全局数组中;检测是否支持AF_IB协议
struct cma_id_private *id_priv =
ucma_alloc_id(channel, context, ps, qp_type); //创建并初始化id_priv对象:若未创建rdma_event_channel,那么调用rdma_create_event_channel创建一个。
CMA_INIT_CMD_RESP(&cmd, sizeof cmd, CREATE_ID, &resp, sizeof resp);
cmd.uid = (uintptr_t) id_priv;
cmd.ps = ps;
cmd.qp_type = qp_type;
ret = write(id_priv->id.channel->fd, &cmd, sizeof cmd); //将id_priv相关信息注册到内核驱动中,不做过多分析
*id = &id_priv->id; //返回rdma_cm_id对象
}
rdma_cm_id数据结构定义如下:
struct rdma_cm_id {
struct ibv_context *verbs; //ibv_open_device
struct rdma_event_channel *channel; //rdma_create_event_channel创建;For Setup connection
void *context; //user specified context
struct ibv_qp *qp; //rdma_create_qp,底层调用的是ibv_create_qp
struct rdma_route route;
enum rdma_port_space ps; //RDMA_PS_IPOIB or RDMA_PS_UDP or RDMA_PS_TCP
uint8_t port_num; //port数目
struct rdma_cm_event *event; //rdma_cm相关的事件events
struct ibv_comp_channel *send_cq_channel; //ibv_create_comp_channel创建;For data transfer
struct ibv_cq *send_cq; //发送CQ,通常和recv_cq是同一个CQ
struct ibv_comp_channel *recv_cq_channel; //ibv_create_comp_channel创建;For data transfer
struct ibv_cq *recv_cq; //接收CQ,通常和send_cq是同一个CQ
struct ibv_srq *srq;
struct ibv_pd *pd; //ibv_open_device
enum ibv_qp_type qp_type; //IBV_QPT_RC or IBV_QPT_UD
};
在创建rdma_cm_id时,如果预先没有创建rdma_event_channel,那么需要调用rdma_create_event_channel函数。
struct rdma_event_channel *rdma_create_event_channel(void)
{
struct rdma_event_channel *channel;
if (ucma_init()) //通过static局部变量,保证只做一次初始化
return NULL;
channel = malloc(sizeof *channel); //创建rdma_event_channel
if (!channel)
return NULL;
channel->fd = open("/dev/infiniband/rdma_cm", O_RDWR | O_CLOEXEC); //可以看出rdma_event_channel本质上就是一个fd
if (channel->fd < 0) {
goto err;
}
return channel;
err:
free(channel);
return NULL;
}
rdma_event_channel的定义如下:
struct rdma_event_channel {
int fd;
}
1.1 RDMA_CM原生事件通知实现(in block way)
static int cma_handler(struct rdma_cm_id *cma_id, struct rdma_cm_event *event);
ret = rdma_get_cm_event(channel, &event); //阻塞操作,直到有rdma_cm event发生才返回
if (!ret) {
ret = cma_handler(event->id, event); //处理事件
rdma_ack_cm_event(event); //ack event
}
static int cma_handler(struct rdma_cm_id *cma_id, struct rdma_cm_event *eve