RPC設(shè)計應(yīng)該使用哪種網(wǎng)絡(luò)IO模型?
網(wǎng)絡(luò)通信在RPC調(diào)用中起到什么作用呢?RPC是解決進程間通信的一種方式。一次RPC調(diào)用,本質(zhì)就是服務(wù)消費者與服務(wù)提供者間的一次網(wǎng)絡(luò)信息交換的過程。服務(wù)調(diào)用者通過網(wǎng)絡(luò)IO發(fā)送一條請求消息,服務(wù)提供者接收并解析,處理完相關(guān)的業(yè)務(wù)邏輯之后,再發(fā)送一條響應(yīng)消息給服務(wù)調(diào)用者,服務(wù)調(diào)用者接收并解析響應(yīng)消息,處理完相關(guān)的響應(yīng)邏輯,一次RPC調(diào)用便結(jié)束了。可以說,網(wǎng)絡(luò)通信是整個RPC調(diào)用流程的基礎(chǔ)。
1 常見網(wǎng)絡(luò)I/O模型
兩臺PC機之間網(wǎng)絡(luò)通信,就是兩臺PC機對網(wǎng)絡(luò)IO的操作。
同步阻塞IO、同步非阻塞IO(NIO)、IO多路復(fù)用和異步非阻塞IO(AIO)。只有AIO為異步IO,其他都是同步IO。
1.1 同步阻塞I/O(BIO)
Linux默認所有socket都是blocking。
應(yīng)用進程發(fā)起IO系統(tǒng)調(diào)用后,應(yīng)用進程被阻塞,轉(zhuǎn)到內(nèi)核空間處理。之后,內(nèi)核開始等待數(shù)據(jù),等待到數(shù)據(jù)后,再將內(nèi)核中的數(shù)據(jù)拷貝到用戶內(nèi)存中,整個IO處理完畢后返回進程。最后應(yīng)用的進程解除阻塞狀態(tài),運行業(yè)務(wù)邏輯。
系統(tǒng)內(nèi)核處理IO操作分為兩階段:
- ? 等待數(shù)據(jù)系統(tǒng)內(nèi)核在等待網(wǎng)卡接收到數(shù)據(jù)后,把數(shù)據(jù)寫到內(nèi)核中
- ? 拷貝數(shù)據(jù)系統(tǒng)內(nèi)核在獲取到數(shù)據(jù)后,將數(shù)據(jù)拷貝到用戶進程的空間
在這兩個階段,應(yīng)用進程中IO操作的線程會一直都處于阻塞狀態(tài),若基于Java多線程開發(fā),每個IO操作都要占用線程,直至IO操作結(jié)束。
用戶線程發(fā)起read調(diào)用后就阻塞了,讓出CPU。內(nèi)核等待網(wǎng)卡數(shù)據(jù)到來,把數(shù)據(jù)從網(wǎng)卡拷貝到內(nèi)核空間,接著把數(shù)據(jù)拷貝到用戶空間,再把用戶線程叫醒。
1.2 IO多路復(fù)用(IO multiplexing)
高并發(fā)場景中使用最為廣泛的一種IO模型,如Java的NIO、Redis、Nginx的底層實現(xiàn)就是此類IO模型的應(yīng)用:
- ? 多路,即多個通道,即多個網(wǎng)絡(luò)連接的IO
- ? 復(fù)用,多個通道復(fù)用在一個復(fù)用器
多個網(wǎng)絡(luò)連接的IO可注冊到一個復(fù)用器(select),當用戶進程調(diào)用select,整個進程會被阻塞。同時,內(nèi)核會“監(jiān)視”所有select負責的socket,當任一socket中的數(shù)據(jù)準備好了,select就會返回。這個時候用戶進程再調(diào)用read操作,將數(shù)據(jù)從內(nèi)核中拷貝到用戶進程。
當用戶進程發(fā)起select調(diào)用,進程會被阻塞,當發(fā)現(xiàn)該select負責的socket有準備好的數(shù)據(jù)時才返回,之后才發(fā)起一次read,整個流程比阻塞IO要復(fù)雜,似乎更浪費性能。但最大優(yōu)勢在于,用戶可在一個線程內(nèi)同時處理多個socket的IO請求。用戶可注冊多個socket,然后不斷調(diào)用select讀取被激活的socket,即可達到在同一個線程內(nèi)同時處理多個IO請求的目的。而在同步阻塞模型中,必須通過多線程實現(xiàn)。
好比我們?nèi)ゲ蛷d吃飯,這次我們是幾個人一起去的,我們專門留了一個人在餐廳排號等位,其他人就去逛街了,等排號的朋友通知我們可以吃飯了,我們就直接去享用。
本質(zhì)上多路復(fù)用還是同步阻塞。
1.3 為何阻塞IO,IO多路復(fù)用最常用?
網(wǎng)絡(luò)IO的應(yīng)用上,需要的是系統(tǒng)內(nèi)核的支持及編程語言的支持。
大多系統(tǒng)內(nèi)核都支持阻塞IO、非阻塞IO和IO多路復(fù)用,但像信號驅(qū)動IO、異步IO,只有高版本Linux系統(tǒng)內(nèi)核支持。
無論C++還是Java,在高性能的網(wǎng)絡(luò)編程框架都是基于Reactor模式,如Netty,Reactor模式基于IO多路復(fù)用。非高并發(fā)場景,同步阻塞IO最常見。
應(yīng)用最多的、系統(tǒng)內(nèi)核與編程語言支持最為完善的,便是阻塞IO和IO多路復(fù)用,滿足絕大多數(shù)網(wǎng)絡(luò)IO應(yīng)用場景。
1.4 RPC框架選擇哪種網(wǎng)絡(luò)IO模型?
IO多路復(fù)用適合高并發(fā),用較少進程(線程)處理較多socket的IO請求,但使用難度較高。
阻塞IO每處理一個socket的IO請求都會阻塞進程(線程),但使用難度較低。在并發(fā)量較低、業(yè)務(wù)邏輯只需要同步進行IO操作的場景下,阻塞IO已滿足需求,并且不需要發(fā)起select調(diào)用,開銷比IO多路復(fù)用低。
RPC調(diào)用大多數(shù)是高并發(fā)調(diào)用,綜合考慮,RPC選擇IO多路復(fù)用。最優(yōu)框架選擇即基于Reactor模式實現(xiàn)的框架Netty。Linux下,也要開啟epoll提升系統(tǒng)性能。
2 零拷貝(Zero-copy)
2.1 網(wǎng)絡(luò)IO讀寫流程
應(yīng)用進程的每次寫操作,都把數(shù)據(jù)寫到用戶空間的緩沖區(qū),CPU再將數(shù)據(jù)拷貝到系統(tǒng)內(nèi)核緩沖區(qū),再由DMA將這份數(shù)據(jù)拷貝到網(wǎng)卡,由網(wǎng)卡發(fā)出去。一次寫操作數(shù)據(jù)要拷貝兩次才能通過網(wǎng)卡發(fā)送出去,而用戶進程讀操作則是反過來,數(shù)據(jù)同樣會拷貝兩次才能讓應(yīng)用程序讀到數(shù)據(jù)。
應(yīng)用進程一次完整讀寫操作,都要在用戶空間與內(nèi)核空間中來回拷貝,每次拷貝,都要CPU進行一次上下文切換(由用戶進程切換到系統(tǒng)內(nèi)核,或由系統(tǒng)內(nèi)核切換到用戶進程),這樣是不是很浪費CPU和性能呢?那有沒有什么方式,可以減少進程間的數(shù)據(jù)拷貝,提高數(shù)據(jù)傳輸?shù)男誓兀?/p>
這就要零拷貝:取消用戶空間與內(nèi)核空間之間的數(shù)據(jù)拷貝操作,應(yīng)用進程每一次的讀寫操作,都讓應(yīng)用進程向用戶空間寫入或讀取數(shù)據(jù),就如同直接向內(nèi)核空間寫或讀數(shù)據(jù)一樣,再通過DMA將內(nèi)核中的數(shù)據(jù)拷貝到網(wǎng)卡,或?qū)⒕W(wǎng)卡中的數(shù)據(jù)copy到內(nèi)核。
2.2 實現(xiàn)
是不是用戶空間與內(nèi)核空間都將數(shù)據(jù)寫到一個地方,就不需要拷貝了?想到虛擬內(nèi)存嗎?
虛擬內(nèi)存
零拷貝有兩種實現(xiàn):
mmap+write
通過虛擬內(nèi)存來解決。
sendfile
Nginx sendfile
3 Netty零拷貝
RPC框架在網(wǎng)絡(luò)通信框架的選型基于Reactor模式實現(xiàn)的框架,如Java首選Netty。那Netty有零拷貝機制嗎?Netty框架中的零拷貝和我之前講的零拷貝又有什么不同呢?
上節(jié)的零拷貝是os層的零拷貝,為避免用戶空間與內(nèi)核空間之間的數(shù)據(jù)拷貝操作,可提升CPU利用率。
而Netty零拷貝不大一樣,他完全站在用戶空間,即JVM上,偏向于數(shù)據(jù)操作的優(yōu)化。
Netty這么做的意義
傳輸過程中,RPC不會把請求參數(shù)的所有二進制數(shù)據(jù)整體一下子發(fā)送到對端機器,中間可能拆分成好幾個數(shù)據(jù)包,也可能合并其他請求的數(shù)據(jù)包,所以消息要有邊界。一端的機器收到消息后,就要對數(shù)據(jù)包處理,根據(jù)邊界對數(shù)據(jù)包進行分割和合并,最終獲得一條完整消息。
那收到消息后,對數(shù)據(jù)包的分割和合并,是在用戶空間完成,還是在內(nèi)核空間完成的呢?
當然是在用戶空間,因為對數(shù)據(jù)包的處理工作都是由應(yīng)用程序來處理的,那么這里有沒有可能存在數(shù)據(jù)的拷貝操作?可能會存在,當然不是在用戶空間與內(nèi)核空間之間的拷貝,是用戶空間內(nèi)部內(nèi)存中的拷貝處理操作。Netty的零拷貝就是為了解決這個問題,在用戶空間對數(shù)據(jù)操作進行優(yōu)化。
那么Netty是怎么對數(shù)據(jù)操作進行優(yōu)化的呢?
- ? Netty 提供了 CompositeByteBuf 類,它可以將多個 ByteBuf 合并為一個邏輯上的 ByteBuf,避免了各個 ByteBuf 之間的拷貝。
- ? ByteBuf 支持 slice 操作,因此可以將 ByteBuf 分解為多個共享同一個存儲區(qū)域的 ByteBuf,避免了內(nèi)存的拷貝。
- ? 通過 wrap 操作,我們可以將 byte[] 數(shù)組、ByteBuf、ByteBuffer 等包裝成一個 Netty ByteBuf 對象, 進而避免拷貝操作。
Netty框架中很多內(nèi)部的ChannelHandler實現(xiàn)類,都是通過CompositeByteBuf、slice、wrap操作來處理TCP傳輸中的拆包與粘包問題的。
Netty解決用戶空間與內(nèi)核空間之間的數(shù)據(jù)拷貝
Netty 的 ByteBuffer 采用 Direct Buffers,使用堆外直接內(nèi)存進行Socket的讀寫操作,最終的效果與我剛才講解的虛擬內(nèi)存所實現(xiàn)的效果一樣。
Netty 還提供 FileRegion 中包裝 NIO 的 FileChannel.transferTo() 方法實現(xiàn)了零拷貝,這與Linux 中的 sendfile 方式在原理一樣。
4 總結(jié)
零拷貝帶來的好處就是避免沒必要的CPU拷貝,讓CPU解脫出來去做其他的事,同時也減少了CPU在用戶空間與內(nèi)核空間之間的上下文切換,從而提升了網(wǎng)絡(luò)通信效率與應(yīng)用程序的整體性能。
Netty零拷貝與os的零拷貝有別,Netty零拷貝偏向于用戶空間中對數(shù)據(jù)操作的優(yōu)化,這對處理TCP傳輸中的拆包粘包問題有重要意義,對應(yīng)用程序處理請求數(shù)據(jù)與返回數(shù)據(jù)也有重要意義。