首頁 > 軟體

淺析MMAP零拷貝在RocketMQ中的運用

2022-07-27 18:00:30

什麼是零拷貝?

零拷貝(英語: Zero-copy)技術是指計算機執行操作時,CPU不需要先將資料從某處記憶體複製到另一個特定區域。這種技術通常用於通過網路傳輸檔案時節省CPU週期和記憶體頻寬。

零拷貝技術可以減少資料拷貝和共用匯流排操作的次數,消除傳輸資料在記憶體之間不必要的中間拷貝次數,從而有效地提高資料傳輸效率。

零拷貝技術減少了使用者程序地址空間和核心地址空間之間因為上下文切換而帶來的開銷。

可以看出沒有說不需要拷貝,只是說減少冗餘不必要的拷貝。

下面這些元件、框架中均使用了零拷貝技術:Kafka、Netty、Rocketmq、Nginx、Apache。

傳統資料傳送機制

比如:讀取檔案,再用socket傳送出去,實際經過四次copy。

偽碼實現如下:

buffer = File.read() 
Socket.send(buffer)

四次拷貝的過程:

  • 第一次:將磁碟檔案,讀取到作業系統核心緩衝區;
  • 第二次:將核心緩衝區的資料,copy到應用程式的buffer;
  • 第三步:將application應用程式buffer中的資料,copy到socket網路傳送緩衝區(屬於作業系統核心的緩衝區);
  • 第四次:將socket buffer的資料,copy到網路卡,由網路卡進行網路傳輸。

分析上述的過程,雖然引入DMA來接管CPU的中斷請求,但四次copy是存在“不必要的拷貝”的。實際上並不需要第二個和第三個資料副本。應用程式除了快取資料並將其傳輸回通訊端緩衝區之外什麼都不做。相反,資料可以直接從讀緩衝區傳輸到通訊端緩衝區。

顯然,第二次和第三次資料copy其實在這種場景下沒有什麼幫助反而帶來開銷(DMA拷貝速度一般比CPU拷貝速度快一個數量級),這也正是零拷貝出現的背景和意義。

打個比喻:200M的資料,讀取檔案,再用socket傳送出去,實際經過四次copy(2次cpu拷貝每次100ms ,2次DMA拷貝每次10ms),傳統網路傳輸的話:合計耗時將有220ms。

同時,read和send都屬於系統呼叫,每次呼叫都牽涉到兩次上下文切換:

總結下,傳統的資料傳送所消耗的成本:4次拷貝,4次上下文切換。4次拷貝,其中兩次是DMA copy,兩次是CPU copy。

mmap記憶體對映

mmap可以將硬碟上檔案的位置和應用程式緩衝區(application buffers)進行對映(建立一種一一對應關係),將檔案直接對映到使用者空間,所以實際檔案讀取時根據這個對映關係,直接將檔案從硬碟拷貝到使用者空間,只進行了一次資料拷貝,不再有檔案內容從硬碟拷貝到核心空間的一個緩衝區。

mmap記憶體對映將會經歷:3次拷貝: 1次cpu copy,2次DMA copy;

打個比喻:200M的資料,讀取檔案,再用socket傳送出去,如果是使用MMAP實際經過三次copy(1次cpu拷貝每次100ms ,2次DMA拷貝每次10ms),合計只需要120ms。

從資料拷貝的角度上來看,就比傳統的網路傳輸,效能提升了近一倍。

mmap()是在<sys/mman.h>中定義的一個函數,此函數的作用是建立一個新的虛擬記憶體區域,並將指定的物件對映到此區域。mmap其實就是通過記憶體對映的機制來進行檔案操作。

mmap的使用:

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;

public class MmapDemo {
    public static void main(String[] args) throws IOException {

        File f = new File("/root/map.txt");
        RandomAccessFile randomAccessFile = new RandomAccessFile(f, "rw");
        FileChannel fileChannel = randomAccessFile.getChannel();
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096);
        mappedByteBuffer.put("hello".getBytes(StandardCharsets.UTF_8));
        mappedByteBuffer.flip();
        byte[] bytes = new byte[5];
        mappedByteBuffer.get(bytes, 0, 5);
        System.out.println("content:" + new String(bytes, StandardCharsets.UTF_8));
    }
}

使用命令:

strace -ff -o out java MmapDemo

追蹤MmapDemo程式產生的系統呼叫:

openat(AT_FDCWD, "/root/map.txt", O_RDWR|O_CREAT, 0666) = 5
... ...
fstat(5, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
ftruncate(5, 4096)                      = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 5, 0) = 0x7f9e1c000000

發現底層呼叫了mmap系統呼叫,後續並沒有產生write等系統呼叫,說明資料的讀寫直接發生在了應用態。

FileChannal的使用

另外RocketMQ在原始碼中還使用了FileChannel來做檔案的寫入。

package com.morris.rocketmq.mmap;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;

public class FileChannelDemo {
    public static void main(String[] args) throws IOException {
        File f = new File("d:\map.txt");
        RandomAccessFile randomAccessFile = new RandomAccessFile(f, "rw");
        FileChannel fileChannel = randomAccessFile.getChannel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(128);
        byteBuffer.put("hello rocketmq".getBytes(StandardCharsets.UTF_8));
        byteBuffer.flip();
        fileChannel.write(byteBuffer);
        fileChannel.close();
    }
}

為什麼RocketMQ會同時使用FileChannel和MappedByteBuffer在做檔案的寫入,讀取卻只用MappedByteBuffer?

RocketMQ中MMAP運用

如果按照傳統的方式進行資料傳送,那肯定效能上不去,作為MQ也是這樣,尤其是RocketMQ,要滿足一個高並行的訊息中介軟體,一定要進行優化。所以RocketMQ使用的是MMAP。

RocketMQ原始碼中,使用MappedFile這個類進行MMAP的對映。

這裡需要注意的是,採用MappedByteBuffer這種記憶體對映的方式一次只能對映2G的檔案至使用者態的虛擬記憶體,這也是為何RocketMQ預設設定單個CommitLog紀錄檔資料檔案為1G的原因了。

為什麼是2G?

sun.nio.ch.FileChannelImpl#map

public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException {
    if (size > Integer.MAX_VALUE)
        throw new IllegalArgumentException("Size exceeds Integer.MAX_VALUE");

雖然size是long型別,但是限制了size只能是int的最大值,也就是2G。

mmap在原始碼MappedFile中的使用:

public MappedFile(final String fileName, final int fileSize) throws IOException {
	init(fileName, fileSize);
}

private void init(final String fileName, final int fileSize) throws IOException {
	this.fileName = fileName;
	this.fileSize = fileSize;
	this.file = new File(fileName);
	this.fileFromOffset = Long.parseLong(this.file.getName());
	boolean ok = false;

	ensureDirOK(this.file.getParent());

	try {
		this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
		this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);
		TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize);
		TOTAL_MAPPED_FILES.incrementAndGet();
		ok = true;
	} catch (FileNotFoundException e) {
		log.error("Failed to create file " + this.fileName, e);
		throw e;
	} catch (IOException e) {
		log.error("Failed to map file " + this.fileName, e);
		throw e;
	} finally {
		if (!ok && this.fileChannel != null) {
			this.fileChannel.close();
		}
	}
}

到此這篇關於MMAP零拷貝在RocketMQ中的運用的文章就介紹到這了,更多相關MMAP RocketMQ運用內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com