浅析MMAP零拷贝在RocketMQ中的运用
作者:morris131 时间:2021-11-21 01:59:47
什么是零拷贝?
零拷贝(英语: 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();
}
}
}
来源:https://blog.csdn.net/u022812849/article/details/126007918