在談論Kafka高性能時不得不提到零拷貝和敬。Kafka通過采用零拷貝大大提供了應用性能,減少了內(nèi)核和用戶模式之間的上下文切換次數(shù)戏阅。那么什么是零拷貝昼弟,如何實現(xiàn)零拷貝呢?
什么是零拷貝
WIKI中對其有如下定義:
"Zero-copy" describes computer operations in which the CPU does not perform the task of copying data from one memory area to another.
從WIKI的定義中奕筐,我們看到“零拷貝”是指計算機操作的過程中舱痘,CPU不需要為數(shù)據(jù)在內(nèi)存之間的拷貝消耗資源。而它通常是指計算機在網(wǎng)絡上發(fā)送文件時离赫,不需要將文件內(nèi)容拷貝到用戶空間(User Space)而直接在內(nèi)核空間(Kernel Space)中傳輸?shù)骄W(wǎng)絡的方式芭逝。
零拷貝給我們帶來的好處
- 減少甚至完全避免不必要的CPU拷貝,從而讓CPU解脫出來去執(zhí)行其他的任務
- 減少內(nèi)存帶寬的占用
- 通常零拷貝技術還能夠減少用戶空間和操作系統(tǒng)內(nèi)核空間之間的上下文切換
零拷貝的實現(xiàn)
零拷貝實際的實現(xiàn)并沒有真正的標準渊胸,取決于操作系統(tǒng)如何實現(xiàn)這一點旬盯。零拷貝完全依賴于操作系統(tǒng)。操作系統(tǒng)支持翎猛,就有胖翰;不支持,就沒有办成。不依賴Java本身泡态。
傳統(tǒng)I/O
在Java中,我們可以通過InputStream從源數(shù)據(jù)中讀取數(shù)據(jù)流到一個緩沖區(qū)里迂卢,然后再將它們輸入到OutputStream里某弦。我們知道,這種IO方式傳輸效率是比較低的而克。那么靶壮,當使用上面的代碼時操作系統(tǒng)會發(fā)生什么情況:
這是一個從磁盤文件讀取并且通過socket寫出的過程,對應的系統(tǒng)調(diào)用如下:
read(file,tmp_buf,len)
write(socket,tmp_buf,len)
- 程序使用read()系統(tǒng)調(diào)用员萍。系統(tǒng)由用戶態(tài)轉(zhuǎn)換為內(nèi)核態(tài)(第一次上線文切換)腾降,磁盤中的數(shù)據(jù)有DMA(Direct Memory Access)的方式讀取到內(nèi)核緩沖區(qū)(kernel buffer)。DMA過程中CPU不需要參與數(shù)據(jù)的讀寫碎绎,而是DMA處理器直接將硬盤數(shù)據(jù)通過總線傳輸?shù)絻?nèi)存中螃壤。
- 系統(tǒng)由內(nèi)核態(tài)轉(zhuǎn)換為用戶態(tài)(第二次上下文切換),當程序要讀取的數(shù)據(jù)已經(jīng)完成寫入內(nèi)核緩沖區(qū)以后筋帖,程序會將數(shù)據(jù)由內(nèi)核緩存區(qū)奸晴,寫入用戶緩存區(qū)),這個過程需要CPU參與數(shù)據(jù)的讀寫日麸。
- 程序使用write()系統(tǒng)調(diào)用寄啼。系統(tǒng)由用戶態(tài)切換到內(nèi)核態(tài)(第三次上下文切換),數(shù)據(jù)從用戶態(tài)緩沖區(qū)寫入到網(wǎng)絡緩沖區(qū)(Socket Buffer),這個過程需要CPU參與數(shù)據(jù)的讀寫墩划。
- 系統(tǒng)由內(nèi)核態(tài)切換到用戶態(tài)(第四次上下文切換)涕刚,網(wǎng)絡緩沖區(qū)的數(shù)據(jù)通過DMA的方式傳輸?shù)骄W(wǎng)卡的驅(qū)動(存儲緩沖區(qū))中(protocol engine)
可以看到,傳統(tǒng)的I/O方式會經(jīng)過4次用戶態(tài)和內(nèi)核態(tài)的切換(上下文切換)乙帮,兩次CPU中內(nèi)存中進行數(shù)據(jù)讀寫的過程杜漠。這種拷貝過程相對來說比較消耗資源
內(nèi)存映射方式I/O
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);
這是使用的系統(tǒng)調(diào)用方法,這種方式的I/O原理就是將用戶緩沖區(qū)(user buffer)的內(nèi)存地址和內(nèi)核緩沖區(qū)(kernel buffer)的內(nèi)存地址做一個映射察净,也就是說系統(tǒng)在用戶態(tài)可以直接讀取并操作內(nèi)核空間的數(shù)據(jù)碑幅。
- mmap()系統(tǒng)調(diào)用首先會使用DMA的方式將磁盤數(shù)據(jù)讀取到內(nèi)核緩沖區(qū),然后通過內(nèi)存映射的方式塞绿,使用戶緩沖區(qū)和內(nèi)核讀緩沖區(qū)的內(nèi)存地址為同一內(nèi)存地址沟涨,也就是說不需要CPU再講數(shù)據(jù)從內(nèi)核讀緩沖區(qū)復制到用戶緩沖區(qū)。
- 當使用write()系統(tǒng)調(diào)用的時候异吻,cpu將內(nèi)核緩沖區(qū)(等同于用戶緩沖區(qū))的數(shù)據(jù)直接寫入到網(wǎng)絡發(fā)送緩沖區(qū)(socket buffer)裹赴,然后通過DMA的方式將數(shù)據(jù)傳入到網(wǎng)卡驅(qū)動程序中準備發(fā)送。
可以看到這種內(nèi)存映射的方式減少了CPU的讀寫次數(shù)诀浪,但是用戶態(tài)到內(nèi)核態(tài)的切換(上下文切換)依舊有四次棋返,同時需要注意在進行這種內(nèi)存映射的時候,有可能會出現(xiàn)并發(fā)線程操作同一塊內(nèi)存區(qū)域而導致的嚴重的數(shù)據(jù)不一致問題雷猪,所以需要進行合理的并發(fā)編程來解決這些問題睛竣。
通過sendfile實現(xiàn)的零拷貝I/O
sendfile(socket, file, len);
通過sendfile()系統(tǒng)調(diào)用,可以做到內(nèi)核空間內(nèi)部直接進行I/O傳輸求摇。
- sendfile()系統(tǒng)調(diào)用也會引起用戶態(tài)到內(nèi)核態(tài)的切換射沟,與內(nèi)存映射方式不同的是,用戶空間此時是無法看到或修改數(shù)據(jù)內(nèi)容与境,也就是說這是一次完全意義上的數(shù)據(jù)傳輸過程验夯。
- 從磁盤讀取到內(nèi)存是DMA的方式,從內(nèi)核讀緩沖區(qū)讀取到網(wǎng)絡發(fā)送緩沖區(qū)摔刁,依舊需要CPU參與拷貝挥转,而從網(wǎng)絡發(fā)送緩沖區(qū)到網(wǎng)卡中的緩沖區(qū)依舊是DMA方式。
依舊有一次CPU進行數(shù)據(jù)拷貝共屈,兩次用戶態(tài)和內(nèi)核態(tài)的切換操作绑谣,相比較于內(nèi)存映射的方式有了很大的進步,但問題是程序不能對數(shù)據(jù)進行修改拗引,而只是單純地進行了一次數(shù)據(jù)的傳輸過程借宵。
理想狀態(tài)下的零拷貝I/O
依舊是系統(tǒng)調(diào)用sendfile()
sendfile(socket, file, len);
可以看到,這是真正意義上的零拷貝寺擂,因為其間CPU已經(jīng)不參與數(shù)據(jù)的拷貝過程暇务,也就是說完全通過其他硬件和中斷的方式來實現(xiàn)數(shù)據(jù)的讀寫過程嗎,但是這樣的過程需要硬件的支持才能實現(xiàn)怔软。
借助于硬件上的幫助垦细,我們是可以辦到的。之前我們是把頁緩存的數(shù)據(jù)拷貝到socket緩存中挡逼,實際上括改,我們僅僅需要把緩沖區(qū)描述符傳到socket緩沖區(qū),再把數(shù)據(jù)長度傳過去家坎,這樣DMA控制器直接將頁緩存中的數(shù)據(jù)打包發(fā)送到網(wǎng)絡中就可以了嘱能。
- 系統(tǒng)調(diào)用sendfile()發(fā)起后,磁盤數(shù)據(jù)通過DMA方式讀取到內(nèi)核緩沖區(qū)虱疏,內(nèi)核緩沖區(qū)中的數(shù)據(jù)通過DMA聚合網(wǎng)絡緩沖區(qū)惹骂,然后一齊發(fā)送到網(wǎng)卡中。
可以看到在這種模式下做瞪,是沒有一次CPU進行數(shù)據(jù)拷貝的对粪,所以就做到了真正意義上的零拷貝,雖然和前一種是同一個系統(tǒng)調(diào)用装蓬,但是這種模式實現(xiàn)起來需要硬件的支持著拭,但對于基于操作系統(tǒng)的用戶來講,操作系統(tǒng)已經(jīng)屏蔽了這種差異牍帚,它會根據(jù)不同的硬件平臺來實現(xiàn)這個系統(tǒng)調(diào)用
Java的實現(xiàn)
NIO的零拷貝
File file = new File("test.zip");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel fileChannel = raf.getChannel();
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("", 1234));
// 直接使用了transferTo()進行通道間的數(shù)據(jù)傳輸
fileChannel.transferTo(0, fileChannel.size(), socketChannel);
NIO的零拷貝由transferTo()方法實現(xiàn)儡遮。transferTo()方法將數(shù)據(jù)從FileChannel對象傳送到可寫的字節(jié)通道(如Socket Channel等)。在內(nèi)部實現(xiàn)中暗赶,由native方法transferTo0()來實現(xiàn)鄙币,它依賴底層操作系統(tǒng)的支持。在UNIX和Linux系統(tǒng)中蹂随,調(diào)用這個方法將會引起sendfile()系統(tǒng)調(diào)用爱榔。
使用場景一般是:
- 較大,讀寫較慢糙及,追求速度
- M內(nèi)存不足详幽,不能加載太大數(shù)據(jù)
- 帶寬不夠,即存在其他程序或線程存在大量的IO操作浸锨,導致帶寬本來就小
以上都建立在不需要進行數(shù)據(jù)文件操作的情況下唇聘,如果既需要這樣的速度,也需要進行數(shù)據(jù)操作怎么辦柱搜?
那么使用NIO的直接內(nèi)存迟郎!
NIO的直接內(nèi)存
File file = new File("test.zip");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel fileChannel = raf.getChannel();
MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
首先,它的作用位置處于傳統(tǒng)IO(BIO)與零拷貝之間聪蘸,為何這么說宪肖?
- IO表制,可以把磁盤的文件經(jīng)過內(nèi)核空間,讀到JVM空間控乾,然后進行各種操作么介,最后再寫到磁盤或是發(fā)送到網(wǎng)絡,效率較慢但支持數(shù)據(jù)文件操作蜕衡。
- 零拷貝則是直接在內(nèi)核空間完成文件讀取并轉(zhuǎn)到磁盤(或發(fā)送到網(wǎng)絡)壤短。由于它沒有讀取文件數(shù)據(jù)到JVM這一環(huán),因此程序無法操作該文件數(shù)據(jù)慨仿,盡管效率很高久脯!
而直接內(nèi)存則介于兩者之間,效率一般且可操作文件數(shù)據(jù)镰吆。直接內(nèi)存(mmap技術)將文件直接映射到內(nèi)核空間的內(nèi)存帘撰,返回==一個操作地址(address)==,它解決了文件數(shù)據(jù)需要拷貝到JVM才能進行操作的窘境万皿。而是直接在內(nèi)核空間直接進行操作骡和,省去了內(nèi)核空間拷貝到用戶空間這一步操作。
NIO的直接內(nèi)存是由==MappedByteBuffer==實現(xiàn)的相寇。核心即是map()方法慰于,該方法把文件映射到內(nèi)存中,獲得內(nèi)存地址addr唤衫,然后通過這個addr構造MappedByteBuffer類婆赠,以暴露各種文件操作API。
由于MappedByteBuffer申請的是堆外內(nèi)存佳励,因此不受Minor GC控制休里,只能在發(fā)生Full GC時才能被回收。而==DirectByteBuffer==改善了這一情況赃承,它是MappedByteBuffer類的子類妙黍,同時它實現(xiàn)了DirectBuffer接口,維護一個Cleaner對象來完成內(nèi)存回收瞧剖。因此它既可以通過Full GC來回收內(nèi)存拭嫁,也可以調(diào)用clean()方法來進行回收。
另外抓于,直接內(nèi)存的大小可通過jvm參數(shù)來設置:-XX:MaxDirectMemorySize做粤。
NIO的MappedByteBuffer還有一個兄弟叫做HeapByteBuffer。顧名思義捉撮,它用來在堆中申請內(nèi)存怕品,本質(zhì)是一個數(shù)組。由于它位于堆中巾遭,因此可受GC管控肉康,易于回收闯估。
參考
https://blog.csdn.net/localhost01/article/details/83422888
https://blog.csdn.net/cringkong/article/details/80274148