淺析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ù)拷貝和共享總線(xiàn)操作的次數(shù),消除傳輸數(shù)據(jù)在存儲(chǔ)器之間不必要的中間拷貝次數(shù),從而有效地提高數(shù)據(jù)傳輸效率。
零拷貝技術(shù)減少了用戶(hù)進(jìn)程地址空間和內(nèi)核地址空間之間因?yàn)樯舷挛那袚Q而帶來(lái)的開(kāi)銷(xiāo)。
可以看出沒(mé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ò)程:
- 第一次:將磁盤(pán)文件,讀取到操作系統(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)景下沒(méi)有什么幫助反而帶來(lái)開(kāi)銷(xiāo)(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ù)脑?huà):合計(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可以將硬盤(pán)上文件的位置和應(yīng)用程序緩沖區(qū)(application buffers)進(jìn)行映射(建立一種一一對(duì)應(yīng)關(guān)系),將文件直接映射到用戶(hù)空間,所以實(shí)際文件讀取時(shí)根據(jù)這個(gè)映射關(guān)系,直接將文件從硬盤(pán)拷貝到用戶(hù)空間,只進(jìn)行了一次數(shù)據(jù)拷貝,不再有文件內(nèi)容從硬盤(pán)拷貝到內(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ù)并沒(méi)有產(chǎn)生write等系統(tǒng)調(diào)用,說(shuō)明數(shù)據(jù)的讀寫(xiě)直接發(fā)生在了應(yīng)用態(tài)。
FileChannal的使用
另外RocketMQ在源碼中還使用了FileChannel來(lái)做文件的寫(xiě)入。
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在做文件的寫(xiě)入,讀取卻只用MappedByteBuffer?
RocketMQ中MMAP運(yùn)用
如果按照傳統(tǒng)的方式進(jìn)行數(shù)據(jù)傳送,那肯定性能上不去,作為MQ也是這樣,尤其是RocketMQ,要滿(mǎn)足一個(gè)高并發(fā)的消息中間件,一定要進(jìn)行優(yōu)化。所以RocketMQ使用的是MMAP。
RocketMQ源碼中,使用MappedFile這個(gè)類(lèi)進(jìn)行MMAP的映射。
這里需要注意的是,采用MappedByteBuffer這種內(nèi)存映射的方式一次只能映射2G的文件至用戶(hù)態(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類(lèi)型,但是限制了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-02Java實(shí)現(xiàn)每日給女友微信發(fā)送早安信息
這篇文章主要為大家詳細(xì)介紹了Java如何實(shí)現(xiàn)每日給女友微信發(fā)送早安等微信信息,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,需要的可以了解一下2022-12-12spring boot tomcat jdbc pool的屬性綁定
這篇文章主要介紹了spring boot tomcat jdbc pool的屬性綁定的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下2018-01-01Java 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