01
業(yè)務(wù)適配RDMA類型
RDMA傳輸?shù)倪m配,從業(yè)務(wù)場(chǎng)景的使用角度來看,大致可分為如下幾種類型。
場(chǎng)景一:機(jī)器學(xué)習(xí)、分布式存儲(chǔ)等場(chǎng)景,使用社區(qū)成熟的方案,如在機(jī)器學(xué)習(xí)場(chǎng)景中使用的NCCL、Tensorflow等框架中都適配了多種傳輸方式(包含tcp、rdma等),塊存儲(chǔ)Ceph中也同時(shí)支持tcp及rdma兩種通信模式,這種業(yè)務(wù)場(chǎng)景下業(yè)務(wù)側(cè)更多關(guān)注的是配置及使用,在IAAS基礎(chǔ)設(shè)施側(cè)將RDMA環(huán)境準(zhǔn)備好后,使能框架使用rdma的傳輸模式即可。
場(chǎng)景二:業(yè)務(wù)程序使用類似于RPC遠(yuǎn)程調(diào)用的通信方式,業(yè)務(wù)側(cè)需要將原有使用的RPC(大部分是GRPC)調(diào)用改為ORPC調(diào)用,在這種場(chǎng)景下業(yè)務(wù)和傳輸更像是兩個(gè)獨(dú)立的模塊,通過SDK的方式進(jìn)行調(diào)用,所以適配起來改造的代碼并不多,通常是業(yè)務(wù)層面修改調(diào)用RPC的接口方式。但由于業(yè)務(wù)方可能使用多種編程語言,RPC over RDMA需要進(jìn)行編程語言進(jìn)行適配。
場(chǎng)景三:業(yè)務(wù)程序通信是私有化通信,比如使用socket套接字結(jié)合epoll完全自有實(shí)現(xiàn)的一套通信機(jī)制。這種場(chǎng)景下其實(shí)改造也區(qū)分情況,即業(yè)務(wù)IO與網(wǎng)絡(luò)IO是否耦合,若比較解耦,代碼中抽象出一層類似于最新Redis代碼中ConnectionType這樣的架構(gòu)[2],那么只需要實(shí)現(xiàn)一套基于RDMA通信且符合Redis ConnectionType接口定義的新傳輸類型即可,改造量相對(duì)可控并且架構(gòu)上也比較穩(wěn)定;而若業(yè)務(wù)IO與網(wǎng)絡(luò)IO結(jié)合的較為緊密的情況下,這種場(chǎng)景下往往改造起來會(huì)比較復(fù)雜,改造的時(shí)候需要抽絲剝繭的找出業(yè)務(wù)與網(wǎng)絡(luò)之間的邊界,再進(jìn)行網(wǎng)絡(luò)部分的改造。
02
Redis RDMA改造方案分析
首先,以Redis改造為RDMA傳輸為例,分析基于RDMA傳輸?shù)膽?yīng)用程序改造邏輯與流程。
第一步是需要梳理出來Redis中與網(wǎng)絡(luò)傳輸相關(guān)的邏輯,這部分有比較多的參考資料,這里簡(jiǎn)單總結(jié)一下。
Redis中實(shí)現(xiàn)了一套R(shí)eactor模式的事件處理邏輯名為AE,其主要流程為:
1、使用epoll等機(jī)制監(jiān)聽各文件句柄,包括新建連接、以及已建立的連接等;
2、根據(jù)事件的不同調(diào)用對(duì)應(yīng)的事件回調(diào)處理;
3、循環(huán)進(jìn)行epoll loop并進(jìn)行處理。
參考[2]中分析了當(dāng)前redis的連接管理是圍繞connection這個(gè)對(duì)象進(jìn)行管理(可類比socket套接字的管理),抽象一層高于socket的connection layer,以便兼容不同的傳輸層,各個(gè)字段解釋如下。
type:各種連接類型的回調(diào)接口,定義了諸如事件回調(diào)、listen、accept、read、write等接口,類比tcp socket實(shí)現(xiàn)的proto_ops。
state:當(dāng)前連接的狀態(tài),如CONNECTING/ACCEPTING/CONNECTED/CLOSED等狀態(tài),類比TCP的狀態(tài)管理。
fd:連接對(duì)應(yīng)的文件句柄。
iovcnt:進(jìn)行iov操作的最大值。
private_data:保存私有數(shù)據(jù),當(dāng)前存放的是redis中client的指針。
conn_handler/write_handler/read_handler:分別對(duì)應(yīng)連接connect、write、read時(shí)的處理接口。
get_type: connection的連接類型,當(dāng)前redis已支持tcp、unix、tls類型,返回字符串。
init:在每種網(wǎng)絡(luò)連接模塊注冊(cè)時(shí)調(diào)用,各模塊私有初始化,如tcp、unix類型當(dāng)前未實(shí)現(xiàn),tls注冊(cè)時(shí)做了一些ssl初始化的前置工作。
ae_handler: redis中的網(wǎng)絡(luò)事件處理回調(diào)函數(shù),redis中使用aeCreateFileEvent為某個(gè)fd及事件注冊(cè)處理函數(shù)為ae_handler,當(dāng)redis的主循環(huán)aeMain中發(fā)現(xiàn)有響應(yīng)的事件時(shí)會(huì)調(diào)用ae_handler進(jìn)行處理,如在tcp連接類型中ae_handler為connSocketEventHandler,該函數(shù)分別處理了鏈接建立、鏈接可讀、鏈接可寫三種事件。
listen: 監(jiān)聽于某個(gè)IP地址和端口,在tcp連接類型中對(duì)應(yīng)的函數(shù)為connSocketListen,該函數(shù)主要調(diào)用bind、listen。
accept_handler: redis作為一個(gè)服務(wù)端,當(dāng)接收到客戶端新建連接的請(qǐng)求時(shí)候的處理函數(shù),一般會(huì)被.accept函數(shù)調(diào)用,比如在tcp連接類型中,connSocketAccept調(diào)用accept_handler,該方法被注冊(cè)為connSocketAcceptHandler,主要是使用accept函數(shù)接收客戶端請(qǐng)求,并調(diào)用acceptCommonHandler創(chuàng)建client。
addr: 返回連接的地址信息,主要用于一些連接信息的debug日志。
is_local:返回連接是否為本地連接,redis在protected模式下時(shí),調(diào)用該接口判斷是否為本地連接進(jìn)行校驗(yàn)。
conn_create/conn_create_accepted:創(chuàng)建connection,對(duì)于tcp連接類型,主要是申請(qǐng)connection的內(nèi)存,以及connection初始化工作。
shutdown/close:釋放connection的資源,關(guān)閉連接,當(dāng)某個(gè)redis客戶端移除時(shí)調(diào)用。
connect/blocking_connect:實(shí)現(xiàn)connection的非阻塞和阻塞連接方法,在tcp連接類型中,非阻塞連接調(diào)用aeCreateFileEvent注冊(cè)連接的可寫事件,繼而由后續(xù)的ae_handler進(jìn)行處理,實(shí)現(xiàn)非阻塞的連接;而阻塞連接則在實(shí)現(xiàn)時(shí)會(huì)等待連接建立完成。
accept:該方法在redis源碼中有明確的定義,可直接調(diào)用上述accept_handler,tcp連接類型中,該方法被注冊(cè)為connScoketAccept。
write/writev/read:和linux下系統(tǒng)調(diào)用write、writev、read行為一致,將數(shù)據(jù)發(fā)送至connection中,或者從connection中讀取數(shù)據(jù)至相應(yīng)緩沖區(qū)。
set_write_handler:注冊(cè)一個(gè)寫處理函數(shù),tcp連接類型中,該方法會(huì)注冊(cè)connection可寫事件,回調(diào)函數(shù)為tcp的ae_handler。
set_read_handler:注冊(cè)一個(gè)讀處理函數(shù),tcp連接類型中,該方法會(huì)注冊(cè)connection可讀事件,回調(diào)函數(shù)為tcp的ae_handler。
sync_write/sync_read/sync_readline:同步讀寫接口,在tcp連接類型中實(shí)現(xiàn)邏輯是使用循環(huán)讀寫。
has_pending_data:檢查connection中是否有尚未處理的數(shù)據(jù),tcp連接類型中該方法未實(shí)現(xiàn),tls連接類型中該方法被注冊(cè)為tlsHasPendingData,tls在處理connection讀事件時(shí),會(huì)調(diào)用SSL_read讀取數(shù)據(jù),但無法保證數(shù)據(jù)已經(jīng)讀取完成[3],所以在tlsHasPendingData函數(shù)中使用SSL_pending檢查緩沖區(qū)是否有未處理數(shù)據(jù),若有的話則交由下面的process_pending_data進(jìn)行處理。has_pending_data方法主要在事件主循環(huán)beforesleep中調(diào)用,當(dāng)有pending data時(shí),事件主循環(huán)時(shí)不進(jìn)行wait,以便快速進(jìn)行下一次的循環(huán)處理。
process_pending_data:處理檢查connection中是否有尚未處理的數(shù)據(jù),tcp連接類型中該方法未實(shí)現(xiàn),tls連接類型中該方法被注冊(cè)為tlsProcessPendingData,主要是對(duì)ssl緩沖區(qū)里面的數(shù)據(jù)進(jìn)行讀取。process_pending_data方法主要在事件主循環(huán)beforesleep中調(diào)用。
get_peer_cert:TLS連接特殊方法。
結(jié)合當(dāng)前代碼中tcp及tls實(shí)現(xiàn)方法,梳理出和redis connection網(wǎng)絡(luò)傳輸相關(guān)的流程:
圖:Redis Connection Call Graph
對(duì)于redis來說新增一個(gè)RDMA方式的傳輸方式,即是要將connection中的各種方法按照上述定義去使用RDMA編程接口去實(shí)現(xiàn)。RDMA編程一般采用CM管理連接加Verbs數(shù)據(jù)收發(fā)的模式,客戶端與服務(wù)端的交互邏輯大致如下圖所示,參考[16]。
圖:RDMA C/S Workflow
字節(jié)跳動(dòng)的pizhenwei同學(xué)目前在redis社區(qū)中已經(jīng)提交了redis over rdma的PR,參見[4],具體的代碼均在rdma.c這一個(gè)文件中。由于RDMA在做遠(yuǎn)程內(nèi)存訪問時(shí),需要使用對(duì)端的內(nèi)存地址,所以作者實(shí)現(xiàn)了一套R(shí)DMA客戶端與服務(wù)端的交互機(jī)制,用于通告對(duì)端進(jìn)行遠(yuǎn)程內(nèi)存寫入的內(nèi)存地址,參見[5]。
交互邏輯及說明如下:
1、增加了RedisRdmaCmd,用于Redis客戶端與服務(wù)端的控制面交互,如特性交換、Keepalive、內(nèi)存地址交換等;
2、在客戶端及服務(wù)端建立完成RDMA連接后,需要先進(jìn)行控制面的交互,當(dāng)內(nèi)存地址交換完成后,方可以進(jìn)行Redis實(shí)際數(shù)據(jù)的交互及處理;
3、控制面消息通過IBV_WR_SEND方式發(fā)送,Redis數(shù)據(jù)交互通過IBV_WR_RDMA_WRITE_WITH_IMM發(fā)送,通過方法的不同來區(qū)分是控制面消息還是Redis的實(shí)際數(shù)據(jù);
4、客戶端及服務(wù)端共享了一片內(nèi)存,則需要對(duì)內(nèi)存的使用管理,目前有三個(gè)變量用戶協(xié)同讀寫雙方的內(nèi)存使用。
tx.offset為RDMA發(fā)送側(cè)已經(jīng)對(duì)內(nèi)存寫入的偏移地址,從發(fā)送端角度看內(nèi)存已經(jīng)使用到了tx.offset位置,下次發(fā)送端再進(jìn)行RDMA寫入時(shí),內(nèi)存地址只能為tx.offset + 1;
rx.offset為RDMA接收側(cè)已經(jīng)收到的內(nèi)存偏移地址,雖然數(shù)據(jù)可能實(shí)際上已經(jīng)到了tx.offset的位置,但由于接收側(cè)需要去處理CQ的事件,才能獲取到當(dāng)前數(shù)據(jù)的位置,rx.offset是通過IMM中的立即數(shù)進(jìn)行傳遞的,發(fā)送側(cè)每次寫入數(shù)據(jù)時(shí),會(huì)將數(shù)據(jù)長(zhǎng)度,所以rx.offset <= tx.offset;
rx.pos 為接收方上層業(yè)務(wù)內(nèi)存的偏移地址,rx.pos <= rx.offset。
5、當(dāng)rx.pos等于memory.len時(shí),說明接收側(cè)內(nèi)存已滿,通過內(nèi)存地址交換這個(gè)RedisRdmaCmd進(jìn)行控制面交互,將tx.offset、rx.offset、rx.pos同時(shí)置零,重新對(duì)這片共享內(nèi)存協(xié)同讀寫。
Connection各方法的主要實(shí)現(xiàn)邏輯及分析如下:
listen:主要涉及RDMA編程圖示中l(wèi)isten、bind的流程,結(jié)合redis的.init相關(guān)調(diào)用流程,會(huì)將cm_channel中的fd返回給網(wǎng)絡(luò)框架AE,當(dāng)后續(xù)客戶端連接該fd時(shí),由AE進(jìn)行事件回調(diào),即后續(xù)的accepHandler。
accept_handler:該函數(shù)作為上述listen fd的事件回調(diào)函數(shù),會(huì)處理客戶端的連接事件,主要調(diào)用.accept方法進(jìn)行接收請(qǐng)求,并使用acceptCommonHandler調(diào)用后續(xù)的.set_read_handler注冊(cè)已連接的讀事件,參見圖Redis Connection Call Graph。
accept:要涉及RDMA編程圖示中accept的流程,處理RDMA_CM_EVENT_CONNECT_REQUEST、RDMA_CM_EVENT_ESTABLISHED等cm event,并進(jìn)行cm event的ack。
set_read_handler:設(shè)置連接可讀事件的回調(diào)為.ae_handler。
read_handler:實(shí)際處理中會(huì)被設(shè)置為readQueryFromClient。
read:從本地緩沖區(qū)中讀取數(shù)據(jù),該數(shù)據(jù)是客戶端通過遠(yuǎn)程DMA能力寫入。
set_write_handler:將write_handler設(shè)置為回調(diào)處理函數(shù),這里和tcp、tls實(shí)現(xiàn)的方式有所區(qū)別,并沒有注冊(cè)connection的可寫事件回調(diào),是因?yàn)镽DMA中不會(huì)觸發(fā)POLLOUT(可寫)事件,connection的寫由ae_handler實(shí)現(xiàn)。
write_handler:實(shí)際工作中被設(shè)置為sendReplyToClient。
write:將Redis的數(shù)據(jù)拷貝到RMDA的本地緩沖區(qū)中,通過ibv_post_send,這部分?jǐn)?shù)據(jù)會(huì)通過遠(yuǎn)程DMA能力寫入對(duì)端。
has_pending_data:檢查內(nèi)部的pending_list,在收到RDMA_CM_EVENT_DISCONNECTED等事件時(shí),會(huì)將當(dāng)前connection加入到pending_list中,由后續(xù)beforeSleep時(shí)調(diào)用process_pending_data進(jìn)行處理。
process_pending_data:檢查pending的connection,并調(diào)用read_handler讀取connection中的數(shù)據(jù)。
ae_handler:該方法有三個(gè)處理流程,第一是處理RDMA CQ事件,包括接收處理RedisRdmaCmd控制面消息,接收RDMA IMM類事件增加rx.offset;第二是調(diào)用read_handler和write_handler,這部分是與tcp、tls流程一致;第三是檢查rx.pos和rx.offset的值,若rx.pos == memory.len時(shí),發(fā)送內(nèi)存地址交換這個(gè)RedisRdmaCmd控制面消息。
03
Redis RDMA測(cè)試
Redis測(cè)試通常采取自帶的redis-benchmark工具進(jìn)行測(cè)試,該工具復(fù)用了redis中的ae處理邏輯,并調(diào)用hiredis進(jìn)行redis數(shù)據(jù)的解析,在參考[6]中fork并改造了一份基于RDMA的redis-benchmark,可直接編譯使用,接下來使用該工具進(jìn)行tcp及RDMA方式的性能測(cè)試對(duì)比。
在實(shí)際測(cè)試中使用的是同一個(gè)交換機(jī)下的兩臺(tái)服務(wù)器,傳輸方式是rocev2,經(jīng)過qperf的測(cè)試,tcp的latency為12us,rocev2的latency為4us。
▎3.1 單并發(fā)單線程
TCP方式
RedisServer:./src/redis-server --protected-mode no
RedisBenchmark:./src/redis-benchmark -h xx.xx.xx.xx -p 6379 -c 1 -n 500000 -t get
RDMA方式
RedisServer:./src/redis-server --loadmodule src/redis-rdma.so port=6379 bind=xx.xx.xx.xx --protected-mode no
RedisBenchmark:./src/redis-benchmark -h xx.xx.xx.xx -p 6379 -c 1 -n 500000 -t get --rdma
▎3.2多并發(fā)多線程
Redisbenchmark單線程4連接:
Redisbenchmark單線程8連接:
Redisbenchmark單線程16/32連接:
注:在我們的測(cè)試環(huán)境中16個(gè)連接時(shí),redis-benchmark已經(jīng)100%,再進(jìn)行增加連接數(shù)測(cè)試時(shí),qps也不會(huì)再增加。
Redisbenchmark 4線程4連接:
Redisbenchmark 4線程16連接:
Redisbenchmark 4線程32/64連接:
注:在我們的測(cè)試環(huán)境中4線程32連接時(shí),redis-server已經(jīng)100%,再進(jìn)行增加連接數(shù)測(cè)試時(shí),qps也不會(huì)再增加。
更多的連接和線程:
▎3.3測(cè)試總結(jié)
整體而言,在我們的測(cè)試環(huán)境下,redis服務(wù)能力rocev2(rdma)的傳輸方式相較tcp,有~50% 到 ~100%左右的能力提升。
可以發(fā)現(xiàn),由于rdma bypass了內(nèi)核協(xié)議棧,相同物理拓?fù)湎聄edis一次讀取時(shí)延下降了16us左右(見3.1單并發(fā)測(cè)試數(shù)據(jù)),這里額外做了一個(gè)測(cè)試,選取了另外一組相隔較遠(yuǎn)的機(jī)器進(jìn)行測(cè)試,發(fā)現(xiàn)讀取時(shí)延仍然縮小的是這個(gè)數(shù)量級(jí),見下圖。
rdma方式建鏈的時(shí)間較長(zhǎng),實(shí)際測(cè)試中連接數(shù)越多,redis-benchmark真正開始測(cè)試的時(shí)間越長(zhǎng)。
04
開源程序基于RDMA方案
▎4.1Tensorflow RDMA
Tensorflow是一個(gè)廣泛使用的深度學(xué)習(xí)框架,在Tensorflow中數(shù)據(jù)通常表示為Tensor張量,Tensor是一個(gè)多為數(shù)據(jù),可以在不同的設(shè)備之間進(jìn)行傳輸,以便進(jìn)行分布式計(jì)算。
在分布式系統(tǒng)中,Tensorflow可以通過網(wǎng)絡(luò)傳輸將Tensor從一個(gè)節(jié)點(diǎn)傳輸?shù)搅硪粋€(gè)節(jié)點(diǎn),從1.1版本開始支持RDMA傳輸,以下為其基于RDMA傳輸?shù)闹饕桨?,參考[7][8]。
在RDMA傳輸通道建立之前,使用基于tcp的grpc通道傳輸傳遞RDMA的內(nèi)存地址、MR key、服務(wù)地址等信息
內(nèi)存拷貝方案:
a)對(duì)于可以DMA的Tensor(包括CPU上的內(nèi)存或者GPU Direct的內(nèi)存),采用直接從源Tensor寫到目標(biāo)Tensor中的方案,實(shí)現(xiàn)內(nèi)存零拷貝
b)對(duì)于非DMA得Tensor,用protobuf序列化后,通過RDMA方式寫到接收端預(yù)先注冊(cè)的內(nèi)存中
c)對(duì)于不支持GPU Direct的Tensor,通過RDMA方式寫到接收端的CPU內(nèi)存,再在接收端通過拷貝的方式到GPU中,發(fā)送與接收CPU之間不存在內(nèi)存拷貝
內(nèi)部使用RdmaBuffer用于RDMA讀寫的內(nèi)存單元,RdmaBuffer有三個(gè)派生類,分別是RdmaAckBuffer、RdmaMessageBuffer和RdmaTensorBuffer,RdmaMessageBuffer負(fù)責(zé)發(fā)送 message ,比如請(qǐng)求一個(gè)tensor等等。一旦一個(gè)message被發(fā)送,message的接收方需要通過RdmaAckBuffer發(fā)送一個(gè)ack來釋放發(fā)送方的message buffer。一個(gè)RdmaAckBuffer和唯一的RdmaMessageBuffer綁定。RdmaTensorBuffer負(fù)責(zé)發(fā)送tensor,tensor的接收方需要返回一個(gè)message來釋放發(fā)送方的buffer
對(duì)于一個(gè)具體的recv和send流程如下:
a)接收側(cè)發(fā)送RDMA_MESSAGE_TENSOR_REQUEST消息,其中包含目的Tensor的地址,以用于發(fā)送側(cè)進(jìn)行RDMA寫入。
b)為避免在每個(gè)步驟中發(fā)送額外的元數(shù)據(jù)消息,為每個(gè)Tensor維護(hù)一個(gè)本地元數(shù)據(jù)緩存,僅在更改時(shí)才會(huì)更新,每個(gè)RDMA_MESSAGE_TENSOR_REQUEST將包含接收方從本地緩存中獲取的元數(shù)據(jù)。發(fā)送方將比較消息中的元數(shù)據(jù)和Tensor的新元數(shù)據(jù),如果元數(shù)據(jù)更改,發(fā)送側(cè)發(fā)送包含新元數(shù)據(jù)的RDMA_MESSAGE_META_DATA_RESPONSE。
c)當(dāng)接收方收到 RDMA_MESSAGE_META_DATA_RESPONSE 時(shí),將更新本地元數(shù)據(jù)緩存,重新分配結(jié)果/代理Tensor,重新發(fā)送Tensor請(qǐng)求。為了可追溯性,新的消息具有不同的名稱RDMA_MESSAGE_TENSOR_RE_REQUEST。
d)當(dāng)發(fā)送方收到 RDMA_MESSAGE_TENSOR_RE_REQUEST 時(shí),它將使用消息中指定的請(qǐng)求索引定位相關(guān)的 RdmaTensorResponse,并調(diào)用其 Resume方法,該方法將 RDMA 寫入之前克隆的Tensor的內(nèi)容,到重新請(qǐng)求中指定的新遠(yuǎn)程地址。
e)當(dāng)接收方接收到 RDMA 寫入時(shí),它將使用立即值作為請(qǐng)求索引,找到相關(guān)的 RdmaTensorRequest,然后調(diào)用其 RecvTensorContent方法,包含可能存在的內(nèi)存復(fù)制、反序列化等工作。
▎4.2BrpcRDMA
百度的brpc當(dāng)前的RDMA傳輸實(shí)現(xiàn)中,數(shù)據(jù)傳輸是使用RMDA_SEND_WITH_IMM進(jìn)行操作,這就要求接收端在接收數(shù)據(jù)前要先準(zhǔn)備好內(nèi)存并預(yù)先POST RECV。為了實(shí)現(xiàn)高效的內(nèi)存管理,brpc內(nèi)部實(shí)現(xiàn)了靜態(tài)內(nèi)存池,且在RDMA數(shù)據(jù)傳輸實(shí)現(xiàn)中做了如下幾點(diǎn)優(yōu)化,參考[9][10]。
數(shù)據(jù)傳輸零拷貝,要發(fā)送的所有數(shù)據(jù)默認(rèn)都存放在IOBuf的Block中,因此所發(fā)送的Block需要等到對(duì)端確認(rèn)接收完成后才可以釋放,這些Block的引用被存放于RdmaEndpoint::_sbuf中。而要實(shí)現(xiàn)接收零拷貝,則需要確保接受端所預(yù)提交的接收緩沖區(qū)必須直接在IOBuf的Block里面,被存放于RdmaEndpoint::_rbuf。注意,接收端預(yù)提交的每一段Block,有一個(gè)固定的大?。╮ecv_block_size)。發(fā)送端發(fā)送時(shí),一個(gè)請(qǐng)求最多只能有這么大,否則接收端則無法成功接收。
數(shù)據(jù)傳輸有滑動(dòng)窗口流控,這一流控機(jī)制是為了避免發(fā)送端持續(xù)在發(fā)送,其速度超過了接收端處理的速度。TCP傳輸中也有類似的邏輯,但是是由內(nèi)核協(xié)議棧來實(shí)現(xiàn)的,brpc內(nèi)實(shí)現(xiàn)了這一流控機(jī)制,通過接收端顯式回復(fù)ACK來確認(rèn)接收端處理完畢。為了減少ACK本身的開銷,讓ACK以立即數(shù)形式返回,可以被附在數(shù)據(jù)消息里。
數(shù)據(jù)傳輸邏輯的第三個(gè)重要特性是事件聚合。每個(gè)消息的大小被限定在一個(gè)recv_block_size,默認(rèn)為8KB。如果每個(gè)消息都觸發(fā)事件進(jìn)行處理,會(huì)導(dǎo)致性能退化嚴(yán)重,甚至不如TCP傳輸(TCP擁有GSO、GRO等諸多優(yōu)化)。因此,brpc綜合考慮數(shù)據(jù)大小、窗口與ACK的情況,對(duì)每個(gè)發(fā)送消息選擇性設(shè)置solicited標(biāo)志,來控制是否在發(fā)送端觸發(fā)事件通知。
▎4.3NCCLRDMA
NCCL的網(wǎng)絡(luò)傳輸實(shí)現(xiàn)是插件式的,各種不同的網(wǎng)絡(luò)傳輸只需要按照ncclNet中定義的方法去具體實(shí)現(xiàn)即可。
其中最主要的是isend、irecv及test方法,在調(diào)用 isend 或 irecv 之前,NCCL 將在所有緩沖區(qū)上調(diào)用 regMr 函數(shù),以便 RDMA NIC 準(zhǔn)備緩沖區(qū),deregMr 將用于注銷緩沖區(qū)。
以下是NCCL RDMA的實(shí)現(xiàn)部分邏輯,基于當(dāng)前NCCL最新版本分析,主要參考[11]及參考[12]
(當(dāng)前實(shí)現(xiàn)與參考中略有不同)。
在NCCL基于RDMA的傳輸實(shí)現(xiàn)中,目前的數(shù)據(jù)傳輸主要是通過RDMA_WRITE操作
由于發(fā)送端進(jìn)行RDMA_WRITE時(shí),需要預(yù)先知道對(duì)端的DMA地址,NCCL中發(fā)送/接收端是通過一個(gè)緩沖區(qū)ncclIbSendFifo進(jìn)行交互
ncclIbSendFifo是發(fā)送端申請(qǐng)的一塊內(nèi)存緩沖區(qū),在connect與accept階段通過傳統(tǒng)tcp socket的方式攜帶給接收端
在接收端異步進(jìn)行接收時(shí),recvProxyProgress調(diào)用irecv接口進(jìn)行接收,在RDMA的實(shí)現(xiàn)中對(duì)應(yīng)的是將本端DMA的地址通過ncclIbSendFifo RDMA_WRITE至發(fā)送端
發(fā)送端進(jìn)行發(fā)送時(shí),sendProxyProgress調(diào)用isend接口進(jìn)行發(fā)送,在RDMA中對(duì)應(yīng)的是從ncclIbSendFifo中獲取接收端的DMA地址,將上層的data直接RDMA_WRITE至接收端的DMA地址中
接收端維護(hù)本地的remFifoTail游標(biāo),每次接收時(shí)游標(biāo)后移一位,接收端會(huì)將idx設(shè)置為一個(gè)自增的索引,同時(shí)將上層的DMA地址通過ncclIbSendFifo攜帶給發(fā)送端
發(fā)送端維護(hù)本地的fifoHead游標(biāo),每次發(fā)送后游標(biāo)后移一位,發(fā)送端檢查fifo中元素的idx值是否為預(yù)期索引來判斷該fifo是否已經(jīng)被接收端設(shè)置過,即接收端的DMA地址已經(jīng)可以寫入
struct ncclIbSendFifo { uint64_t addr; int size; uint32_t rkey; uint32_t nreqs; uint32_t tag; uint64_t idx; }; // 發(fā)送端 ncclIbIsend: uint64_t idx = comm->fifoHead+1; if (slots[0].idx != idx) { *request = NULL; return ncclSuccess; } comm->fifoHead++; //接收端 ncclIbIrecv -> ncclIbPostFifo : localElem[i].idx = comm->remFifo.fifoTail+1; comm->remFifo.fifoTail++;
▎4.4Libvma及SMC-R方式
除了上述修改業(yè)務(wù)源碼的方案,業(yè)內(nèi)也有“零入侵”業(yè)務(wù)程序的方案,比如libvma及smc-r方式。
SMC-R:
smc-r(SMC over RDMA)是IBM在2017提交至linux kernel的一種兼容socket層,使用共享內(nèi)存技術(shù)、基于RDMA技術(shù)實(shí)現(xiàn)的高性能內(nèi)核網(wǎng)絡(luò)協(xié)議棧。smc-r的主要實(shí)現(xiàn)是在內(nèi)核態(tài)實(shí)現(xiàn)了一個(gè)新的af_smc協(xié)議族,基于RDMA verbs接口實(shí)現(xiàn)內(nèi)核proto_ops中的各方法。
smc-r支持fallback回退機(jī)制,在通信雙方最開始建立連接時(shí)是使用tcp握手(特定的tcp選項(xiàng))進(jìn)行協(xié)商是否雙方均支持SMC-R能力,當(dāng)協(xié)商不成功時(shí)fallback為原始的tcp通信。完成協(xié)議協(xié)商并建立連接后,協(xié)議棧為SMC-R socket分配一塊用于緩存待發(fā)送數(shù)據(jù)的環(huán)形緩沖區(qū)sndbuf和一塊用于緩存待接收數(shù)據(jù)的環(huán)形緩沖區(qū)RMB(Remote Memory Buffer)。
發(fā)送端應(yīng)用程序通過socket接口將待發(fā)送數(shù)據(jù)拷貝到本側(cè)sndbuf中,由SMC-R協(xié)議棧通過RDMA WRITE操作直接高效地寫入對(duì)側(cè)節(jié)點(diǎn)的RMB中。同時(shí)伴隨著使用RDMA SEND/RECV操作交互連接數(shù)據(jù)管理消息,用于更新、同步環(huán)形緩沖區(qū)中的數(shù)據(jù)游標(biāo)。
接收端SMC-R協(xié)議棧感知到RMB中填入新數(shù)據(jù)后,通過epoll等方式告知接收端應(yīng)用程序?qū)MB中的數(shù)據(jù)拷貝到用戶態(tài),完成數(shù)據(jù)傳輸。所以在SMC-R中,RMB充當(dāng)傳輸過程中的共享內(nèi)存。
圖 smc-r發(fā)送接收(轉(zhuǎn)自阿里云)
下面是一個(gè)基于smc-r通信的實(shí)際測(cè)試場(chǎng)景的協(xié)商交互抓包:
Libvma:
Libvma是Mellanox公司開源的一款高性能的用戶態(tài)網(wǎng)絡(luò)協(xié)議棧,它將socket的相關(guān)接口全部在用戶態(tài)空間實(shí)現(xiàn),實(shí)現(xiàn)對(duì)內(nèi)核的旁路,使用RDMA verbs接口直接調(diào)用網(wǎng)卡驅(qū)動(dòng),從而節(jié)省了大量的上下文數(shù)據(jù)拷貝,節(jié)省了 CPU 的資源降低了時(shí)延,業(yè)務(wù)在使用libvma時(shí)只需要使用LD_PRELOAD libvma.so替換原有的系統(tǒng)調(diào)用即可完成傳輸協(xié)議的替換。
Libvma內(nèi)部在tcp協(xié)議棧的實(shí)現(xiàn)上使用了lwip方案,重寫了epoll,使用了hugepage,內(nèi)部使用單獨(dú)的線程去輪詢RDMA CQ事件等方案,相較于內(nèi)核協(xié)議棧的實(shí)現(xiàn),在主機(jī)側(cè)的處理延遲有200%至500%的降低。
此外,在實(shí)際測(cè)試過程中發(fā)現(xiàn)libvma雖然使用的是RDMA verbs接口,但實(shí)際針對(duì)Mellanox mlx5系列驅(qū)動(dòng)的網(wǎng)卡是直接用戶態(tài)驅(qū)動(dòng)網(wǎng)卡,發(fā)送的仍然是原始基于tcp的以太報(bào)文,并不是rocev2的報(bào)文,具體討論可以見github上的issue參考[15]。
下面是基于libvma測(cè)試redis的場(chǎng)景,由于libvma bypass協(xié)議棧,并且重寫了epoll等其它特性,性能提升大概3倍:
總結(jié):
相較于業(yè)務(wù)使用raw verbs進(jìn)行源碼修改,libvma及smc-r方式可以提供“零入侵、零修改”源碼的優(yōu)勢(shì),但由于應(yīng)用程序在將數(shù)據(jù)提供給socket接口時(shí)仍然存在一次拷貝,所以性能上對(duì)比verbs方案來說有一定的損耗,對(duì)于想快速驗(yàn)證RDMA能力的業(yè)務(wù)是一個(gè)不錯(cuò)的POC驗(yàn)證方式。
目前阿里云的Alibaba Cloud Linux3默認(rèn)支持smc-r能力,結(jié)合阿里云的eRDMA能力網(wǎng)卡,可以使業(yè)務(wù)進(jìn)行透明無損的RDMA傳輸替換,減少cpu的使用率,降低一定的通信延時(shí)。但目前該能力在阿里云上屬于公測(cè)能力,生產(chǎn)穩(wěn)定性待驗(yàn)證,參考[14]。
libvma方式?jīng)]有l(wèi)inux社區(qū)的支持,并且更多的是針對(duì)Mellanox系列網(wǎng)卡的支持,在工業(yè)界使用的場(chǎng)景也不太多,目前在金融的高頻交易領(lǐng)域有一些使用嘗試。
05
總結(jié)與展望
前面主要分析和調(diào)研了一些開源應(yīng)用在進(jìn)行業(yè)務(wù)適配RDMA傳輸?shù)姆桨?,整體來看RDMA改造的方案是分為兩部分,分別為通信接口的改造以及RDMA內(nèi)存管理設(shè)計(jì)。 通信接口改造主要指將tcp socket的傳輸接口修改為ib verbs或者cm接口,這部分同時(shí)涉及到適配現(xiàn)有業(yè)務(wù)網(wǎng)絡(luò)事件的處理模型。 由于RDMA傳輸數(shù)據(jù)時(shí),需要預(yù)先將內(nèi)存注冊(cè)到HCA卡上,所以RDMA內(nèi)存管理會(huì)比較復(fù)雜,同時(shí)也是性能高低與否的關(guān)鍵。
1)數(shù)據(jù)傳輸時(shí)申請(qǐng)內(nèi)存,并進(jìn)行內(nèi)存注冊(cè),再進(jìn)行RDMA操作。顯然這種模式在代碼實(shí)現(xiàn)上最為簡(jiǎn)單,但是性能及效率最低,現(xiàn)有方案中很少有在fast path中使用這種內(nèi)存管理方案。
2)提前注冊(cè)好一大塊內(nèi)存,在上層業(yè)務(wù)需要發(fā)送數(shù)據(jù)時(shí),將數(shù)據(jù)拷貝至RDMA注冊(cè)好的內(nèi)存。這種模式性能相較第一種有提升,但存在一定的內(nèi)存拷貝。
3)使用內(nèi)存池,業(yè)務(wù)及RDMA的內(nèi)存使用同一塊。性能明顯是最優(yōu)的,但是實(shí)現(xiàn)邏輯較復(fù)雜,需要管理好內(nèi)存的申請(qǐng)及釋放、某些實(shí)現(xiàn)中通信雙方也需要做內(nèi)存使用量的協(xié)商。
結(jié)合前面應(yīng)用的RDMA方案,匯總?cè)缦卤恚?/p>
應(yīng)用名稱 | 網(wǎng)絡(luò)處理模型 | 內(nèi)存方案 | 其他特性 |
Redis (pr stage) |
1.適配原有的單線程reactor非阻塞模式 2.rdma無pollout時(shí)間,在業(yè)務(wù)邏輯中額外處理 3.網(wǎng)絡(luò)支持插件式,不同的傳輸模式實(shí)現(xiàn)相同的網(wǎng)絡(luò)方法 |
1.預(yù)注冊(cè)內(nèi)存,RDMA Write模式 2.DMA地址通過控制消息交互 3.應(yīng)用與RDMA之間存在拷貝 |
1.有控制面交互,如xferbuffer 2.控制面信息復(fù)用RDMA通道 |
Tensorflow | 異步發(fā)送、阻塞接收 |
1.RDMA Write模式 2.應(yīng)用及RDMA共享內(nèi)存池 3.通信雙方通過消息交互DMA地址 |
1.使用基于TCP的GRPC通道進(jìn)行RDMA鏈接的協(xié)商 2.有控制面消息交互,如metadata更新 3.控制面信息復(fù)用RDMA通道 |
BRPC | reactor模式 |
1.內(nèi)存池模式 2.使用RDMA Send模式 |
1.額外的流控機(jī)制 2.事件聚合 |
NCCL |
1.reactor模式 2.網(wǎng)絡(luò)支持插件式,不同的傳輸模式實(shí)現(xiàn)相同的網(wǎng)絡(luò)方法 |
1.RDMA Write模式 2.通信雙方通過一個(gè)共享fifo來交互具體的DMA地址 3.DMA地址是預(yù)先注冊(cè)的內(nèi)存 |
1.RDMA建立階段使用TCP鏈接進(jìn)行協(xié)商 |
隨著AI的火熱,國(guó)產(chǎn)DPU、GPU的高速發(fā)展,數(shù)據(jù)中心內(nèi)在高性能計(jì)算、機(jī)器學(xué)習(xí)、分布式存儲(chǔ)等場(chǎng)景下的業(yè)務(wù)也需要隨著硬件能力的提升去適配使用這些能力,RDMA因其諸多優(yōu)點(diǎn)目前已經(jīng)廣泛被應(yīng)用。調(diào)研學(xué)習(xí)現(xiàn)有的方案是為了更好地適配及修改自研的業(yè)務(wù),相信隨著越來越多業(yè)務(wù)場(chǎng)景下RDMA的使用,其相關(guān)生態(tài)及應(yīng)用方案也會(huì)越來越成熟。
審核編輯:劉清
-
處理器
+關(guān)注
關(guān)注
68文章
19259瀏覽量
229653 -
RPC
+關(guān)注
關(guān)注
0文章
111瀏覽量
11529 -
RDMA
+關(guān)注
關(guān)注
0文章
77瀏覽量
8945 -
TCP通信
+關(guān)注
關(guān)注
0文章
146瀏覽量
4221 -
TLS
+關(guān)注
關(guān)注
0文章
44瀏覽量
4248
原文標(biāo)題:RDMA在典型場(chǎng)景下的技術(shù)應(yīng)用分析與探索
文章出處:【微信號(hào):OSC開源社區(qū),微信公眾號(hào):OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論