[重磅]支持rdma通信的高性能的rpc库--yalantinglibs.coro_rpc
yalantinglibs的coro_rpc是基于C++20的协程的高性能的rpc库,提供了简洁易用的接口,让用户几行代码就可实现rpc通信,现在coro_rpc除了支持tcp通信之外还支持了rdma通信(ibverbs).
通过简单示例来感受一下rdma通信的coro_rpc.
示例
启动rpcserver
cpp
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请求
cpp
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发送,并利用底层的小包写入合并技术提高吞吐.
cpp
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");
}
}