Kafka是如何利用零拷貝提高性能的

Kafka 在執行消息的寫入和讀取這麼快的原因,其中的一個原因是零拷貝(Zero-copy)技術,下面我們來了解一下這麼高效的原因。

傳統的文件讀寫

傳統的文件讀寫或者網絡傳輸,通常需要將數據從內核態轉換爲用戶態。應用程序讀取用戶態內存數據,寫入文件 / Socket之前,需要從用戶態轉換爲內核態之後纔可以寫入文件或者網卡當中。

數據首先從磁盤讀取到內核緩衝區,這裏面的內核緩衝區就是頁緩存(PageCache)。然後從內核緩衝區中複製到應用程序緩衝區(用戶態),輸出到輸出設備時,又會將用戶態數據轉換爲內核態數據。

DMA

在介紹零拷貝之前,我們先來看一個技術名詞DMA(Direct Memory Access 直接內存訪問)。它是現代電腦的重要特徵之一,允許不同速度的硬件之間直接交互,而不需要佔用CPU的中斷負載。DMA傳輸將一個地址空間複製到另一個地址空間,當CPU 初始化這個傳輸之後,實際的數據傳輸是有DMA設備之間完成,這樣可以大大的減少CPU的消耗。我們常見的硬件設備都支持DMA,如下圖所示:

零拷貝

對於常見的零拷貝,我們下面主要介紹一下mmap sendfile 兩種方式。下面的介紹我們基於磁盤文件拷貝的方式去講解。

mmap

mmap 就是在用戶態直接引用文件句柄,也就是用戶態和內核態共享內核態的數據緩衝區,此時數據不需要複製到用戶態空間。當應用程序往 mmap 輸出數據時,此時就直接輸出到了內核態數據,如果此時輸出設備是磁盤的話,會直接寫盤(flush間隔是30秒)。

上面的圖片我們可以這樣去理解,比如我們需要從 src.data 文件複製數據到 dest.data 文件中。此時我們不需要更改 src.data 裏面的數據,但是對於 dest.data 需要追加一些數據。此時src.data 裏面的數據可以直接通過DMA 設備傳輸,而應用程序還需要對 dest.data 做一些數據追加,此時應用對 dest.data 做 mmap 映射,直接對內核態數據進行修改。

sendfile

對於sendfile 而言,數據不需要在應用程序做業務處理,僅僅是從一個 DMA 設備傳輸到另一個 DMA設備。 此時數據只需要複製到內核態,用戶態不需要複製數據,並且也不需要像 mmap 那樣對內核態的數據的句柄(文件引用)。如下圖所示:

從上圖我們可以發現(輸出設備可以是網卡/磁盤驅動),內核態有 2 份數據緩存 。sendfile 是 Linux 2.1 開始引入的,在 Linux 2.4 又做了一些優化。也就是上圖中磁盤頁緩存中的數據,不需要複製到 Socket 緩衝區,而只是將數據的位置和長度信息存儲到 Socket 緩衝區。實際數據是由DMA 設備直接發送給對應的協議引擎,從而又減少了一次數據複製。

零拷貝的Java實現

JDK 中的 FileChannel 提供了外部 channel 交互的傳輸方法。transferTo 方法會將當前 FileChannel 的字節直接傳輸到 channel 中,transferFrom() 方法可以將可讀 channel 的字節直接傳輸到當前 FileChannel 中。transferTo() 方法底層是基於操作系統的 sendfile 這個系統調用來實現的,map 是對 Channel 做 mmap 映射。

下面我們看一下 Java NIO 中的方法摘要:

// 將當前 FileChannel 的字節傳輸到給定的可寫 channel 中public abstract long transferTo(long position, long count,  WritableByteChannel target) throws IOException;// 將一個可讀 channel 的字節傳輸到當前 FileChannel中public abstract long transferFrom(ReadableByteChannel src, long position, long count) throws IOException;// 對 Channel 做 mmap 映射public abstract MappedByteBuffer map(MapMode mode, long position, long size) throws IOException;

文件拷貝測試對比

下面我們看一下執行下面3段代碼,並且 src.log 文件在不同大小的情況下的測試耗時結果。

1、傳統拷貝

public class OldFileCopy {
    public static final String source = "C:/data/src.log";    public static final String dest = "C:/data/dest.log";
    public static void main(String[] args) {        try {            FileInputStream inputStream = new FileInputStream(source);            FileOutputStream outputStream = new FileOutputStream(dest);            long start = System.currentTimeMillis();            byte[] buff = new byte[4096];            long read = 0, total = 0;            while ((read = inputStream.read(buff)) >= 0) {                total += read;                outputStream.write(buff);            }            outputStream.flush();            System.out.println("耗時:" + (System.currentTimeMillis() - start));        } catch (Exception e) {            e.printStackTrace();        }    }}

2、mmap 拷貝

public class MmapFileCopy {
    public static final String source = "C:/data/src.log";    public static final String dest = "C:/data/dest.log";
    public static void main(String[] args) {        try {            FileChannel sourceChannel = new RandomAccessFile(source, "rw").getChannel();            FileChannel destChannel = new RandomAccessFile(dest, "rw").getChannel();            long start = System.currentTimeMillis();
            MappedByteBuffer map = destChannel.map(FileChannel.MapMode.READ_WRITE, 0, sourceChannel.size());            sourceChannel.write(map);            map.flip();
            System.out.println("耗時:" + (System.currentTimeMillis() - start));        } catch (Exception e) {            e.printStackTrace();        }    }
}

3、sendfile 拷貝

public class SendFileCopy {


    public static final String source = "C:/data/src.log";
    public static final String dest = "C:/data/dest.log";


    public static void main(String[] args) {
        try {
            FileChannel sourceChannel = new RandomAccessFile(source, "rw").getChannel();
            FileChannel destChannel = new RandomAccessFile(dest, "rw").getChannel();
            long start = System.currentTimeMillis();


            sourceChannel.transferTo(0, sourceChannel.size(), destChannel);
            System.out.println("耗時:" + (System.currentTimeMillis() - start));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

通過對不同大小的文件進行對比測試,我們得到了下面的測試結果。

從上面測試結果可以看出,mmap 和 sendfile 的方式要遠遠優於傳統的文件拷貝。對於 mmap 和 sendfile 在文件較小的時候, mmap 耗時更短,當文件較大時 sendfile 的方式最優。

本文來自:

http://moguhu.com/article/detail?articleId=146

往期推薦  點擊標題可跳轉

1、實時數倉 | 你想要的數倉分層設計與技術選型

2、HBase實踐 | HBase內核優化與吞吐能力建設

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章