四十七、Netty零拷貝
零拷貝的定義
Zero-copy, 就是在操作資料時, 不需要將資料 buffer 從一個記憶體區域拷貝到另一個記憶體區域. 因為少了一次記憶體的拷貝, 因此 CPU 的效率就得到的提升.
在 OS 層面上的 Zero-copy 通常指避免在 使用者態(User-space) 與 核心態(Kernel-space) 之間來回拷貝資料。
但Netty 中的 Zero-copy 與 OS 的 Zero-copy 不太一樣, Netty的 Zero-coyp 完全是在使用者態(Java 層面)的, 它的 Zero-copy 的更多的是偏向於 優化資料操作 。
Netty的“零拷貝”主要體現以下幾個方面:
1.Netty的接收和傳送ByteBuffer採用DIRECT BUFFERS,使用堆外直接記憶體進行Socket讀寫,不需要進行位元組緩衝區的二次拷貝。如果使用傳統的堆記憶體(HEAP BUFFERS)進行Socket讀寫,JVM會將堆記憶體Buffer拷貝一份到直接記憶體中,然後才寫入Socket中。相比於堆外直接記憶體,訊息在傳送過程中多了一次緩衝區的記憶體拷貝。
2.Netty 提供了 CompositeByteBuf 類, 它可以將多個 ByteBuf 合併為一個邏輯上的 ByteBuf, 避免了傳統通過記憶體拷貝的方式將幾個小Buffer合併成一個大的Buffer。
3.通過 FileRegion 包裝的FileChannel.tranferTo方法 實現檔案傳輸, 可以直接將檔案緩衝區的資料傳送到目標 Channel,避免了傳統通過迴圈write方式導致的記憶體拷貝問題。
4.通過 wrap 操作, 我們可以將 byte[] 陣列、ByteBuf、ByteBuffer等包裝成一個 Netty ByteBuf 物件, 進而避免了拷貝操作。
零拷貝的具體分析
1.ByteBuffer分配Direct Buffers
原始碼如下:
分析:從原始碼知,ByteBuffer由ChannelConfig分配,而ChannelConfig建立ByteBufAllocator預設使用Direct Buffer,這就避免了讀寫資料的二次記憶體拷貝問題,從而實現了讀寫Socket的零拷貝功能,
2.用CompositeByteBuf 類實現了將多個 ByteBuf 合併為一個邏輯上的 ByteBuf
例:
//定義兩個ByteBuf型別的 body 和 header
CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();
compositeByteBuf.addComponents(true, header, body);
分析:addComponents方法將 header 與 body 合併為一個邏輯上的 ByteBuf, 這兩個 ByteBuf 在CompositeByteBuf 內部都是單獨存在的, CompositeByteBuf 只是邏輯上是一個整體
圖解:
注:
addComponents方法的引數是 true, 它表示當新增新的 ByteBuf 時, 自動遞增 CompositeByteBuf 的 writeIndex,若沒有這個引數,那麼 compositeByteBuf 的 writeIndex 仍然是0, 就不可能從 compositeByteBuf 中讀取到資料,
除了直接使用 CompositeByteBuf 類外, 還可以使用 Unpooled.wrappedBuffer 方法, 它底層封裝了 CompositeByteBuf 操作,
例:ByteBuf allByteBuf = Unpooled.wrappedBuffer(header, body);
3.通過 FileRegion 實現零拷貝
例3.1:使用NIO實現零拷貝
public static void copyFileWithFileChannel(String srcFileName, String destFileName) throws Exception {
RandomAccessFile srcFile = new RandomAccessFile(srcFileName, "r");
FileChannel srcFileChannel = srcFile.getChannel();
RandomAccessFile destFile = new RandomAccessFile(destFileName, "rw");
FileChannel destFileChannel = destFile.getChannel();
long position = 0;
long count = srcFileChannel.size();
srcFileChannel.transferTo(position, count, destFileChannel);
}
分析:有了 FileChannel 後, 就可以直接將原始檔的內容通過transferTo)方法直接拷貝到目的檔案中, 而不需要額外借助一個臨時 buffer, 避免了不必要的記憶體操作.
例3.2 :Netty官網的例子:
public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
RandomAccessFile raf = null;
long length = -1;
try {
// 1. 通過 RandomAccessFile 開啟一個檔案.
raf = new RandomAccessFile(msg, "r");
length = raf.length();
} catch (Exception e) {
ctx.writeAndFlush("ERR: " + e.getClass().getSimpleName() + ": " + e.getMessage() + '\n');
return;
} finally {
if (length < 0 && raf != null) {
raf.close();
}
}
ctx.write("OK: " + raf.length() + '\n');
if (ctx.pipeline().get(SslHandler.class) == null) {
// SSL not enabled - can use zero-copy file transfer.
// 2. 呼叫 raf.getChannel() 獲取一個 FileChannel.
// 3. 將 FileChannel 封裝成一個 DefaultFileRegion
ctx.write(new DefaultFileRegion(raf.getChannel(), 0, length));
} else {
// SSL enabled - cannot use zero-copy file transfer.
ctx.write(new ChunkedFile(raf));
}
ctx.writeAndFlush("\n");
}
分析:通過 RandomAccessFile 開啟一個檔案, 然後 Netty 使用了 DefaultFileRegion 來封裝一個 FileChannel,然後就可以直接通過它將檔案的內容直接寫入 Channel 中, 而不需要傳統方式:拷貝檔案內容到臨時 buffer, 然後再將 buffer 寫入 Channel.
4.通過 wrap / slice 實現零拷貝
例4.1: wrap方法
ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);
分析:通過wrappedBuffer 方法來將 bytes 包裝成為一個 UnpooledHeapByteBuf 物件, 而在包裝的過程中, 是不會有拷貝操作的.
例4.2:slice 方法
ByteBuf header = byteBuf.slice(0, 5);
ByteBuf body = byteBuf.slice(5, 10);
分析: slice 操作可以將一個 ByteBuf 切片 為多個共享一個儲存區域的 ByteBuf 物件.它產生 header 和 body 的過程是沒有拷貝操作的, header 和 body 物件在內部其實是共享了 byteBuf 儲存空間的不同部分而已.
注:也可以設定Netty的接收Buffer為堆記憶體模式,有兩種方法
boot.option(ChannelOption.ALLOCATOR,PooledByteBufAllocator.DEFAULT)
socketchannel.config.setAllocator(UnpooledByteBufAllocator.DEFAULT)
本人才疏學淺,若有錯,請指出,謝謝!
如果你有更好的建議,可以留言我們一起討論,共同進步!
衷心的感謝您能耐心的讀完本篇博文。
參考資料:
對 Netty 的Zero Copy理解
Netty高效能之道