Java 開發者必知的零拷貝技術:RocketMQ/Kafka 性能優化的核心原理
在現代計算機系統中,高效的IO(輸入/輸出)操作對于系統整體性能至關重要。隨著應用程序對數據處理需求的不斷增長,如何減少CPU在IO操作中的開銷,提高數據傳輸效率,成為系統設計中的重要課題。零拷貝(Zero-Copy)技術應運而生,成為解決這一問題的關鍵技術之一。
零拷貝技術通過減少或消除數據在內存中的拷貝次數,以及減少用戶態與內核態之間的上下文切換,顯著提升了IO操作的性能。本文將詳細介紹零拷貝技術的工作原理及其在實際項目中的應用。

一、詳解零拷貝工作原理
1. 傳統的IO流程是什么樣的
我們先簡單了解一下文件讀取的粗略流程,應用程序需要讀取文件時,對應的流程為:
- 應用程序發起read讀取請求。
- 系統內核將數據從硬盤加載到內核緩沖區。
- 內核緩沖區拷貝用戶空間緩沖區。
- 應用程序基于用戶緩沖區讀取數據進行業務流程處理。

基于上述基礎,我們在進行深入了解如下幾個概念,這對我們了解傳統IO流程的理解有著重要的作用:
- 內核態:內核態是操作系統內核運行的模式,當我們需要操作系統內核級別的特權指令(例如上文的read操作),就需要切換為內核態。內核態具備操作系統的最高權限,可以訪問計算機所有的硬件資源和數據。
- 用戶態:和內核態相反,應用程序所處的模式也就是用戶態,是應用程序運行的模式,在該模式下僅僅能執行普通指令,無法直接訪問操作操作系統敏感數據和計算機硬件資源。
- 內核緩沖區:內核緩沖區可以理解為應用程序和外部存儲介質數據的中介,即應用程序和外部存儲介質或者網絡socket交互的數據都會經由內核緩沖區進行中轉。
- 用戶緩沖區:提供于應用程序直接讀寫操作的內存空間,這也就意味著我們需要處理任何外部存儲介質或者網絡socket數據都必須加載到內核緩沖區應用程序才能進行進一步的操作。
- 磁盤空間緩沖區:磁盤緩沖區用于處理那些從磁盤中讀取或者準備寫入磁盤的數據的臨時內存存儲空間,它是一種對于磁盤I/O的優化策略,本質上就是通過內存高速的訪問速度,減少讀取磁盤數據的耗時,從而提高數據讀寫的執行性能。
- PageCache:PageCache也就是我們所說的磁盤高速緩存,操作系統為了保證讀寫性能,用到了局部性原理,通俗來說也就是操作系統認為近期被讀取的數據以及相鄰的數據再次被訪問的概率很高,于是這些讀取過的數據以及相鄰的數據都會緩存在PageCache中,當我們再次進行相同數據讀取時,如果PageCache存在該數據則會直接返回,反之則會到外部存儲介質讀取。注意PageCache數據并非一直活躍于內存中,一旦內存空間被占滿,由于緩存置換算法,某些長時間未被訪問的PageCache就會被淘汰。
有了初步的認識我們就可以更加深入的去分析傳統IO流程了,先來說說讀的詳細流程,對應的時序圖如下所示,可以看到完整IO讀流程為:
- 應用程序發起read調用,因為涉及系統內核的操作,所以需要進行一次模式切換,從用戶態轉為內核態。
- 內核通過外部存儲介質或者網絡socket發起讀操作。
- 磁盤或者網絡數據寫入磁盤緩沖區。
- 內核將數據從磁盤緩沖區加載到內核緩沖區。
- 內核緩沖區將數據拷貝到用戶緩沖區,提供應用程序處理。
- 完成上述操作后,再次進行模式切換,從內核態轉為用戶態。

同理我們再次給出傳統IO的寫入操作:
- 應用程序發起write調用,進行一次模式切換,從用戶態轉為內核態。
- 將數據從用戶緩沖區寫入內核緩沖區。
- 內核緩沖區將數據寫入到磁盤緩沖區。
- 最終磁盤緩沖區數據被寫入到磁盤或者網絡套接字中。

2. 解決傳統IO性能瓶頸的思路有哪些
傳統IO模式性能開銷存在于以下三點:
- 整個數據的傳輸過程都需要CPU參與,在此過程期間CPU不能做其他事情。
- 因為數據需要經過內核緩沖區的緣故,導致發起IO調用時存在用戶態到內核態模式上下文切換的開銷。
- 數據傳輸時需要在用戶緩沖區、內核緩沖區來回拷貝的開銷,消耗了大量CPU時間片和內存帶寬。
3. mmap+write零拷貝
第一點本質上可以通過內存映射文件技術(Memory-mapped Files)解決。該技術通過將文件直接映射到用戶空間的內存區域,使得應用程序可以直接訪問文件數據,避免了數據在用戶空間和內核空間之間的拷貝操作:

通過DMA進行數據寫入時,也是一個道理,通過DMA將內核緩沖區數據寫入至外部存儲/socket:

再來聊聊第二點,針對用戶態、內核態上下文切換的開銷,我們可以通過內存映射文件技術(Memory-mapped Files)解決。該技術將文件直接映射到用戶空間的內存區域,使得應用程序可以直接訪問文件數據,避免了數據在用戶空間和內核空間之間的拷貝操作,從而減少了上下文切換的開銷:

