淺析MMAP零拷貝在RocketMQ中的運(yùn)用
什么是零拷貝?
零拷貝(英語(yǔ): Zero-copy)技術(shù)是指計(jì)算機(jī)執(zhí)行操作時(shí),CPU不需要先將數(shù)據(jù)從某處內(nèi)存復(fù)制到另一個(gè)特定區(qū)域。這種技術(shù)通常用于通過(guò)網(wǎng)絡(luò)傳輸文件時(shí)節(jié)省CPU周期和內(nèi)存帶寬。
零拷貝技術(shù)可以減少數(shù)據(jù)拷貝和共享總線操作的次數(shù),消除傳輸數(shù)據(jù)在存儲(chǔ)器之間不必要的中間拷貝次數(shù),從而有效地提高數(shù)據(jù)傳輸效率。
零拷貝技術(shù)減少了用戶進(jìn)程地址空間和內(nèi)核地址空間之間因?yàn)樯舷挛那袚Q而帶來(lái)的開銷。
可以看出沒有說(shuō)不需要拷貝,只是說(shuō)減少冗余不必要的拷貝。
下面這些組件、框架中均使用了零拷貝技術(shù):Kafka、Netty、Rocketmq、Nginx、Apache。
傳統(tǒng)數(shù)據(jù)傳送機(jī)制
比如:讀取文件,再用socket發(fā)送出去,實(shí)際經(jīng)過(guò)四次copy。
偽碼實(shí)現(xiàn)如下:
buffer = File.read() Socket.send(buffer)
四次拷貝的過(guò)程:
- 第一次:將磁盤文件,讀取到操作系統(tǒng)內(nèi)核緩沖區(qū);
- 第二次:將內(nèi)核緩沖區(qū)的數(shù)據(jù),copy到應(yīng)用程序的buffer;
- 第三步:將application應(yīng)用程序buffer中的數(shù)據(jù),copy到socket網(wǎng)絡(luò)發(fā)送緩沖區(qū)(屬于操作系統(tǒng)內(nèi)核的緩沖區(qū));
- 第四次:將socket buffer的數(shù)據(jù),copy到網(wǎng)卡,由網(wǎng)卡進(jìn)行網(wǎng)絡(luò)傳輸。

分析上述的過(guò)程,雖然引入DMA來(lái)接管CPU的中斷請(qǐng)求,但四次copy是存在“不必要的拷貝”的。實(shí)際上并不需要第二個(gè)和第三個(gè)數(shù)據(jù)副本。應(yīng)用程序除了緩存數(shù)據(jù)并將其傳輸回套接字緩沖區(qū)之外什么都不做。相反,數(shù)據(jù)可以直接從讀緩沖區(qū)傳輸?shù)教捉幼志彌_區(qū)。
顯然,第二次和第三次數(shù)據(jù)copy其實(shí)在這種場(chǎng)景下沒有什么幫助反而帶來(lái)開銷(DMA拷貝速度一般比CPU拷貝速度快一個(gè)數(shù)量級(jí)),這也正是零拷貝出現(xiàn)的背景和意義。
打個(gè)比喻:200M的數(shù)據(jù),讀取文件,再用socket發(fā)送出去,實(shí)際經(jīng)過(guò)四次copy(2次cpu拷貝每次100ms ,2次DMA拷貝每次10ms),傳統(tǒng)網(wǎng)絡(luò)傳輸?shù)脑挘汉嫌?jì)耗時(shí)將有220ms。
同時(shí),read和send都屬于系統(tǒng)調(diào)用,每次調(diào)用都牽涉到兩次上下文切換:

總結(jié)下,傳統(tǒng)的數(shù)據(jù)傳送所消耗的成本:4次拷貝,4次上下文切換。4次拷貝,其中兩次是DMA copy,兩次是CPU copy。
mmap內(nèi)存映射
mmap可以將硬盤上文件的位置和應(yīng)用程序緩沖區(qū)(application buffers)進(jìn)行映射(建立一種一一對(duì)應(yīng)關(guān)系),將文件直接映射到用戶空間,所以實(shí)際文件讀取時(shí)根據(jù)這個(gè)映射關(guān)系,直接將文件從硬盤拷貝到用戶空間,只進(jìn)行了一次數(shù)據(jù)拷貝,不再有文件內(nèi)容從硬盤拷貝到內(nèi)核空間的一個(gè)緩沖區(qū)。
mmap內(nèi)存映射將會(huì)經(jīng)歷:3次拷貝: 1次cpu copy,2次DMA copy;

