網(wǎng)上關于“零拷貝”原理相關的文章滿天飛,但你知道如何使用零拷貝嗎?
點擊上方“服務端思維”,選擇“設為星標”
回復”669“獲取獨家整理的精選資料集
回復”加群“加入全國服務端高端社群「后端圈」
零拷貝是中間件相關面試中必考題,本文就和大家一起來總結一下NIO拷貝的原理,并結合Netty代碼,從代碼實現(xiàn)層面近距離觀摩如何使用java實現(xiàn)零拷貝。
1、零拷貝實現(xiàn)原理
**“零拷貝”**其實包括兩個層面的含義:
拷貝 一份相同的數(shù)據(jù)從一個地方移動到另外一個地方的過程,叫拷貝。 零 希望在IO讀寫過程中,CPU控制的數(shù)據(jù)拷貝到次數(shù)為0。
在IO編程領域,當然是拷貝的次數(shù)越少越好,逐步優(yōu)化,將其拷貝次數(shù)將為0,最大化的提高性能。
那接下來我們循序漸進來看一下如何減少數(shù)據(jù)復制。
接下來我們將以RocketMQ消息發(fā)送、消息讀取場景來闡述IO讀寫過程中可能需要進行的數(shù)據(jù)復制與上下文切換。
1.1 傳統(tǒng)的IO讀流程
一次傳統(tǒng)的IO讀序列流程如下所示:
java應用中,如果要將從文件中讀取數(shù)據(jù),其基本的流程如下所示:
當broker收到拉取請求時發(fā)起一次read系統(tǒng)調用,此時操作系統(tǒng)會進行一次上下文的切換,從用態(tài)間切換到內核態(tài)。 通過直接存儲訪問器(DMA)從磁盤將數(shù)據(jù)加載到內核緩存區(qū)(DMA Copy,這個階段不需要CPU參與,如果是阻塞型IO,該過程用戶線程會處于阻塞狀態(tài)) 然后在CPU的控制下,將內核緩存區(qū)的數(shù)據(jù)copy到用戶空間的緩存區(qū)(由于這個是操作系統(tǒng)級別的行為,通常這里指的內存緩存區(qū),通常使用的是堆外內存),這里將發(fā)生一次CPU復制與一次上下文切換(從內核態(tài)切換到用戶態(tài)) 將堆外內存中的數(shù)據(jù)復制到應用程序的堆內存,供應用程序使用,本次復制需要經(jīng)過CPU控制。 將數(shù)據(jù)加載到堆空間,需要傳輸?shù)骄W(wǎng)卡,這個過程又要進入到內核空間,然后復制到sockebuffer,然后進入網(wǎng)卡協(xié)議引擎,從而進入到網(wǎng)絡傳輸中。該部分會在接下來會詳細介紹。
溫馨提示:RocketMQ底層的工作機制并不是上述模型,是經(jīng)過優(yōu)化后的讀寫模型,本文將循序漸進的介紹優(yōu)化過程。
1.2 傳統(tǒng)的IO寫流程
一次傳統(tǒng)的IO寫入流程如下圖所示:
核心關鍵步驟如下:
在broker收到消息時首先會在堆空間中創(chuàng)建一個堆緩存區(qū),用于存儲用戶需要寫入的數(shù)據(jù),然后需要將jvm堆內存中數(shù)據(jù)復制到操作系統(tǒng)內存(CPU COPY) 發(fā)起write系統(tǒng)調用,將用戶空間中的數(shù)據(jù)復制到內存緩存區(qū),**此過程發(fā)生一次上下文切換(用戶態(tài)切換到內核態(tài))**并進行一次CPU Copy。 通過直接存儲訪問器(DMA)將內核空間的數(shù)據(jù)寫入到磁盤,并返回結果,此過程發(fā)生一次DMA Copy 與一次上下文切換(內核態(tài)切換到用戶態(tài))
1.3 讀寫優(yōu)化技巧
從上面兩張流程圖,我們不能看出讀寫處理流程中存在太多復制,同樣的數(shù)據(jù)需要被復制多次,造成性能損耗,故IO讀寫通常的優(yōu)化方向主要為:減少復制次數(shù)、減少用戶態(tài)/內核態(tài)切換次數(shù)。
1.3.1 引入堆外內存
jvm堆空間中數(shù)據(jù)要發(fā)送到內核緩存區(qū),通常需要先將jvm堆空間中的數(shù)據(jù)拷貝到系統(tǒng)內存(一個非官方的理解,用C語言實現(xiàn)的本地方法調用中,首先需要將堆空間中數(shù)據(jù)拷貝到C語言相關的存儲結構),故提高性能的第一個措施:使用堆外內存。
不過堆外內存中的數(shù)據(jù),通常還是需要從堆空間中獲取,從這個角度來看,貌似提升的性能有限。
1.3.2 引入內存映射(MMap與write)
通過引入內存映射機制,減少用戶空間與內核空間之間的數(shù)據(jù)復制,如下圖所示:
內存映射的核心思想就是將內核緩存區(qū)、用戶空間緩存區(qū)映射到同一個物理地址上,可以減少用戶緩存區(qū)與內核緩存區(qū)之間的數(shù)據(jù)拷貝。
但由于內存映射機制并不會減少上下文切換次數(shù)。
1.3.3 大名鼎鼎鼎sendfile
在Linux 2.1內核引入了sendfile函數(shù)用于將文件通過socket傳送。
注意sendfile的傳播方向:使用于將文件中的內容直接傳播到Socket,通常使用客戶端從服務端文件中讀取數(shù)據(jù),在服務端內部實現(xiàn)零拷貝。
在1.3.1中介紹客戶端從服務端讀取消息的過程中,并沒有展開介紹從服務端寫入到客戶端網(wǎng)絡中的過程,接下來看看sendfile的數(shù)據(jù)拷貝圖解:
sendfile的主要特點是在內核空間中通過DMA將數(shù)據(jù)從磁盤文件拷貝到內核緩存區(qū),然后可以直接將內核緩存區(qū)中的數(shù)據(jù)在CPU控制下將數(shù)據(jù)復制到socket緩存區(qū),最終在DMA的控制下將socketbufer中拷貝到協(xié)議引擎,然后經(jīng)網(wǎng)卡傳輸?shù)侥繕硕恕?/p>
sendfile的優(yōu)勢(特點):
一次sendfile調用會只設計兩次上下文切換,比read+write減少兩次上下文切換。 一次sendfile會存在3次copy,其中一次CPU拷貝,兩次DMA拷貝。
1.3.4 Linux Gather
Linux2.4內核引入了gather機制,用以消除最后一次CPU拷貝,即不再將內核緩存區(qū)中的數(shù)據(jù)拷貝到socketbuffer,而是將內存緩存區(qū)中的內存地址、需要讀取數(shù)據(jù)的長度寫入到socketbuffer中,然后DMA直接根據(jù)socketbuffer中存儲的內存地址,直接從內核緩存區(qū)中的數(shù)據(jù)拷貝到協(xié)議引擎(注意,這次拷貝由DMA控制)。
從而實現(xiàn)真正的零拷貝。
2、結合Netty談零拷貝實戰(zhàn)
上面講述了“零拷貝”的實現(xiàn)原理,接下來將嘗試從Netty源碼去探究在代碼層面如何使用“零拷貝”。
從網(wǎng)上的資料可以得知,在java nio提供的類庫中真正能運用底層操作系統(tǒng)的零拷貝機制只有FileChannel的transferTo,而在Netty中也不出意料的對這種方式進行了封裝,其類圖如下:
其主要的核心要點是FileRegion的transferTo方法,我們結合該方法再來介紹DefaultFileRegion各個核心屬性的含義。
上述代碼并不復雜,我們不難得出如下觀點:
首先介紹DefaultFileRegion的核心屬性含義: File f 底層抽取數(shù)據(jù)來源的底層磁盤文件 FileChannel file 底層文件的文件通道。 long position 數(shù)據(jù)從通道中抽取的起始位置 long count 需要傳遞的總字節(jié)數(shù) long transfered 已傳遞的字節(jié)數(shù)量。 核心要點是調用java nio FileChannel的transferTo方法,底層調用的是操作系統(tǒng)的sendfile函數(shù),即真正的零拷貝。 調用一次transferTo方法并不一定能將需要的數(shù)據(jù)全部傳輸完成,故該方法返回已傳輸?shù)淖止?jié)數(shù),是否需要再次調用該方法的判斷方法:已傳遞的字節(jié)數(shù)是否等于需要傳遞的總字節(jié)數(shù)(transfered == count)
接下來我們看一下FileRegion的transferTo在netty中的調用鏈,從而推斷一下Netty中的零拷貝的觸發(fā)要點。
在Netty中代表兩個類型的通道:
EpollSocketChannel 基于Epoll機制進行事件的就緒選擇機制。
NioSocketChannel
基于select機制的事件就緒選擇。
在Netty中調用通道Channel的flush或writeAndFlush方法,都會最終觸發(fā)底層通道的網(wǎng)絡寫事件,如果待寫入的對象是FileRegion,則會觸發(fā)零拷貝機制,接下來我們對兩個簡單介紹一下:
2.1 EpollSocketChannel 通道零拷貝
寫入的入口函數(shù)為如下:
核心思想為:如果待寫入的消息是DefaultFileRegion,EpollSocketChannel將直接調用sendfile函數(shù)進行數(shù)據(jù)傳遞;如果是FileRegion類型,則按照約定調用FileRegion的transferTo進行數(shù)據(jù)傳遞,這種方式是否真正進行零拷貝取決于FileRegion的transferTo中是否調用了FileChannel的transferTo方法。
溫馨提示:本文并沒有打算詳細分析Epoll機制以及編程實踐。
2.2 NioSocketChannel 通道零拷貝實現(xiàn)
實現(xiàn)入口為:
從這里可知,NioSocketChannel就是中規(guī)中矩的調用FileRegion的transferTo方法,是否真正實現(xiàn)了零拷貝,取決于底層是否調用了FileChannel的transferTo方法。
2.3 零拷貝實踐總結
從Netty的實現(xiàn)中我們基本可以得出結論:是否是零拷貝,判斷的依據(jù)是是否調用了FileChannel的transferTo方法,更準備的表述是底層是否調用了操作系統(tǒng)的sendfile函數(shù),并且操作系統(tǒng)底層還需要支持gather機制,即linux的內核版本不低于2.4。
— 本文結束 —

關注我,回復 「加群」 加入各種主題討論群。
對「服務端思維」有期待,請在文末點個在看
喜歡這篇文章,歡迎轉發(fā)、分享朋友圈


