1. 程式人生 > >理解Netty中的零拷貝(Zero-Copy)機制【轉】

理解Netty中的零拷貝(Zero-Copy)機制【轉】

理解零拷貝

零拷貝是Netty的重要特性之一,而究竟什麼是零拷貝呢? 
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)中傳輸到網路的方式。

Non-Zero Copy方式: 
Non-Zero Copy

Zero Copy方式: 
在此輸入圖片描述

從上圖中可以清楚的看到,Zero Copy的模式中,避免了資料在使用者空間和記憶體空間之間的拷貝,從而提高了系統的整體效能。Linux中的sendfile()以及Java NIO中的FileChannel.transferTo()方法都實現了零拷貝的功能,而在Netty中也通過在FileRegion中包裝了NIO的FileChannel.transferTo()方法實現了零拷貝。

而在Netty中還有另一種形式的零拷貝,即Netty允許我們將多段資料合併為一整段虛擬資料供使用者使用,而過程中不需要對資料進行拷貝操作,這也是我們今天要講的重點。我們都知道在stream-based transport(如TCP/IP)的傳輸過程中,資料包有可能會被重新封裝在不同的資料包中,例如當你傳送如下資料時:

Data Stream Sent

有可能實際收到的資料如下:

Data Stream Received

因此在實際應用中,很有可能一條完整的訊息被分割為多個數據包進行網路傳輸,而單個的資料包對你而言是沒有意義的,只有當這些資料包組成一條完整的訊息時你才能做出正確的處理,而Netty可以通過零拷貝的方式將這些資料包組合成一條完整的訊息供你來使用。而此時,零拷貝的作用範圍僅在使用者空間中。

Virtual Buffer

Netty3中零拷貝的實現機制

以下以Netty 3.8.0.Final的原始碼來進行說明

ChannelBuffer介面

Netty為需要傳輸的資料制定了統一的ChannelBuffer介面。該介面的主要設計思路如下:

  • 使用getByte(int index)
    方法來實現隨機訪問
  • 使用雙指標的方式實現順序訪問
    • 每個Buffer都有一個讀指標(readIndex)和寫指標(writeIndex)
    • 在讀取資料時讀指標後移,在寫入資料時寫指標後移 
      在此輸入圖片描述

定義了統一的介面之後,就是來做各種實現了。Netty主要實現了HeapChannelBuffer,ByteBufferBackedChannelBuffer等等,下面我們就來講講與Zero Copy直接相關的CompositeChannelBuffer類。

CompositeChannelBuffer類

CompositeChannelBuffer類的作用是將多個ChannelBuffer組成一個虛擬的ChannelBuffer來進行操作。為什麼說是虛擬的呢,因為CompositeChannelBuffer並沒有將多個ChannelBuffer真正的組合起來,而只是儲存了他們的引用,這樣就避免了資料的拷貝,實現了Zero Copy。 
下面我們來看看具體的程式碼實現,首先是成員變數

private int readerIndex;
private int writerIndex;
private ChannelBuffer[] components;
private int[] indices;
private int lastAccessedComponentId;

以上這裡列出了幾個比較重要的成員變數。其中readerIndex既讀指標和writerIndex既寫指標是從AbstractChannelBuffer繼承而來的;然後components是一個ChannelBuffer的陣列,他儲存了組成這個虛擬Buffer的所有子Buffer,indices是一個int型別的陣列,它儲存的是各個Buffer的索引值;最後的lastAccessedComponentId是一個int值,它記錄了最後一次訪問時的子Buffer ID。從這個資料結構,我們不難發現所謂的CompositeChannelBuffer實際上就是將一系列的Buffer通過陣列儲存起來,然後實現了ChannelBuffer 的介面,使得在上層看來,操作這些Buffer就像是操作一個單獨的Buffer一樣。

建立

接下來,我們再看一下CompositeChannelBuffer.setComponents方法,它會在初始化CompositeChannelBuffer時被呼叫。

/**
 * Setup this ChannelBuffer from the list
 */
private void setComponents(List<ChannelBuffer> newComponents) {
    assert !newComponents.isEmpty();

    // Clear the cache.
    lastAccessedComponentId = 0;

    // Build the component array.
    components = new ChannelBuffer[newComponents.size()];
    for (int i = 0; i < components.length; i ++) {
        ChannelBuffer c = newComponents.get(i);
        if (c.order() != order()) {
            throw new IllegalArgumentException(
                    "All buffers must have the same endianness.");
        }

        assert c.readerIndex() == 0;
        assert c.writerIndex() == c.capacity();

        components[i] = c;
    }

    // Build the component lookup table.
    indices = new int[components.length + 1];
    indices[0] = 0;
    for (int i = 1; i <= components.length; i ++) {
        indices[i] = indices[i - 1] + components[i - 1].capacity();
    }

    // Reset the indexes.
    setIndex(0, capacity());
}

通過程式碼可以看到該方法的功能就是將一個ChannelBuffer的List給組合起來。它首先將List中得元素放入到components陣列中,然後建立indices用於資料的查詢,最後使用setIndex來重置指標。這裡需要注意的是setIndex(0, capacity())會將讀指標設定為0,寫指標設定為當前Buffer的長度,這也就是前面需要做assert c.readerIndex() == 0assert c.writerIndex() == c.capacity()這兩個判斷的原因,否則很容易會造成資料重複讀寫的問題,所以Netty推薦我們使用ChannelBuffers.wrappedBuffer方法來進行Buffer的合併,因為在該方法中Netty會通過slice()方法來確保構建CompositeChannelBuffer是傳入的所有子Buffer都是符合要求的。

資料訪問

CompositeChannelBuffer.getByte(int index)的實現如下:

public byte getByte(int index) {
    int componentId = componentId(index);
    return components[componentId].getByte(index - indices[componentId]);
}

從程式碼我們可以看到,在隨機查詢時會首先通過index獲取這個位元組所在的componentId既位元組所在的子Buffer序列,然後通過index - indices[componentId]計算出它在這個子Buffer中的第幾個位元組,然後返回結果。

下面再來看一下componentId(int index)的實現:

private int componentId(int index) {
    int lastComponentId = lastAccessedComponentId;
    if (index >= indices[lastComponentId]) {
        if (index < indices[lastComponentId + 1]) {
            return lastComponentId;
        }

        // Search right
        for (int i = lastComponentId + 1; i < components.length; i ++) {
            if (index < indices[i + 1]) {
                lastAccessedComponentId = i;
                return i;
            }
        }
    } else {
        // Search left
        for (int i = lastComponentId - 1; i >= 0; i --) {
            if (index >= indices[i]) {
                lastAccessedComponentId = i;
                return i;
            }
        }
    }

    throw new IndexOutOfBoundsException("Invalid index: " + index + ", maximum: " + indices.length);
}

從程式碼中我們發現,Netty以lastComponentId既上次訪問的子Buffer序號為中心,向左右兩邊進行搜尋,這樣做的目的是,當我們兩次隨機查詢的字元序列相近時(大部分情況下都是這樣),可以最快的搜尋到目標索引的componentId

參考資料

  1. http://my.oschina.net/flashsword/blog/164237
  2. http://en.wikipedia.org/wiki/Zero-copy
  3. http://stackoverflow.com/questions/20727615/is-nettys-zero-copy-different-from-os-level-zero-copy
  4. http://www-old.itm.uni-luebeck.de/teaching/ws1112/vs/Uebung/GrossUebungNetty/VS-WS1112-xx-Zero-Copy_Event-Driven_Servers_with_Netty.pdf?lang=de