在談論Kafka高性能時不得不提到零拷貝。Kafka通過采用零拷貝大大提供了應用性能,減少了內核和用戶模式之間的上下文切換次數。那么什么是零拷貝,如何實現零拷貝呢?
什么是零拷貝
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不需要為數據在內存之間的拷貝消耗資源。而它通常是指計算機在網絡上發送文件時,不需要將文件內容拷貝到用戶空間(User Space)而直接在內核空間(Kernel Space)中傳輸到網絡的方式。
零拷貝給我們帶來的好處
- 減少甚至完全避免不必要的CPU拷貝,從而讓CPU解脫出來去執行其他的任務
- 減少內存帶寬的占用
- 通常零拷貝技術還能夠減少用戶空間和操作系統內核空間之間的上下文切換
零拷貝的實現
零拷貝實際的實現並沒有真正的標准,取決於操作系統如何實現這一點。零拷貝完全依賴於操作系統。操作系統支持,就有;不支持,就沒有。不依賴Java本身。
傳統I/O
在Java中,我們可以通過InputStream從源數據中讀取數據流到一個緩沖區里,然后再將它們輸入到OutputStream里。我們知道,這種IO方式傳輸效率是比較低的。那么,當使用上面的代碼時操作系統會發生什么情況:
這是一個從磁盤文件讀取並且通過socket寫出的過程,對應的系統調用如下:
read(file,tmp_buf,len) write(socket,tmp_buf,len)
- 程序使用read()系統調用。系統由用戶態轉換為內核態(第一次上線文切換),磁盤中的數據有DMA(Direct Memory Access)的方式讀取到內核緩沖區(kernel buffer)。DMA過程中CPU不需要參與數據的讀寫,而是DMA處理器直接將硬盤數據通過總線傳輸到內存中。
- 系統由內核態轉換為用戶態(第二次上下文切換),當程序要讀取的數據已經完成寫入內核緩沖區以后,程序會將數據由內核緩存區,寫入用戶緩存區),這個過程需要CPU參與數據的讀寫。
- 程序使用write()系統調用。系統由用戶態切換到內核態(第三次上下文切換),數據從用戶態緩沖區寫入到網絡緩沖區(Socket Buffer),這個過程需要CPU參與數據的讀寫。
- 系統由內核態切換到用戶態(第四次上下文切換),網絡緩沖區的數據通過DMA的方式傳輸到網卡的驅動(存儲緩沖區)中(protocol engine)
可以看到,傳統的I/O方式會經過4次用戶態和內核態的切換(上下文切換),兩次CPU中內存中進行數據讀寫的過程。這種拷貝過程相對來說比較消耗資源
內存映射方式I/O
tmp_buf = mmap(file, len); write(socket, tmp_buf, len);
這是使用的系統調用方法,這種方式的I/O原理就是將用戶緩沖區(user buffer)的內存地址和內核緩沖區(kernel buffer)的內存地址做一個映射,也就是說系統在用戶態可以直接讀取並操作內核空間的數據。
- mmap()系統調用首先會使用DMA的方式將磁盤數據讀取到內核緩沖區,然后通過內存映射的方式,使用戶緩沖區和內核讀緩沖區的內存地址為同一內存地址,也就是說不需要CPU再講數據從內核讀緩沖區復制到用戶緩沖區。
- 當使用write()系統調用的時候,cpu將內核緩沖區(等同於用戶緩沖區)的數據直接寫入到網絡發送緩沖區(socket buffer),然后通過DMA的方式將數據傳入到網卡驅動程序中准備發送。
可以看到這種內存映射的方式減少了CPU的讀寫次數,但是用戶態到內核態的切換(上下文切換)依舊有四次,同時需要注意在進行這種內存映射的時候,有可能會出現並發線程操作同一塊內存區域而導致的嚴重的數據不一致問題,所以需要進行合理的並發編程來解決這些問題。
通過sendfile實現的零拷貝I/O
sendfile(socket, file, len);
通過sendfile()系統調用,可以做到內核空間內部直接進行I/O傳輸。
- sendfile()系統調用也會引起用戶態到內核態的切換,與內存映射方式不同的是,用戶空間此時是無法看到或修改數據內容,也就是說這是一次完全意義上的數據傳輸過程。
- 從磁盤讀取到內存是DMA的方式,從內核讀緩沖區讀取到網絡發送緩沖區,依舊需要CPU參與拷貝,而從網絡發送緩沖區到網卡中的緩沖區依舊是DMA方式。
依舊有一次CPU進行數據拷貝,兩次用戶態和內核態的切換操作,相比較於內存映射的方式有了很大的進步,但問題是程序不能對數據進行修改,而只是單純地進行了一次數據的傳輸過程。
理想狀態下的零拷貝I/O
依舊是系統調用sendfile()
sendfile(socket, file, len);
可以看到,這是真正意義上的零拷貝,因為其間CPU已經不參與數據的拷貝過程,也就是說完全通過其他硬件和中斷的方式來實現數據的讀寫過程嗎,但是這樣的過程需要硬件的支持才能實現。
借助於硬件上的幫助,我們是可以辦到的。之前我們是把頁緩存的數據拷貝到socket緩存中,實際上,我們僅僅需要把緩沖區描述符傳到socket緩沖區,再把數據長度傳過去,這樣DMA控制器直接將頁緩存中的數據打包發送到網絡中就可以了。
- 系統調用sendfile()發起后,磁盤數據通過DMA方式讀取到內核緩沖區,內核緩沖區中的數據通過DMA聚合網絡緩沖區,然后一齊發送到網卡中。
可以看到在這種模式下,是沒有一次CPU進行數據拷貝的,所以就做到了真正意義上的零拷貝,雖然和前一種是同一個系統調用,但是這種模式實現起來需要硬件的支持,但對於基於操作系統的用戶來講,操作系統已經屏蔽了這種差異,它會根據不同的硬件平台來實現這個系統調用
Java的實現
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()進行通道間的數據傳輸 fileChannel.transferTo(0, fileChannel.size(), socketChannel);
NIO的零拷貝由transferTo()方法實現。transferTo()方法將數據從FileChannel對象傳送到可寫的字節通道(如Socket Channel等)。在內部實現中,由native方法transferTo0()來實現,它依賴底層操作系統的支持。在UNIX和Linux系統中,調用這個方法將會引起sendfile()系統調用。
使用場景一般是:
- 較大,讀寫較慢,追求速度
- M內存不足,不能加載太大數據
- 帶寬不夠,即存在其他程序或線程存在大量的IO操作,導致帶寬本來就小
以上都建立在不需要進行數據文件操作的情況下,如果既需要這樣的速度,也需要進行數據操作怎么辦?
那么使用NIO的直接內存!
NIO的直接內存
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());
首先,它的作用位置處於傳統IO(BIO)與零拷貝之間,為何這么說?
- IO,可以把磁盤的文件經過內核空間,讀到JVM空間,然后進行各種操作,最后再寫到磁盤或是發送到網絡,效率較慢但支持數據文件操作。
- 零拷貝則是直接在內核空間完成文件讀取並轉到磁盤(或發送到網絡)。由於它沒有讀取文件數據到JVM這一環,因此程序無法操作該文件數據,盡管效率很高!
而直接內存則介於兩者之間,效率一般且可操作文件數據。直接內存(mmap技術)將文件直接映射到內核空間的內存,返回==一個操作地址(address)==,它解決了文件數據需要拷貝到JVM才能進行操作的窘境。而是直接在內核空間直接進行操作,省去了內核空間拷貝到用戶空間這一步操作。
NIO的直接內存是由==MappedByteBuffer==實現的。核心即是map()方法,該方法把文件映射到內存中,獲得內存地址addr,然后通過這個addr構造MappedByteBuffer類,以暴露各種文件操作API。
由於MappedByteBuffer申請的是堆外內存,因此不受Minor GC控制,只能在發生Full GC時才能被回收。而==DirectByteBuffer==改善了這一情況,它是MappedByteBuffer類的子類,同時它實現了DirectBuffer接口,維護一個Cleaner對象來完成內存回收。因此它既可以通過Full GC來回收內存,也可以調用clean()方法來進行回收。
另外,直接內存的大小可通過jvm參數來設置:-XX:MaxDirectMemorySize。
NIO的MappedByteBuffer還有一個兄弟叫做HeapByteBuffer。顧名思義,它用來在堆中申請內存,本質是一個數組。由於它位於堆中,因此可受GC管控,易於回收。
參考
https://blog.csdn.net/localhost01/article/details/83422888
https://blog.csdn.net/cringkong/article/details/80274148
作者:攀山客
鏈接:https://www.jianshu.com/p/497e7640b57c
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
