【Ceph】Async RDMA网络通信性能优化
目录
网络通信模块的实现在源代码src/msg的目录下,该目录主要包括Messenger、Connection、Message、Dispatch等类,这些类定义了网络通信的框架与接口。
三个子目录simple、async、xio分别对应三种不同的网络通信模型。simple、xio在最新的版本中已经被废弃,async是目前系统默认的网络通信方式。
因此,本次网络通信优化的工作主要在async基础之上开展。
Reactor模型
为了处理高并发的网络I/O流,async模块采用了Reactor模型。在Reactor中,每一种handler会出处理一种event。这里会有一个全局的管理者selector,我们需要把channel注册感兴趣的事件,那么这个selector就会不断在channel上检测是否有该类型的事件发生,如果没有,那么主线程就会被阻塞,否则就会调用相应的事件处理函数即handler来处理。
Reactor模型原理
Reactor模型主要组件
Reactor模型的优点
响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的;编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销; 可以方便的通过增加Reactor实例个数来充分利用CPU资源;reactor框架本身与具体事件处理逻辑无关,具有很高的复用性;
Reactor模型的缺点
相比传统的简单模型,Reactor增加了一定的复杂性,因而有一定的门槛,并且不易于调试; Reactor模式需要底层的Synchronous Event Demultiplexer支持,比如Java中的Selector支持,操作系统的select系统调用支持,如果要自己实现Synchronous Event Demultiplexer可能不会有那么高效;Reactor模式在IO读写数据时还是在同一个线程中实现的,即使使用多个Reactor机制的情况下,那些共享一个Reactor的Channel如果出现一个长时间的数据读写,会影响这个Reactor中其他Channel的相应时间,比如在大文件传输时,IO操作就会影响其他Client的相应时间,因而对这种操作,使用传统的Thread-Per-Connection或许是一个更好的选择,或则此时使用Proactor模式。
有限状态机(Finite State Machine, FSM),是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
FSM模型把模型的多状态、多状态间的转换条件解耦;可以使维护变得容易,代码也更加具有可读性。
(FSM说明文章:https://blog.csdn.net/bandaoyu/article/details/111626406)
AsyncConnection连接建立过程中地状态迁移图参阅附录(Ⅰ)。
Async模块
Async工作原理
Async主要组件
AsyncMessenger | 管理网络连接 |
AsyncConnection | 网路通信连接,定义网络通信应用层协议 |
NetworkStack | 管理Worker对象及其对应地线程 |
Worker | 网络I/O流处理单元,每个Worker对应一个工作线程 |
ServerSocket/ServerSocketImpl | C/S模式监听套接字,向上屏蔽了各种不同的网络编程接口 |
ConnectedSocket/ConnectedSocketImpl | C/S模式连接套接字,向上屏蔽了各种不同的网络编程接口 |
EventCenter | 事件分发器,负责事件注册、事件分发 |
EventCallback | 当对应的事件发生时,由EventCenter负责回调 |
EventEpoll | 对epoll进行封装,轮询网络I/O事件 |
RDMA是Remote Direct Memory Access的缩写,通俗的说可以看成是远程的DMA技术,为了解决网络传输中服务器端数据处理的延迟而产生的。
RDMA三种不同的硬件实现
目前,有三种RDMA协议的实现:Infiniband、RoCE、iWARP。由于RoCE具备明显性能和成本优势,将逐渐成为市场主流。
Infiniband
支持RDMA的新一代网络协议。
由于这是一种新的网络技术,因此需要支持该技术的NIC和交换机。
RoCE
一个允许在以太网上执行RDMA的网络协议。
其较低的网络标头是以太网标头,其较高的网络标头(包括数据)是InfiniBand标头。
这支持在标准以太网基础设施(交换机)上使用RDMA。
只有网卡应该是特殊的,支持RoCE。
iWARP
一个允许在TCP上执行RDMA的网络协议。
IB和RoCE中存在的功能在iWARP中不受支持。
这支持在标准以太网基础设施(交换机)上使用RDMA。
只有网卡应该是特殊的,并且支持iWARP(如果使用CPU卸载),否则所有iWARP堆栈都可以在软件中实现,并且丧失了大部分RDMA性能优势。
https://houbb.github.io/2019/11/20/rdma-01-protocol
软件栈对比
| Infiniband (IB) | iWARP | RoCE |
标准组织 | IBTA | IETF | IBTA |
性能 | 最好 | 稍差 | 与IB相当 |
成本 | 高 | 中 | 低 |
网卡厂商 | Mellanox | Chelsio | Mellanox Emulex |
性能、成本对比
Infiniband网络最好,但网卡和交换机是价格也很高,然而RoCEv2和iWARP仅需使用特殊的网卡就可以了,价格也相对便宜很多。
Device/DeviceList | 抽象RDMA网卡,根据icfs.conf配置网卡参数 |
Infiniband | 封装IB Verbs网络编程接口及组件 |
RDMAConnectedSocketImpl | 仿socket连接套接字,采用伪fd实现网络I/O流的数据读写 |
RDMAConnTCP | 为RDMAConnectedSocketImpl服务,利用利用TCP/IP协议建立RDMA连接 |
RDMAServerSocketImpl | 仿socket服务套接字,定义服务接口 |
RDMAServerConnTCP | 实现RDMAServerSocketImpl接口,利用TCP/IP协议建立RDMA连接 |
RDMADispatcher | 轮询RDMA网络I/O流可读事件,将网络I/O流可读数据分发到对应RDMAConnectedSocketImpl 轮询RDMA网络I/O流可写事件,将网络I/O流可写数据分发到某个RDMAWorker |
RDMAWorker | 网络I/O流处理单元,每个RDMAWorker对应一个工作线程 |
RDMAStack | 管理RDMAWorker对象及其对应地线程 |
RDMA网络通信配置
在安装完网卡及其驱动之后,需要启动openibd,运行以下命令
service openibd start
chkconfig openibd on
对于IB网络,还需要启动opensmd,
service opensmd start
chkconfig opensmd on
网络启动之后,通过ibstat可以查看当前网络设备状态,
[root@server42 ~]# ibstat
CA 'mlx5_0'
CA type: MT4119
Number of ports: 1
Firmware version: 16.25.1020
Hardware version: 0
Node GUID: 0xb8599f0300bd417a
System image GUID: 0xb8599f0300bd417a
Port 1:
State: Active
Physical state: LinkUp
Rate: 40
Base lid: 0
LMC: 0
SM lid: 0
Capability mask: 0x04010000
Port GUID: 0xba599ffffebd417a
Link layer: Ethernet
CA 'mlx5_1'
CA type: MT4119
Number of ports: 1
Firmware version: 16.25.1020
Hardware version: 0
Node GUID: 0xb8599f0300bd417b
System image GUID: 0xb8599f0300bd417a
Port 1:
State: Active
Physical state: LinkUp
Rate: 40
Base lid: 0
LMC: 0
SM lid: 0
Capability mask: 0x04010000
Port GUID: 0xba599ffffebd417b
Link layer: Ethernet
通过ib_send_bw、ib_send_lat等工具可以测试网络带宽、延迟等性能。
Async提供了posix、rdma两种底层网络通信的方式,为了使用RDMA协议实现高带宽、低延迟的网络通信,需要配置rdma网络及软件定义参数。
使用rdma verbs创建QueuePair时,需要通信双方rdma设备的硬件信息,通常利用TCP/IP完成rdma连接双方的硬件参数的交换,因此需要配置集群网段,即
public_network = 100.7.45.0/20
cluster_network = 188.188.44.0/20
Async模块默认采用TCP/IP协议进行网络通信,需要改成rdma协议
ms_type = async
ms_async_transport_type = rdma
RDMA Verbs API按照设备名对设备进行操作,为了兼容Linux操作系统的命名,需要进行设备网络名到设备名的转换,Mellanox驱动提供了以下命令用于获取设备名与网络名之间的映射关系:
[root@server42 ~]# ibdev2netdev
i40iw0 port 1 ==> eno1 (Up)
mlx5_0 port 1 ==> enp59s0f0 (Up)
mlx5_1 port 1 ==> enp59s0f1 (Up)
据此,可以在配置环境中设置网络通信设备的名称,即
ms_async_rdma_public_device_name = enp59s0f0
ms_async_rdma_cluster_device_name = enp59s0f1
RoCE网络通信的实现
由于Infiniband与RoCE网络开发采用相同上层Verbs API,因此,IB网络通信代码可以完全在RoCE硬件上运行,整个代码几乎不需要改动。
为了能够对网络模块通信性能及优化效果进行定性、定量地深入研究,需要一套相对独立地RDMA网络通信性能测试工具。
async_client向async_server发送MSG_DATA_PING类型地数据包,async_server当受到2000个数据包之后会自动关闭连接,async_client监测到async_server端关闭之后,async_client会停止发送数据包,同时输出网络通信性能地统计信息。
async_server命令参数
--addr X ip to listen
--port X port to bind
async_client命令参数:
--msgs X number of msg to transport
--dszie X size of each msg to transport
--addr X ip of the server
--port X port of the server
这种测试工具其实是利用async_server端连接关闭作为消息数据包发送结束的标志,因为async_client感知到async_server连接关闭需要一定的时间,从而导致不能够准确地测试网络性能。
ceph_perf_msgr_server/ceph_perf_msgr_client
采用“请求-应答”模式,具体实现上与实际的OSD业务通信流程比较相似,因此可以较好的反映网络通信性能。
client向server端发送指定数量的MOSDOp消息,server端对于收到的每个MOSDOp消息,都会向client端发送MOSDOpReply消息。
但是,ceph_perf_msgr_client在ClientThread::entry()中存在一个Bug,即
void *entry()
{
lock.Lock();
for (int i = 0; i < ops; ++i)
{
if (inflight > uint64_t(concurrent))
{
cond.Wait(lock);
}
MOSDOp *m = new MOSDOp(client_inc.read(), 0, oid, oloc, pgid, 0, 0, 0);
m->write(0, msg_len, data);
inflight++;
conn->send_message(m);
//cerr << __func__ << " send m=" << m << std::endl;
}
由于调用write()函数之后,data内的数据会被清空,所以第一次调用之后,后面发送的数据包其实没有数据,需要改成
void *entry() {
lock.Lock();
for (int i = 0; i < ops; ++i) {
if (inflight > uint64_t(concurrent)) {
cond.Wait(lock);
}
MOSDOp *m = new MOSDOp(client_inc.read(), 0, oid, oloc, pgid, 0, 0, 0);
/*
m->write(0, msg_len, data);
*/
bufferlist msg_data(data);
m->write(0, msg_len, msg_data);
inflight++;
conn->send_message(m);
//cerr << __func__ << " send m=" << m << std::endl;
}
lock.Unlock();
msgr->shutdown();
return 0;
}
server 41 | server 42 | Client性能 | ||||
Server配置 | Client | |||||
并行数 | 并行数 | 队列深度 | request个数 | 耗时(us) | IOPS | 延时(us) |
1 | 1 | 32 | 100K | 4324564 | 23123.72 | 43.24564 |
1 | 2 | 32 | 100K | 3464919 | 57721.41 | 34.64919 |
1 | 4 | 32 | 100K | 4003939 | 99901.62 | 40.03939 |
1 | 8 | 32 | 100K | 5313240 | 150567.3 | 53.1324 |
1 | 16 | 32 | 100K | 11167830 | 143268.7 | 111.6783 |
1 | 32 | 32 | 100K | 27079705 | 118169.7 | 270.7971 |
1 | 64 | 32 | 100K | 68204271 | 93835.77 | 682.0427 |
1 | 64 | 64 | 100K | 66653653 | 96018.74 | 666.5365 |
server 41 | server 42 | Client性能 | ||||
Server配置 | Client | |||||
并行数 | 并行数 | 队列深度 | request个数 | 耗时(us) | IOPS | 延时(us) |
1 | 1 | 32 | 100K | 4952843 | 20190.424 | 49.52843 |
1 | 2 | 32 | 100K | 3712582 | 53870.864 | 37.12582 |
1 | 4 | 32 | 100K | 3664009 | 109170.038 | 36.64009 |
1 | 8 | 32 | 100K | 5526721 | 144751.291 | 55.26721 |
1 | 16 | 32 | 100K | 11834255 | 135200.737 | 118.3426 |
1 | 32 | 32 | 100K | 33805670 | 94658.6771 | 338.0567 |
1 | 64 | 32 | 100K | 67214894 | 95216.9916 | 672.1489 |
1 | 64 | 64 | 100K | 68273589 | 93740.4946 | 682.7359 |
从以上测试结果来看,主要有以下结论:
- 无论采用polling还是event轮询模式,网络性能几乎一样。
- 随着连接数的增大,网络性能逐渐达到性能瓶颈,最大IOPS为14万左右。
- 当连接数增大到一定程度,IOPS维持在9万左右。
- QueuePair发送队列
通过读取ms_async_rdma_receive_buffer与ms_async_rdma_send_buffers来配置注册内存大小,在Device::create_queue_pair()中,会根据ms_async_rdma_send_buffers来创建QueuePair.,即
Infiniband::QueuePair* Device::create_queue_pair(IcfsContext *cct, ibv_qp_type type)
{
Infiniband::QueuePair *qp = new QueuePair(
cct, *this, type, active_port->get_port_num(), srq, rx_cq, rx_cq, max_send_wr, max_recv_wr);
if (qp->init()) {
delete qp;
return NULL;
}
return qp;
}
但是ms_async_rdma_send_buffers设置较大会导致创建QueuePair失败,需要独立地设置注册内存以及QueuePair的创建,
Infiniband::QueuePair* Device::create_queue_pair(IcfsContext *cct, ibv_qp_type type)
{
//<nene>: use the "ms_async_rdma_qp_max_send_wr" instead of "max_send_wr"
/*
Infiniband::QueuePair *qp = new QueuePair(
cct, *this, type, active_port->get_port_num(), srq, rx_cq, rx_cq, max_send_wr, max_recv_wr);
*/
uint32_t qp_max_send_wr = cct->_conf->ms_async_rdma_qp_max_send_wr;
Infiniband::QueuePair *qp = new QueuePair(
cct, *this, type, active_port->get_port_num(), srq, rx_cq, rx_cq, qp_max_send_wr, max_recv_wr);
//</nene>
if (qp->init()) {
delete qp;
return NULL;
}
return qp;
}
经过修改之后,达到了以下效果,
- 注册内存buffer大小(ms_async_rdma_buffer_size)可由4096增加到131072
- 注册内存buffer数量(ms_async_rdma_send_buffers/ms_async_receive_buffers)可由1024增加到10240
- 解决了1M大小数据块测试过程中数据断流问题
- TCMalloc优化内存分配
TCMalloc全称Thread-Caching Malloc,即线程缓存的malloc,实现了高效的多线程内存管理,用于替代系统的内存分配相关的函数(malloc、free,new,new[]等)。
icfs_perf_msgr_server/icfs_perf_msgr_client测试工具没有采用TCMalloc,但是msg模块却使用了TCMAlloc进行优化,为了更加准确地描述网络模块地性能,需要对测试程序配置对TCMalloc的支持。
在测试程序中采用TCMalloc分配内存,测试结果如下,
server 41 | server 42 | Client性能 | ||||
Server配置 | Client | |||||
并行数 | 并行数 | 队列深度 | request个数 | 耗时(us) | IOPS | 延时(us) |
1 | 1 | 32 | 100K | 3208947 | 31162.87 | 32.08947 |
1 | 2 | 32 | 100K | 3432609 | 58264.72 | 34.32609 |
1 | 4 | 32 | 100K | 3349781 | 119410.8 | 33.49781 |
1 | 8 | 32 | 100K | 4502944 | 177661.5 | 45.02944 |
1 | 16 | 32 | 100K | 6317459 | 253266.4 | 63.17459 |
1 | 32 | 32 | 100K | 12766794 | 250650.2 | 127.6679 |
1 | 64 | 32 | 100K | 25002414 | 255975.3 | 250.0241 |
1 | 64 | 64 | 100K | 25310469 | 252859.8 | 253.1047 |
从上面地优化结果可以看出,
- 经过TCMalloc内存分配优化,最大IOPS增加近160%。
- 连接数增大到一定程度,整体性能不再提高,1S1C情况下,最大IOPS为25万左右。
每个AsyncMessenger根据ms_async_op_threads生成Worker线程,每个Worker线程包含一个事件分发器EventCenter来处理网络I/O流事件及其回调函数分发。
对于单个AsyncMessenger,增大ms_async_op_threads,生成多个Worker线程,研究不同情况地网络通信性能。
server 41 | server 42 | Client性能 | CPU占有率 | ||||
Server配置 | Client | ||||||
并行数 | 并行数 | 队列深度 | request个数 | 耗时(us) | IOPS | 延时(us) | |
1 | 1 | 32 | 100K | 3331462 | 30016.85 | 33.31462 | 69.1% |
1 | 2 | 32 | 100K | 3372494 | 59303.29 | 33.72494 | 133.4% |
1 | 4 | 32 | 100K | 3927981 | 101833.5 | 39.27981 | 231.1% |
1 | 8 | 32 | 100K | 6795892 | 117718.2 | 67.95892 | 284.8% |
1 | 16 | 32 | 100K | 11972282 | 133642 | 119.7228 | 343% |
1 | 32 | 32 | 100K | 19545797 | 163718.1 | 195.458 | 342.9% |
1 | 64 | 32 | 100K | 34377666 | 186167.4 | 343.7767 | 362.8% |
1 | 64 | 64 | 100K | 29780075 | 214908.8 | 297.8008 | 369.5% |
server 41 | server 42 | Client性能 | CPU占有率 | ||||
Server配置 | Client | ||||||
并行数 | 并行数 | 队列深度 | request个数 | 耗时(us) | IOPS | 延时(us) | |
1 | 1 | 32 | 100K | 3208947 | 31162.87 | 32.08947 | 53.5% |
1 | 2 | 32 | 100K | 3432609 | 58264.72 | 34.32609 | 114.6% |
1 | 4 | 32 | 100K | 3349781 | 119410.8 | 33.49781 | 249% |
1 | 8 | 32 | 100K | 4502944 | 177661.5 | 45.02944 | 356% |
1 | 16 | 32 | 100K | 6317459 | 253266.4 | 63.17459 | 616% |
1 | 32 | 32 | 100K | 12766794 | 250650.2 | 127.6679 | 654% |
1 | 64 | 32 | 100K | 25002414 | 255975.3 | 250.0241 | 649% |
1 | 64 | 64 | 100K | 25310469 | 252859.8 | 253.1047 | 691% |
从结果来看,Worker线程数由3增加到10,最大IOPS增加19%,但是相应地CPU占有率增加近87%。
Async模块采用Reactor模型,当网络I/O流事件发生时,EventCenter会调用对应对应地事件回调函数EventCallback进行处理,由于同一EventCenter内地事件回调函数地执行是顺序地,所以当存在较耗时地回调函数调用时,EventCenter::process_events就成为了整个网络通信性能瓶颈。
为了改进这种高性能网络I/O流模型,主要有两种思路:
- 增加EventCenter地数量,达到降低单个EventCenter内地事件回调数量地目的。
- 采用多线程模型,异步地执行同一EventCenter内的事件回调。
经过测试分析,多线程Reactor模型并未达到预期地效果,性能没有提升。
主要代码如下:
ThreadPool cb_tp;
class EventCallbackWQ : public ThreadPool::WorkQueue<EventCallback> {
list<EventCallback*> callbacks;
public:
EventCallbackWQ(time_t timeout, time_t suicide_timeout, ThreadPool *tp)
: ThreadPool::WorkQueue<EventCallback>("EventCenter::EventCallback", timeout, suicide_timeout, tp) {}
bool _enqueue(EventCallback *cb) {
auto iter = std::find(callbacks.begin(), callbacks.end(), cb);
if (iter == callbacks.end()) {
callbacks.push_back(cb);
}
return true;
}
void _dequeue(EventCallback *cb) {
assert(0);
}
bool _empty() {
return callbacks.empty();
}
EventCallback *_dequeue() {
if (callbacks.empty())
return NULL;
EventCallback *cb = callbacks.front();
callbacks.pop_front();
return cb;
}
void _process(EventCallback *cb, ThreadPool::TPHandle &handle) override {
if (cb) {
cb->do_request(cb->fd_or_id);
} else {
assert(0);
}
}
void _process_finish(EventCallback *cb) { }
void _clear() {
assert(callbacks.empty());
}
} cb_wq;
当网络I/O流存在可读数据的时候,EventCenter::process_events()会调用AsyncConnection::process()函数来读取消息数据。
在读取消息的data部分的时候,会不断地调用alloc_aligned_buffer()来申请内存,从而严重地影响程序地性能。为了提高内存分配地利用效率,通过封装boost::pool内存池来完成bufferlist中内存分配。
目前这项工作还在进行中,需要进一步地分析验证。
主要代码如下:
class buffer::boost_buffer : public buffer::raw {
boost::pool<> &mempool;
unsigned chunk_size = 0;
unsigned chunk_num = 0;
public:
explicit boost_buffer(unsigned l, boost::pool<> &p) : raw(l), mempool(p) {
if (len) {
chunk_size = p.get_requested_size();
chunk_num = len/chunk_size+1;
if (len%chunk_size==0) {
--chunk_num;
}
data = static_cast<char *>(mempool.ordered_malloc(chunk_num));
}
assert(data != nullptr);
inc_total_alloc(len);
bdout << "boost_buffer" << this << " alloc " << (void *)data << " " << l << " " << buffer::get_total_alloc() << bendl;
}
~boost_buffer() {
mempool.ordered_free(data, chunk_num);
dec_total_alloc(len);
bdout << "boost_buffer " << this << " free " << (void *)data << " " << buffer::get_total_alloc() << bendl;
}
raw* clone_empty() {
return new boost_buffer(len, mempool);
}
};
buffer::raw* buffer::create_boost_buffer(unsigned len, boost::pool<> &p) {
return new buffer::boost_buffer(len, p);
}
static void alloc_boost_buffer(boost::pool<> &p, unsigned len, bufferlist &data)
{
// create a buffer to read into that matches the data alignment
assert(len != 0);
bufferptr ptr
(
buffer::create_boost_buffer
(
len, p
)
);
data.push_back(std::move(ptr));
}
case STATE_OPEN_MESSAGE_READ_DATA_PREPARE:
{
// read data
unsigned data_len = le32_to_cpu(current_header.data_len);
unsigned data_off = le32_to_cpu(current_header.data_off);
if (data_len) {
// get a buffer
map<icfs_tid_t,pair<bufferlist,int> >::iterator p = rx_buffers.find(current_header.tid);
if (p != rx_buffers.end()) {
ldout(async_msgr->cct,10) << __func__ << " seleting rx buffer v " << p->second.second
<< " at offset " << data_off
<< " len " << p->second.first.length() << dendl;
data_buf = p->second.first;
// make sure it's big enough
if (data_buf.length() < data_len)
data_buf.push_back(buffer::create(data_len - data_buf.length()));
data_blp = data_buf.begin();
} else {
ldout(async_msgr->cct,20) << __func__ << " allocating new rx buffer at offset " << data_off << dendl;
//<nene>: Use the memepool
//alloc_aligned_buffer(data_buf, data_len, data_off);
alloc_boost_buffer(mempool, data_len, data_buf);
data_blp = data_buf.begin();
}
}
msg_left = data_len;
state = STATE_OPEN_MESSAGE_READ_DATA;
}
附录(Ⅰ):连接过程状态迁移图
参阅资料
- 罗军舟. <TCP/IP协议及网络编程技术>, 清华大学出版社.
- 游双. <Linux高性能服务器编程>, 机械工业出版社.
- 陈硕. <Linux多线程服务端编程>, 电子工业出版社。
- 谢希仁. <计算机网络>, 电子工业出版社。
- Mellanox. RDMA Aware Networks Programming User Manual.
- 罗剑锋. Boost程序库完全开发指南, 电子工业出版社.
- Douglas C. Schmidt. An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events.
- Stephen Prata. C++ Primer Plus, 人民邮电出版社.
- 严蔚敏. 数据结构(C语言描述), 清华大学出版社.
原文:https://blog.csdn.net/qq_26221775/article/details/107312062
RDMA 数据传输
https://zhuanlan.zhihu.com/p/55142557
3. RDMA Send/Receive
让我们看个简单的例子。在这个例子中,我们将把一个缓冲区里的数据从系统A的内存中搬到系统B的内存中去。这就是我们所说的消息传递语义学。接下来我们要讲的一种操作为SEND,是RDMA中最基础的操作类型。
3.1 第一步
第1步:系统A和B都创建了他们各自的QP的完成队列(CQ), 并为即将进行的RDMA传输注册了相应的内存区域(MR)。 系统A识别了一段缓冲区,该缓冲区的数据将被搬运到系统B上。系统B分配了一段空的缓冲区,用来存放来自系统A发送的数据。
3.2 第二步
第二步:系统B创建一个WQE并放置到它的接收队列(RQ)中。这个WQE包含了一个指针,该指针指向的内存缓冲区用来存放接收到的数据。系统A也创建一个WQE并放置到它的发送队列(SQ)中去,该WQE中的指针执行一段内存缓冲区,该缓冲区的数据将要被传送。
3.3 第三步
第三步:系统A上的HCA总是在硬件上干活,看看发送队列里有没有WQE。HCA将消费掉来自系统A的WQE, 然后将内存区域里的数据变成数据流发送给系统B。当数据流开始到达系统B的时候,系统B上的HCA就消费来自系统B的WQE,然后将数据放到该放的缓冲区上去。在高速通道上传输的数据流完全绕过了操作系统内核。
3.4 第四步
第四步:当数据搬运完成的时候,HCA会创建一个CQE。 这个CQE被放置到完成队列(CQ)中,表明数据传输已经完成。HCA每消费掉一个WQE, 都会生成一个CQE。因此,在系统A的完成队列中放置一个CQE,意味着对应的WQE的发送操作已经完成。同理,在系统B的完成队列中也会放置一个CQE,表明对应的WQE的接收操作已经完成。如果发生错误,HCA依然会创建一个CQE。在CQE中,包含了一个用来记录传输状态的字段。
我们刚刚举例说明的是一个RDMA Send操作。在IB或RoCE中,传送一个小缓冲区里的数据耗费的总时间大约在1.3µs。通过同时创建很多WQE, 就能在1秒内传输存放在数百万个缓冲区里的数据。
4. 总结
在这博客中,我们学习了如何使用RDMA verbs API。同时也介绍了队列的概念,而队列概念是RDMA编程的基础。最后,我们演示了RDMA send操作,展现了缓冲区的数据是如何在从一个系统搬运到另一个系统上去的。
2. 比较基于Socket与RDMA的通信
https://zhuanlan.zhihu.com/p/139548242
本篇的目的是通过对比一次典型的Socket和RDMA通信,直观的展示RDMA技术相比传统以太网的优势,尽量不涉及协议和软件实现细节。
假设本端的某个应用想把自己内存中的数据复制到对端某个应用可以访问的内存中(或者通俗的讲,本端要给对端发送数据),我们来看一下Socket和RDMA的SEND-RECV语义都做了哪些操作。
Socket
在描述通信过程时的软硬件关系时,我们通常将模型划分为用户层Userspace,内核Kernel以及硬件Hardware。Userspace和Kernel实际上使用的是同一块物理内存,但是处于安全考虑,Linux将内存划分为用户空间和内核空间。用户层没有权限访问和修改内核空间的内存内容,只能通过系统调用陷入内核态,Linux的内存管理机制比较复杂,本文不展开讨论。
一次典型的Socket通信过程的可以如下图所示进行分层:
一次收-发过程的步骤如下:
- 发送端和接收端通过Socket库提供的接口建立链接(就是在两个节点间建立了一条逻辑上的道路,数据可以沿这条道路从一端发送到另一端)并分别在内存中申请好发送和接收Buffer。
- 发送端APP通过Socket接口陷入内核态,待发送数据经过TCP/IP协议栈的一层层封装,最后被CPU复制到Socket Buffer中。
- 发送端通过网卡驱动,告知网卡可以发送数据了,网卡将通过DMA从Buffer中复制封装好的数据包到内部缓存中,然后将其发送到物理链路。
- 接收端网卡收到数据包后,将数据包放到接收Buffer中,然后CPU将通过内核中的TCP/IP协议栈对报文进行层层解析,取出有效的数据。
- 接收端APP通过Socket接口陷入内核态,CPU将数据从内核空间复制到用户空间。
Socket模型的数据流向大致是像上图这个样子,数据首先需要从用户空间复制一份到内核空间,这一次复制由CPU完成,将数据块从用户空间复制到内核空间的Socket Buffer中。内核中软件TCP/IP协议栈给数据添加各层头部和校验信息。最后网卡会通过DMA从内存中复制数据,并通过物理链路发送给对端的网卡。
而对端是完全相反的过程:硬件将数据包DMA拷贝到内存中,然后CPU会对数据包进行逐层解析和校验,最后将数据复制到用户空间。
上述过程中的关键点是需要CPU参与的把数据从用户空间拷贝到内核空间,以及同样需要CPU全程参与的数据包组装和解析,数据量大的情况下,这将对CPU将造成很大的负担。
下面我们看一下RDMA是如何将CPU“解放”出来的。
RDMA
同样是一端发送,一端接收的场景,我们将RDMA的分层模型分成两部分“控制通路”和“数据通路”,控制通路需要进入内核态准备通信所需的内存资源,而数据通路指的是实际数据交互过程中的流程。这一过程的分层关系如下图所示:
同Socket一样,我们简单描述下通信的过程:
- 发送端和接收端分别通过控制通路陷入内核态创建好通信所需要的内存资源。
- 在数据通路上,接收端APP通知硬件准备接收数据,告诉硬件将接收到的数据放在哪片内存中。
- 在数据通路上,发送端APP通知硬件发送数据,告诉硬件待发送数据位于哪片内存中。
- 发送端RDMA网卡从内存中搬移数据,组装报文发送给对端。
- 对端收到报文,对其进行解析并通过DMA将有效载荷写入内存。然后以某种方式通知上层APP,告知其数据已接收并妥善存放到指定位置。
这一过程中的数据流向大致如上图所示。通过和Socket的对比,我们可以明显看到,数据收发绕过了内核并且数据交换过程并不需要CPU参与,报文的组装和解析是由硬件完成的。
通过上面的对比,我们可以明显的体会到RDMA的优势,既将CPU从数据包封装和解析中解放出来,又减少了CPU拷贝数据的功率和时间损耗。需要注意的是,本文只描述了SEND-RECV流程,而RDMA技术所独有的,效率更高的WRITE/READ语义将在后续文章中介绍。
下一篇我们将介绍一些RDMA技术中的重要且基本的概念。