已经做了一段时间的RDMA相关开发了,陆续总结几篇相关文章。
RDMA是Remote Direct Memory Access的简称,目前主流的RDMA实现大致可以分为IB、RoCE、iWarp三类。
0x00 RDMA CM背景介绍
通过RDMA的verbs接口,我们可以创建不同类型的QP,如下:
- RC: Reliable Connection,可靠连接,可以理解为基于msg的TCP实现,有点像SCTP
- UC: Unreliable Connection,不可靠的连接,但和RC一样,需要握手
- UD: Unreliable Datagram,不可靠数据报,类似UDP
- RD: Reliable Datagram,顾名思义,应该还没有大量使用
由于UD目前仅支持RDMA send/recv语义,相较于write/read语义的延时稍高,因为需要对端CPU介入,合理开发,时延差距应该在1us内,至少在我所知的场景中应用不多,比较适合探测场景,比如rping,当然CM也是基于UD实现的。
RC和UC都是基于连接的,需要建联操作,该操作可以通过RDMA CM库完成,CM即Connection Manager的缩写。
如上文所述,CM是基于UD实现的,主要实现包含在ib cm/iw cm内核模块中,用户态的librdmacm只是一个agent,基于cm event进行驱动。
稍作总结,使用CM的优缺点如下:
优点:
- 避免重复性的工作
- 经过生产环境验证的细节实现,如可靠传输、超时机制等
- 生命周期比用户态长,可以较为妥善解决进程异常关闭场景的断连问题
缺点:
- 性能较差,单server cm约300~500连接/s
- 进程结束时,需要断连,导致进程hang,处理速率约1500 qp/s
- 不支持
REUSEPORT
- 主要功能在内核模块中实现,不利于二次开发和运维部署(毕竟大家都怕panic)
除常规的建连/断连类的操作外,CM还封装了一系列的verbs接口:
- 创建/销毁memory region
- 创建/销毁qp和cq
- 获取CM事件
总体而言,CM是在尽可能模拟TCP和UDP的操作,大概是作者意识到了verbs接口的复杂性,但从实现细节上来考虑,和传统的Socket API依旧存在不小区别。也正因为这个原因,你才能在librdmacm库中找到rsocket API,但听我一句劝,千万别在生产环境使用rsocket。
0x01 非CM托管CQ创建QP
就我个人而言,我更倾向于仅使用CM进行建联及部分的断链操作。在本文的需求中,我们会自行创建CQ,因为这些CQ可能需要做跨QP共享,同时又需要使用rdma_create_qp()
进行qp的创建。
先来看下rdma_create_qp()
的函数声明:
int rdma_create_qp(struct rdma_cm_id *id, struct ibv_pd *pd, struct ibv_qp_init_attr
*qp_init_attr);
id
是通过cm创建的,可以理解为它是内核cm的一个userspace cm agent,所以你可以在librdmacm的源码中找到很多以ucma_
为前缀的函数名,因为ucma正是它的缩写。pd
可以是自行创建的,亦可使用cm内置的,当需要依赖后者时,传入NULL
即可。qp_init_attr
即qp的初始化配置,按需填写即可。
int rdma_create_qp_ex(struct rdma_cm_id *id,
struct ibv_qp_init_attr_ex *attr) {
struct cma_id_private *id_priv;
struct ibv_qp *qp;
int ret;
...
ret = ucma_create_cqs(id, attr->send_cq || id->send_cq ? 0 : attr->cap.max_send_wr,
attr->recv_cq || id->recv_cq ? 0 : attr->cap.max_recv_wr);
if (ret)
return ret;
...
qp = ibv_create_qp_ex(id->verbs, attr);
if (!qp) {
ret = -1;
goto err1;
}
...
id->pd = qp->pd;
id->qp = qp;
return 0;
err2:
ibv_destroy_qp(qp);
err1:
ucma_destroy_cqs(id);
return ret;
}
根据上述函数实现来看,我们只要在id或者attr其中之一填写CQ指针,即可避免CM自行创建CQ。
然而实际测试下来并非如此,当遇到QP创建失败的场景时,例如ENOMEM
,均会调用ucma_destroy_cqs()
函数,如下代码所示,但凡我们在id字段中填写了CQ指针,都会触发CQ的销毁操作。
static void ucma_destroy_cqs(struct rdma_cm_id *id)
{
if (id->qp_type == IBV_QPT_XRC_RECV && id->srq)
return;
if (id->recv_cq) {
ibv_destroy_cq(id->recv_cq);
...
id->recv_cq = NULL;
}
if (id->recv_cq_channel) {
ibv_destroy_comp_channel(id->recv_cq_channel);
...
id->recv_cq_channel = NULL;
}
}
结合上述描述以及代码实现,CM创建CQ的行为就比较明显了:
- 如果id和attr字段中均未填写CQ信息,此时CM负责创建CQ
- 只要id填写了CQ信息,那么该CQ的销毁权就归CM所有
请注意,rdma_destroy_qp()
的逻辑也是如此,因此为了满足本文的需求,我们需要保持id中的CQ和CC指针均为NULL
,但需要在attr中准确填写相关信息,否则QP是无法被创建的。
0xFF 总结
坦白说,本文所描述的问题还是较难发现的,除非创建大量的QP,触发ibv_create_qp()
报错,进而导致连锁问题,通常在测试环境较难发现,若是带着该问题上线,就是妥妥的一个故障。
那么,归根到底,CM的实现是否合理?就我个人而言,显然是不合理的,理由如下:
- manual中没有描述
- 既然CQ并非
rdma_create_qp()
函数创建,那么该函数调用失败后,也不应由其销毁 - CM中并没有单独的CQ销毁API,谁能想到
rdma_create_qp()
函数竟然会销毁CQ ???