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