打個(gè)比喻:200M的數(shù)據(jù),讀取文件,再用socket發(fā)送出去,如果是使用MMAP實(shí)際經(jīng)過(guò)三次copy(1次cpu拷貝每次100ms ,2次DMA拷貝每次10ms),合計(jì)只需要120ms。
從數(shù)據(jù)拷貝的角度上來(lái)看,就比傳統(tǒng)的網(wǎng)絡(luò)傳輸,性能提升了近一倍。
mmap()是在<sys/mman.h>中定義的一個(gè)函數(shù),此函數(shù)的作用是創(chuàng)建一個(gè)新的虛擬內(nèi)存區(qū)域,并將指定的對(duì)象映射到此區(qū)域。mmap其實(shí)就是通過(guò)內(nèi)存映射的機(jī)制來(lái)進(jìn)行文件操作。
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程序產(chǎn)生的系統(tǒng)調(diào)用:
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
發(fā)現(xiàn)底層調(diào)用了mmap系統(tǒng)調(diào)用,后續(xù)并沒有產(chǎn)生write等系統(tǒng)調(diào)用,說(shuō)明數(shù)據(jù)的讀寫直接發(fā)生在了應(yīng)用態(tài)。
FileChannal的使用
另外RocketMQ在源碼中還使用了FileChannel來(lái)做文件的寫入。
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會(huì)同時(shí)使用FileChannel和MappedByteBuffer在做文件的寫入,讀取卻只用MappedByteBuffer?
RocketMQ中MMAP運(yùn)用
如果按照傳統(tǒng)的方式進(jìn)行數(shù)據(jù)傳送,那肯定性能上不去,作為MQ也是這樣,尤其是RocketMQ,要滿足一個(gè)高并發(fā)的消息中間件,一定要進(jìn)行優(yōu)化。所以RocketMQ使用的是MMAP。
RocketMQ源碼中,使用MappedFile這個(gè)類進(jìn)行MMAP的映射。
這里需要注意的是,采用MappedByteBuffer這種內(nèi)存映射的方式一次只能映射2G的文件至用戶態(tài)的虛擬內(nèi)存,這也是為何RocketMQ默認(rèn)設(shè)置單個(gè)CommitLog日志數(shù)據(jù)文件為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();
}
}
}
到此這篇關(guān)于MMAP零拷貝在RocketMQ中的運(yùn)用的文章就介紹到這了,更多相關(guān)MMAP RocketMQ運(yùn)用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springcloud-gateway整合jwt+jcasbin實(shí)現(xiàn)權(quán)限控制的詳細(xì)過(guò)程
這篇文章主要介紹了springcloud-gateway整合jwt+jcasbin實(shí)現(xiàn)權(quán)限控制,基于springboot+springcloud+nacos的簡(jiǎn)單分布式項(xiàng)目,項(xiàng)目交互采用openFeign框架,單獨(dú)提取出來(lái)成為一個(gè)獨(dú)立的model,需要的朋友可以參考下2023-02-02
Java實(shí)現(xiàn)每日給女友微信發(fā)送早安信息
這篇文章主要為大家詳細(xì)介紹了Java如何實(shí)現(xiàn)每日給女友微信發(fā)送早安等微信信息,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,需要的可以了解一下2022-12-12
spring boot tomcat jdbc pool的屬性綁定
這篇文章主要介紹了spring boot tomcat jdbc pool的屬性綁定的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下2018-01-01
Java 8 Stream Api 中的 map和 flatMap 操作方法
Java 8提供了非常好用的 Stream API ,可以很方便的操作集合。今天通過(guò)這篇文章給大家分享Java 8 Stream Api 中的 map和 flatMap 操作方法,需要的朋友可以參考下2019-11-11
解決Maven項(xiàng)目中 Invalid bound statement 無(wú)效的綁定問(wèn)題
這篇文章主要介紹了解決Maven項(xiàng)目中 Invalid bound statement 無(wú)效的綁定問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06

