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模型把模型的多状态、多状态间的转换条件解耦;可以使维护变得容易,代码也更加具有可读性。
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三种不同的硬件实现
目前,有三种RDMA协议的实现:Infiniband、RoCE、iWARP。由于RoCE具备明显性能和成本优势,将逐渐成为市场主流。
软件栈对比
|
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可以查看当前网络设备状态,
[[email protected] ~]# 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驱动提供了以下命令用于获取设备名与网络名之间的映射关系:
[[email protected] ~]# 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连接关闭需要一定的时间,从而导致不能够准确地测试网络性能。
采用“请求-应答”模式,具体实现上与实际的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语言描述), 清华大学出版社.