[重磅]支持rdma
通信的高性能
的rpc
库–yalantinglibs.coro_rpc
yalantinglibs
的coro_rpc
是基于C++20
的协程
的高性能
的rpc
库,提供了简洁易用的接口,让用户几行代码
就可实现rpc
通信,现在coro_rpc
除了支持tcp
通信之外还支持了rdma
通信(ibverbs
).
通过简单示例
来感受一下rdma
通信的coro_rpc
.
示例
启动rpcserver
std::string_view echo(std::string str) { return str; }
coro_rpc_server server(/*thread_number*/ std::thread::hardware_concurrency(), /*端口*/ 9000);
server.register_handler<echo>();
server.init_ibv();
//初化rdma资源
server.start();
客户发送rpc请求
Lazy<void> async_request() {
coro_rpc_client client{};
client.init_ibv();
//初化rdma资源
co_await client.connect("127.0.0.1:9000");
auto result = co_await client.call<echo>("hello rdma");
assert(result.value() == "hello rdma");
}
int main() {
syncAwait(async_request());
}
几行代码
就可完成基于rdma
通信的rpcserver
和客户
了.如果用户需要设置更多rdma
相关的参数,则可在调用init_ibv
时传入配置
对象,在该对象中设置ibverbs
相关的各种参数.详见文档.
如果要允许tcp
通信该怎么做呢?不调用init_ibv()
即可,默认就是tcp
通信,调用了init_ibv()
之后才是rdma
通信.
benchmark
在180Gbrdma(RoCEV2)
带宽环境,两台主机之间对coro_rpc
做了一些性能测试,在高并发小包场景下qps
可到150w
;
发送稍大的数据包
时(256K
以上)不到10
个并发就可轻松打满带宽
.
请求数据大小 | 并发数 | 吞吐(Gb/s) |
P90(us) |
P99(us) |
qps |
---|---|---|---|---|---|
128B |
1 | 0.04 |
24 |
26 |
43394 |
- | 4 | 0.15 |
29 |
44 |
149130 |
- | 16 |
0.40 |
48 |
61 |
393404 |
- | 64 |
0.81 |
100 |
134 |
841342 |
- | 256 |
1.47 |
210 |
256 |
1533744 |
4K |
1 | 1.21 |
35 |
39 |
37017 |
- | 4 | 4.50 |
37 |
48 |
137317 |
- | 16 |
11.64 |
62 |
74 |
355264 |
- | 64 |
24.47 |
112 |
152 |
745242 |
- | 256 |
42.36 |
244 |
312 |
1318979 |
32K |
1 | 8.41 |
39 |
41 |
32084 |
- | 4 | 29.91 |
42 |
55 |
114081 |
- | 16 |
83.73 |
58 |
93 |
319392 |
- | 64 |
148.66 |
146 |
186 |
565878 |
- | 256 |
182.74 |
568 |
744 |
697849 |
256K |
1 | 28.59 |
81 |
90 |
13634 |
- | 4 | 100.07 |
96 |
113 |
47718 |
- | 16 |
182.58 |
210 |
242 |
87063 |
- | 64 |
181.70 |
776 |
864 |
87030 |
- | 256 |
180.98 |
3072 |
3392 |
88359 |
1M |
1 | 55.08 |
158 |
172 |
6566 |
- | 4 | 161.90 |
236 |
254 |
19299 |
- | 16 |
183.41 |
832 |
888 |
21864 |
- | 64 |
184.29 |
2976 |
3104 |
21969 |
- | 256 |
184.90 |
11648 |
11776 |
22041 |
8M |
1 | 78.64 |
840 |
1488 |
1171 |
- | 4 | 180.88 |
1536 |
1840 |
2695 |
- | 16 |
185.01 |
5888 |
6010 |
2756 |
- | 64 |
185.01 |
23296 |
23552 |
2756 |
- | 256 |
183.47 |
93184 |
94208 |
2733 |
具体benchmark
的代码在此.
RDMA
优化性能
RDMA
内存池
rdma
请求,需要预先注册内存收发数据
.在实际测试中,注册rdma
内存的成本远大于
内存拷贝.相比每次发送或接收数据
时注册rdma
内存.
最好是,用已注册好内存池缓存
的rdma
内存.每次发起请求时,将数据
分成多片来接收/发送
,每一片数据
的最大长度恰好是预先注册好的内存长度
,并从内存池中取出注册好的内存
,并在内存块
和实际数据
地址之间做一次拷贝
.
RNR
与接收缓冲队列
RDMA
直接操作远端内存
,当远端内存
未准备好时,就会触发一次RNR
错误,对RNR
错误,或断开,或休息一段时间.
显然避免RNR
错误是提高RDMA
传输性能和稳定度的关键.
coro_rpc
用如下策略解决RNR
问题:对每个连接,都准备一个接收缓冲队列
.队列中含若干块内存
(默认8块*256KB
),每当收到一块数据
传输完成的通知时,在缓冲队列
中,立即补充一块新的内存
,并把该块内存
提交到RDMA
的接收队列
中.
发送缓冲队列
在发送链路中,最天真思路是,先在RDMA
缓冲中拷贝数据
,再把它提交到RDMA
的发送队列
.当数据
写入到对端后,再重复上述步骤
发送下一块数据
.
上述步骤有两个瓶颈,第一个是如何并行化内存拷贝和网络传输
,第二个是,网卡发送完一块数据
,再到CPU
提交下一块数据
的这段时间,网卡实际上是空闲状态
,未能最大化
利用带宽.
为了提高发送数据
,需要引入发送缓冲
的概念.每次读写,不等待对端完成写入
,而是在将内存提交到RDMA
的发送队列
后就立即完成发送
,让上层代码
发送下个请求/数据块
,直到未完成发送的数据
达到发送缓冲队列
的上限.
此时才等待发送请求完成
,随后在RDMA
发送队列中提交新的内存块
.
对大数据包
,使用上述算法可同时内存拷贝和网络传输
,同时因为同时发送多块数据
,网卡发送完一片数据
到应用层提交新数据块
的这段时间,网卡可发送另外一块待发送的数据
,从而最大化
利用了带宽.
小包写入合并
rdma
在发送小数据包
时吞吐量相对较低
.对小包请求,一个既能提高吞吐又不引入额外延迟
的思路是按大数据包
合并多个小包.
假如应用层
提交了一个发送请求
,且此时发送队列
已满,则数据
不会立即发送到远端
,而是临时在缓冲中.此时假如应用层又提交了下个请求
,则可将这次请求的数据
合并写入到上次数据
临时的缓冲中,从而实现数据
的合并发送.
内联数据
某些rdma
网卡对小数据包
,可通过内联数据
的方式发送数据
,它不需要注册rdma
内存,同时可取得更好的传输性能
.
coro_rpc
在数据包
小于256
字节并且网卡支持内联数据
时,会用该方式发送数据
.
内存消费控制
RDMA
通信需要自己管理内存缓冲
.当前,coro_rpc
默认使用的内存片大小是256KB
.接收缓冲
初始大小为8
,发送缓冲
上限为2
,因此单连接的内存消费为10*256KB
约为2.5MB
.
用户可通过调整缓冲的大小
和缓冲
大小来控制内存的消费
.
此外,RDMA
内存池同样提供水位配置
,来控制内存消费
上限.当RDMA
内存池的水位过高时,试从该内存池
中取新内存的连接会失败并关闭.
使用连接池
高并发场景下,可通过coro_rpc
提供的连接池复用连接
,这可避免重复创建连接
.此外,因为coro_rpc
支持连接复用
,可将多个小数据包
请求提交到同一个连接
中,实现pipeline
发送,并利用底层的小包写入合并技术
提高吞吐.
static auto pool = coro_io::client_pool<coro_rpc::coro_rpc_client>::create(
conf.url, pool_conf);
auto ret = co_await pool->send_request(
[&](coro_io::client_reuse_hint, coro_rpc::coro_rpc_client& client) {
return client.send_request<echo>("hello");
});
if (ret.has_value()) {
auto result = co_await std::move(ret.value());
if (result.has_value()) {
assert(result.value()=="hello");
}
}