【Ceph】Async RDMA网络通信性能优化

目录

Infiniband

RoCE

iWARP

附录(Ⅰ):连接过程状态迁移图

参阅资料

RDMA 数据传输

3. RDMA Send/Receive

3.1 第一步

3.2 第二步

3.3 第三步

3.4 第四步

4. 总结


现有代码分析

网络通信模块的实现在源代码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

RDMA是Remote Direct Memory Access的缩写,通俗的说可以看成是远程的DMA技术,为了解决网络传输中服务器端数据处理的延迟而产生的。

                      

RDMA三种不同的硬件实现

目前,有三种RDMA协议的实现:Infiniband、RoCE、iWARP。由于RoCE具备明显性能和成本优势,将逐渐成为市场主流。

https://www.snia.org/sites/default/files/SNIA_ESF_Benefits_of_RDMA_In_Accelerating_Ethernet_Storage_Connectivity_Final_1_0.pdf 

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网络通信性能测试工具。

aysnc_server/async_client

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配置
RoCE,event

Client
RoCE,event

并行数
worker threads

并行数
numjobs

队列深度
concurrency

request个数
ios

耗时(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配置
RoCE,polling

Client
RoCE,polling

并行数
worker threads

并行数
numjobs

队列深度
concurrency

request个数
ios

耗时(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万左右。
  1. 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大小数据块测试过程中数据断流问题
  1. 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配置
RoCE,event,tcmalloc

Client
RoCE,event,tcmalloc

并行数
worker threads

并行数
numjobs

队列深度
concurrency

request个数
ios

耗时(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万左右。
  1. 工作线程数调优

每个AsyncMessenger根据ms_async_op_threads生成Worker线程,每个Worker线程包含一个事件分发器EventCenter来处理网络I/O流事件及其回调函数分发。

对于单个AsyncMessenger,增大ms_async_op_threads,生成多个Worker线程,研究不同情况地网络通信性能。

 

server 41

server 42

Client性能

CPU占有率

Server配置
RoCE,event,tcmalloc
ms_async_op_threads=3

Client
RoCE,event,tcmalloc
ms_async_op_threads=3

并行数
worker threads

并行数
numjobs

队列深度
concurrency

request个数
ios

耗时(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配置
RoCE,event,tcmalloc
ms_async_op_threads=10

Client
RoCE,event,tcmalloc
ms_async_op_threads=10

并行数
worker threads

并行数
numjobs

队列深度
concurrency

request个数
ios

耗时(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%

  1. 多线程Reactor模型

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;

 

  1. 消息接收缓存

当网络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;
        }



附录(Ⅰ):连接过程状态迁移图

 

 

参阅资料

  1. 罗军舟.  <TCP/IP协议及网络编程技术>, 清华大学出版社.
  2. 游双. <Linux高性能服务器编程>, 机械工业出版社.
  3. 陈硕. <Linux多线程服务端编程>, 电子工业出版社。
  4. 谢希仁. <计算机网络>, 电子工业出版社。
  5. Mellanox. RDMA Aware Networks Programming User Manual.
  6. 罗剑锋. Boost程序库完全开发指南, 电子工业出版社.
  7. Douglas C. Schmidt. An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events.
  8. Stephen Prata. C++ Primer Plus, 人民邮电出版社.
  9. 严蔚敏. 数据结构(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通信过程的可以如下图所示进行分层:

一次收-发过程的步骤如下:

  1. 发送端和接收端通过Socket库提供的接口建立链接(就是在两个节点间建立了一条逻辑上的道路,数据可以沿这条道路从一端发送到另一端)并分别在内存中申请好发送和接收Buffer。
  2. 发送端APP通过Socket接口陷入内核态,待发送数据经过TCP/IP协议栈的一层层封装,最后被CPU复制到Socket Buffer中。
  3. 发送端通过网卡驱动,告知网卡可以发送数据了,网卡将通过DMA从Buffer中复制封装好的数据包到内部缓存中,然后将其发送到物理链路。
  4. 接收端网卡收到数据包后,将数据包放到接收Buffer中,然后CPU将通过内核中的TCP/IP协议栈对报文进行层层解析,取出有效的数据。
  5. 接收端APP通过Socket接口陷入内核态,CPU将数据从内核空间复制到用户空间。

Socket模型的数据流向大致是像上图这个样子,数据首先需要从用户空间复制一份到内核空间,这一次复制由CPU完成,将数据块从用户空间复制到内核空间的Socket Buffer中。内核中软件TCP/IP协议栈给数据添加各层头部和校验信息。最后网卡会通过DMA从内存中复制数据,并通过物理链路发送给对端的网卡。

而对端是完全相反的过程:硬件将数据包DMA拷贝到内存中,然后CPU会对数据包进行逐层解析和校验,最后将数据复制到用户空间。

上述过程中的关键点是需要CPU参与的把数据从用户空间拷贝到内核空间,以及同样需要CPU全程参与的数据包组装和解析,数据量大的情况下,这将对CPU将造成很大的负担。

下面我们看一下RDMA是如何将CPU“解放”出来的。

RDMA

同样是一端发送,一端接收的场景,我们将RDMA的分层模型分成两部分“控制通路”和“数据通路”,控制通路需要进入内核态准备通信所需的内存资源,而数据通路指的是实际数据交互过程中的流程。这一过程的分层关系如下图所示:

同Socket一样,我们简单描述下通信的过程:

  1. 发送端和接收端分别通过控制通路陷入内核态创建好通信所需要的内存资源。
  2. 在数据通路上,接收端APP通知硬件准备接收数据,告诉硬件将接收到的数据放在哪片内存中。
  3. 在数据通路上,发送端APP通知硬件发送数据,告诉硬件待发送数据位于哪片内存中。
  4. 发送端RDMA网卡从内存中搬移数据,组装报文发送给对端。
  5. 对端收到报文,对其进行解析并通过DMA将有效载荷写入内存。然后以某种方式通知上层APP,告知其数据已接收并妥善存放到指定位置。

这一过程中的数据流向大致如上图所示。通过和Socket的对比,我们可以明显看到,数据收发绕过了内核并且数据交换过程并不需要CPU参与,报文的组装和解析是由硬件完成的

通过上面的对比,我们可以明显的体会到RDMA的优势,既将CPU从数据包封装和解析中解放出来,又减少了CPU拷贝数据的功率和时间损耗。需要注意的是,本文只描述了SEND-RECV流程,而RDMA技术所独有的,效率更高的WRITE/READ语义将在后续文章中介绍。

下一篇我们将介绍一些RDMA技术中的重要且基本的概念。

 

posted on 2022-10-04 01:24  bdy  阅读(408)  评论(0编辑  收藏  举报

导航