1. 程式人生 > >四十七、Netty零拷貝

四十七、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高效能之道