原文
[重磅]支持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");}
}