1、mmap技术:Broker读写磁盘文件的核心技术
Broker中就是大量的使用mmap技术去实现CommitLog这种大磁盘文件的高性能的读写优化的。Broker对磁盘文件的写入主要是借助写入os cache来实现性能优化的,以为写入了os cache,相当于就是写入内存一样的性能,后续等os内核中的线程异步把cache中的数据刷入磁盘文件即可。
2、传统文件IO操作的多次数据拷贝问题
假设没有mmap技术,使用最最传统的和基本的普通文件IO操作去进行磁盘文件的读写,会存在什么性能问题?--》多次数据拷贝问题
首先,假设我们有一个程序,这个程序需要对磁盘文件发起IO操作读取他里面的数据到他自己这儿来,那么他会经过一个顺序:首先从磁盘上把数据读取到内核IO缓冲区里去,然后再从内核IO缓存区里去读取到用户进程私有空间里去,然后我们才能拿到这个文件里的数据。
为了读取磁盘文件里的数据,是不是发生了两次数据拷贝?没错,所以这个就是普通的IO操作的一个弊端,必然涉及到两次数据拷贝操作,对磁盘读写性能是有影响的。如果我们要将一些数据写入到磁盘文件里去呢?那这个就是一样的过程了,必须先把数据写入到用户进程私有空间里去,然后从这里再进入内核IO缓冲区,最后再进入磁盘文件里去。
这种就是传统普通的IO的问题,有两次数据拷贝的问题。
3、RocketMQ是如何基于mmap技术+page cache技术优化的?
RocketMQ如何利用mmap技术配合page cache技术实现文件读写优化的?
首先,RocketMQ底层对CommitLog、ConsumeQueue之类的磁盘文件的读写操作,基本上都会采用mmap技术来实现。如果具体到代码层面,就是基于JDK NIO包下的MappedByteBuffer的map()函数,来先将一个磁盘文件(比如一个CommitLog文件,或者是一个ConsumeQueue文件)映射到内存里来。
这个内存映射是什么意思呢? 有的人可能会误认为是直接把那些磁盘文件里的数据给读取到内存里来了,这个意思不完全是对的。因为刚开始建立映射的时候,并没有任何的数据拷贝操作,其实磁盘文件还是停留在那里。只不过他把物理上的磁盘文件的一些地址和用户进程私有空间的一些虚拟内存地址进行了一个映射。这个地址映射的过程,就是JDK NIO包下的MappedByteBuffer.map()函数干的事情,底层就是基于mmap技术实现的。这个mmap技术在尽心文件映射的时候,一般都大小限制,在1.5GB~2GB之间。所以RocketMQ才让CommitLog单个文件在1GB,ConsumeQueue文件在5.72MB,不会太大。这样限制了RocketMQ底层文件的大小,就可以在进行文件读写的时候,很方便的进行内存映射了。
PageCache实际上就是对应于虚拟内存。PageCache实际上就是对应于虚拟内存。
PageCache实际上就是对应于虚拟内存。PageCache实际上就是对应于虚拟内存。
PageCache实际上就是对应于虚拟内存。PageCache实际上就是对应于虚拟内存。
基于mmap技术+pageCache技术实现高性能的文件读写。
已经可以对已经映射到内存里的磁盘文件进行读写操作了,比如要写入消息到CommitLog文件,你先把CommitLog文件通过MappedByteBuffer的map()函数映射其他地址到你的虚拟内存地址。接着就可以对这个MappedByteBuffer执行写入操作了,写入的时候他会直接进入PageCache中,然后过一段时间之后,由os的线程异步刷入磁盘中。
这样的情况下,就是从PageCache里拷贝到磁盘文件里而已。这个就是你使用mmap技术之后,相比于传统磁盘IO的一个性能优化。
如果我们要从磁盘文件里读取数据呢?那么此时要判断一下,当前你要读取的数据是否在PageCache?如果在的话,就可以直接从PageCache里读取了。比如刚写入CommitLog的数据还在PageCache里,此时你Consumer来消费肯定是从PageCache里读取数据的。但是如果PageCache里没有你要的数据,那么此时就会从磁盘文件里加载数据到PageCache中去。而且PageCache技术在加载数据的时候,还会将你加载的数据块的临近的其他数据块也一起加载到PageCache里去。
也就是说在你读取数据的时候,也就仅仅发生了一次拷贝而已,而不是两次拷贝,所以这个性能相对于传统IO来说,肯定是又提高了。
4、预映射机制+文件预热机制
几个Broker针对上述的磁盘文件高性能读写机制做的一些优化:
(1)内存预映射机制:Broker会针对磁盘上的各种CommitLog、ConsumeQueue文件预先分配好MappedFile,也就是提前对一些可能接下来要读写的磁盘文件,提前使用MappedByteBuffer执行map()函数完成映射。这样后续读写文件的时候,就可以直接执行了。
(2)文件预热:在提前对一些文件完成映射之后,因为映射不会直接将数据加载到内存里来,那么后续在读取尤其是CommitLog、ConsumeQueue的时候,其实有可能会频繁的从磁盘里加载数据到内存中去。所以其实,在执行完map()函数之后,会进行madvise系统调用,就是提前尽可能多的把磁盘文件加载到内存里去。
通过上述优化,才真正实现一个效果,就是写磁盘文件的时候都是进入PageCache的,保证写入高性能;同时尽可能多的通过map+madvise的映射后预热机制,把磁盘文件里的数据尽可能多的加载到PageCache里来,后续对ConsumeQueue、CommitLog进行读取的时候,才能尽可能从内存里读取数据。