Kafka 在執(zhí)行消息的寫(xiě)入和讀取這么快的原因,其中的一個(gè)原因是零拷貝(Zero-copy)技術(shù),下面我們來(lái)了解一下這么高效的原因。
傳統(tǒng)的文件讀寫(xiě)
傳統(tǒng)的文件讀寫(xiě)或者網(wǎng)絡(luò)傳輸,通常需要將數(shù)據(jù)從 內(nèi)核態(tài) 轉(zhuǎn)換為 用戶(hù)態(tài) 。應(yīng)用程序讀取用戶(hù)態(tài)內(nèi)存數(shù)據(jù),寫(xiě)入文件 / Socket之前,需要從用戶(hù)態(tài)轉(zhuǎn)換為內(nèi)核態(tài)之后才可以寫(xiě)入文件或者網(wǎng)卡當(dāng)中。
數(shù)據(jù)首先從磁盤(pán)讀取到內(nèi)核緩沖區(qū),這里面的內(nèi)核緩沖區(qū)就是頁(yè)緩存(PageCache)。然后從內(nèi)核緩沖區(qū)中復(fù)制到應(yīng)用程序緩沖區(qū)(用戶(hù)態(tài)),輸出到輸出設(shè)備時(shí),又會(huì)將用戶(hù)態(tài)數(shù)據(jù)轉(zhuǎn)換為內(nèi)核態(tài)數(shù)據(jù)。
DMA
在介紹零拷貝之前,我們先來(lái)看一個(gè)技術(shù)名詞DMA(Direct Memory Access 直接內(nèi)存訪問(wèn))。它是現(xiàn)代電腦的重要特征之一,允許不同速度的硬件之間直接交互,而不需要占用CPU的中斷負(fù)載。DMA傳輸將一個(gè)地址空間復(fù)制到另一個(gè)地址空間,當(dāng)CPU 初始化這個(gè)傳輸之后,實(shí)際的數(shù)據(jù)傳輸是有DMA設(shè)備之間完成,這樣可以大大的減少CPU的消耗。我們常見(jiàn)的硬件設(shè)備都支持DMA,如下圖所示:
零拷貝
對(duì)于常見(jiàn)的零拷貝,我們下面主要介紹一下 mmap 和 sendfile 兩種方式。下面的介紹我們基于磁盤(pán)文件拷貝的方式去講解。
mmap
mmap 就是在用戶(hù)態(tài)直接引用文件句柄,也就是用戶(hù)態(tài)和內(nèi)核態(tài)共享內(nèi)核態(tài)的數(shù)據(jù)緩沖區(qū),此時(shí)數(shù)據(jù)不需要復(fù)制到用戶(hù)態(tài)空間。當(dāng)應(yīng)用程序往 mmap 輸出數(shù)據(jù)時(shí),此時(shí)就直接輸出到了內(nèi)核態(tài)數(shù)據(jù),如果此時(shí)輸出設(shè)備是磁盤(pán)的話,會(huì)直接寫(xiě)盤(pán)(flush間隔是30秒)。
上面的圖片我們可以這樣去理解,比如我們需要從 src.data 文件復(fù)制數(shù)據(jù)到 dest.data 文件中。此時(shí)我們不需要更改 src.data 里面的數(shù)據(jù),但是對(duì)于 dest.data 需要追加一些數(shù)據(jù)。此時(shí)src.data 里面的數(shù)據(jù)可以直接通過(guò)DMA 設(shè)備傳輸,而應(yīng)用程序還需要對(duì) dest.data 做一些數(shù)據(jù)追加,此時(shí)應(yīng)用對(duì) dest.data 做 mmap 映射,直接對(duì)內(nèi)核態(tài)數(shù)據(jù)進(jìn)行修改。
sendfile
對(duì)于sendfile 而言,數(shù)據(jù)不需要在應(yīng)用程序做業(yè)務(wù)處理,僅僅是從一個(gè) DMA 設(shè)備傳輸?shù)搅硪粋€(gè) DMA設(shè)備。 此時(shí)數(shù)據(jù)只需要復(fù)制到內(nèi)核態(tài),用戶(hù)態(tài)不需要復(fù)制數(shù)據(jù),并且也不需要像 mmap 那樣對(duì)內(nèi)核態(tài)的數(shù)據(jù)的句柄(文件引用)。如下圖所示:
從上圖我們可以發(fā)現(xiàn)(輸出設(shè)備可以是網(wǎng)卡/磁盤(pán)驅(qū)動(dòng)),內(nèi)核態(tài)有 2 份數(shù)據(jù)緩存 。sendfile 是 Linux 2.1 開(kāi)始引入的,在 Linux 2.4 又做了一些優(yōu)化。也就是上圖中磁盤(pán)頁(yè)緩存中的數(shù)據(jù),不需要復(fù)制到 Socket 緩沖區(qū),而只是將數(shù)據(jù)的位置和長(zhǎng)度信息存儲(chǔ)到 Socket 緩沖區(qū)。實(shí)際數(shù)據(jù)是由DMA 設(shè)備直接發(fā)送給對(duì)應(yīng)的協(xié)議引擎,從而又減少了一次數(shù)據(jù)復(fù)制。
零拷貝的Java實(shí)現(xiàn)
JDK 中的 FileChannel 提供了外部 channel 交互的傳輸方法。transferTo 方法會(huì)將當(dāng)前 FileChannel 的字節(jié)直接傳輸?shù)?channel 中,transferFrom() 方法可以將可讀 channel 的字節(jié)直接傳輸?shù)疆?dāng)前 FileChannel 中。transferTo() 方法底層是基于操作系統(tǒng)的 sendfile 這個(gè)系統(tǒng)調(diào)用來(lái)實(shí)現(xiàn)的,map 是對(duì) Channel 做 mmap 映射。
下面我們看一下 Java NIO 中的方法摘要:
// 將當(dāng)前 FileChannel 的字節(jié)傳輸?shù)浇o定的可寫(xiě) channel 中
public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;
// 將一個(gè)可讀 channel 的字節(jié)傳輸?shù)疆?dāng)前 FileChannel中
public abstract long transferFrom(ReadableByteChannel src, long position, long count) throws IOException;
// 對(duì) Channel 做 mmap 映射
public abstract MappedByteBuffer map(MapMode mode, long position, long size) throws IOException;
文件拷貝測(cè)試對(duì)比
下面我們看一下執(zhí)行下面3段代碼,并且 src.log 文件在不同大小的情況下的測(cè)試耗時(shí)結(jié)果。
1、傳統(tǒng)拷貝
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("耗時(shí):" + (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("耗時(shí):" + (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("耗時(shí):" + (System.currentTimeMillis() - start));
} catch (Exception e) {
e.printStackTrace();
}
}
}
通過(guò)對(duì)不同大小的文件進(jìn)行對(duì)比測(cè)試,我們得到了下面的測(cè)試結(jié)果。
從上面測(cè)試結(jié)果可以看出,mmap 和 sendfile 的方式要遠(yuǎn)遠(yuǎn)優(yōu)于傳統(tǒng)的文件拷貝。對(duì)于 mmap 和 sendfile 在文件較小的時(shí)候, mmap 耗時(shí)更短,當(dāng)文件較大時(shí) sendfile 的方式最優(yōu)。
本文來(lái)自:http://moguhu.com/article/detail?articleId=146