4. sendfile實現零拷貝
接下來就是第三點,針對直接文件傳輸,實際上Linux內核2.1及其以上版本提供sendfile內核函數,該函數可直接將文件數據從一個文件描述符傳輸到另一個文件描述符(如從文件到socket),減少了數據在內核緩沖區和用戶緩沖區之間的拷貝操作,節省了一大部分拷貝的開銷:

5. sendfile更進一步的優化
實際上sendfile內核函數在Linux的2.4版本做了更進一步的優化,若網卡支持SG-DMA(Scatter-Gather DMA)技術的情況下,上一步將磁盤數據寫入到內核緩沖區再通過CPU將磁盤數據拷貝到socket緩沖區的步驟可以省去,通過DMA控制器將數據直接寫入到網卡,將寫入的文件描述符和數據長度告知socket緩沖區,由此通過避免CPU參與,完成大文件的高效傳輸:

6. splice實現零拷貝
除了sendfile,Linux還提供了splice系統調用,它可以在兩個文件描述符之間移動數據,其中一個必須是管道描述符。splice通過在內核空間中直接移動數據,避免了用戶空間和內核空間之間的數據拷貝,進一步提升了IO性能:
應用程序 → splice() → 管道緩沖區 → splice() → 目標文件描述符splice特別適用于需要在文件和管道之間傳輸數據的場景,例如在網絡服務器中將文件數據傳輸到網絡套接字。
二、聊聊零拷貝技術在大型開源項目中的運用
1. mmap+write技術的運用
對于mmap+write技術的運用,最典型的就是RocketMQ中MappedFile的init方法,可以看到它的mappedByteBuffer 就是通過map方法與內核緩沖區構成映射,實現盡可能少的數據拷貝提升數據讀寫性能:
private void init(final String fileName, final int fileSize) throws IOException {
//封裝文件信息
this.fileName = fileName;
this.fileSize = fileSize;
this.file = new File(fileName);
this.fileFromOffset = Long.parseLong(this.file.getName());
boolean ok = false;
ensureDirOK(this.file.getParent());
try {
//與文件file的內核緩沖區數據構成映射,并將內核緩沖區數據地址信息封裝到mappedByteBuffer
this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);
//......
} catch (FileNotFoundException e) {
//......
} catch (IOException e) {
//......
} finally {
if (!ok && this.fileChannel != null) {
this.fileChannel.close();
}
}
}從java開發者的角度來說,mmap+write技術在java中的實現有如下優缺點,先來說說優點:
- 通過內存映射減少了內核緩沖區和用戶緩沖區來回拷貝的開銷,提升程序讀寫效率。
- 對于小文件,這種方式即使頻繁調用,效果也會比sendfile更好。
說完了優點,我們再來說說缺點:
- MappedByteBuffer 一次只能映射2G的文件,超出則會拋出異常,這也是為什么RocketMQ的CommitLog日志文件大小為1G。
- 在網絡傳輸過程中,內核緩沖區的數據仍然需要CPU進行拷貝,在某些場景下相較于sendfile會多消耗CPU資源。
- mmap技術內存分配存在復雜的安全性控制,對于內存進行嚴格管控處理,避免JVM Crash問題。
2. Kafka對于sendfile技術的運用
查看Kafka中FileRecords的writeTo方法可知,Kafka中partition leader到follower的消息同步和consumer拉取partition中的消息,都是直接通過transferFrom(底層就是通過sendfile實現)實現的:
// org.apache.kafka.common.record.FileRecords
@Override
public long writeTo(GatheringByteChannel destChannel, long offset, int length) throws IOException {
//......
if (destChannel instanceof TransportLayer) {
TransportLayer tl = (TransportLayer) destChannel;
//調用transferFrom從channel中拉取數據到destChannel中
bytesTransferred = tl.transferFrom(channel, position, count);
} else {
//將channel數據寫到destChannel中
bytesTransferred = channel.transferTo(position, count, destChannel);
}
return bytesTransferred;
}這種方式實現的零拷貝可以很好的利用DMA方式,盡可能減少CPU的消耗,對于大塊的文件傳輸,效率會高一些,但它也有著如下幾個缺點:
- 就當前java的實現而言,它僅僅支持源為FileChannel傳輸到socketChannel,不支持源為socketChannel。
- 對于小文件傳輸,處理效率不如mmap方式,只能是BIO方式傳輸,不能使用NIO。
三、小結
零拷貝技術通過減少或消除數據在內存中的拷貝次數,以及減少用戶態與內核態之間的上下文切換,顯著提升了IO操作的性能。本文詳細介紹了以下幾種零拷貝技術:
- mmap+write:通過內存映射文件技術,將文件直接映射到用戶空間,避免了數據在用戶空間和內核空間之間的拷貝。
- sendfile:通過系統調用直接在內核空間中傳輸數據,避免了用戶空間和內核空間之間的數據拷貝。
- splice:通過管道在內核空間中傳輸數據,進一步減少了數據拷貝。
在實際應用中,不同的零拷貝技術適用于不同的場景:
- 對于小文件傳輸,mmap方式通常表現更好
- 對于大文件傳輸,sendfile方式通常表現更好
- 在網絡傳輸中,sendfile可以更好地利用DMA技術,減少CPU消耗
通過合理選擇和應用零拷貝技術,可以顯著提升系統的IO性能,特別是在處理大量數據傳輸的場景中。

























