传统文件IO操作产生的数据多次拷贝问题
假如没有使用mmap技术的时候,使用最传统和基本普通文件进行io操作会产生数据多拷贝问题。
比如从磁盘上把数据读取到内核IO缓冲区里面,然后再从内核IO缓冲区中读取到用户进程私有空间里去,然后我们才能拿到这个数据。
![](https://img.haomeiwen.com/i9857208/acef9c50f91bac8f.png)
如上图显示,可以明显的看出数据被拷贝了两次,这样肯定对磁盘读性能是有影响的。同样的如果想给磁盘中写内容,也是得先从用户进程的私有空间把数据拷贝到内核IO缓冲区,然后从内核IO缓冲区再拷贝到磁盘文件。
![](https://img.haomeiwen.com/i9857208/3f5dc97ed34d733f.png)
Rocket是如何基于mmap技术和page cache来优化磁盘文件读写的
首先,RocketMQ底层对commitLog、consumeQueue之类的磁盘文件的读写操作都采用了mmap技术。具体到代码里面就是利用JDK里面NIO的MapperByteBuffer的map()函数,来先将磁盘文件(CommitLog文件、consumeQueue文件)映射到内存里来。
所谓的这个内存映射是什么意思呢。看见这个内存映射有的人可能就会认为是把磁盘文件的数据读取到内存里面,其实这不完全正确。因为在刚开始建立映射的时候,并没有任何的数据拷贝操作,其实磁盘文件还是停留在哪里,只不过就是它把物理磁盘上的一些地址和用户进程私有空间的一些虚拟内存地址进行了一个映射。这个映射的过程就是JDK下面的NIO包里面的MapperByteBuffer的map函数干的事情,底层就是基于mmap技术实现的。
![](https://img.haomeiwen.com/i9857208/e0e365ac6ca1603a.png)
这个mmap技术在地址映射的过程中对文件的大小是有限制对,在1.5G~2G之间,所以,RocketMQ就会把单个的commitLog文件大小控制在1GB,consumeQueue文件大小控制在5.72MB,这样就在读写的时候,方便的进行内存映射了。
基于mmap技术+pagecache技术实现高性能的文件读写
磁盘文件和用户进程私有空间的一些虚拟内存地址进行映射之后,我们就可以对这个已经映射到内存里的磁盘文件进行读写了,比如要写入commitLog文件,就会先把commitlog文件通过MapperByteBuffer的map函数映射起地址到你的虚拟内存地址。接着可以对这个mapperByteBuffer执行写入操作,写入的时候会直接写入到pagecache中,然后过一段时间,由os的线程异步刷入到磁盘中。
![](https://img.haomeiwen.com/i9857208/93470953102efd32.png)
此时我们看上面的图是不是就会发现只有一次数据的拷贝,这样是不是性能就有所提高了呢。这就是基于mmap技术相比传统io操作的一个性能优化。
接着如果我们从磁盘上读取数据的时候,就会先在pagecache中判断一些缓冲中是否有数据,如果有就直接读取了。比如刚刚把数据写入commiglog中,此时consumer就从里面直接消费数据,那基本pagecache中肯定会有该数据,哪怕如果没有,此时就会从磁盘文件中加载数据到pagecache中,而且pagecache在加载数据的时候,还会把你要加载的数据块临近的其他数据块也一起加载到pagecache里去。
![](https://img.haomeiwen.com/i9857208/f2c870b0db95acb4.png)
预映射机制+文件预热机制
(1)内存预映射机制:Broker会针对磁盘上的各种commitlog和consumeQueue预先分配好mapperfile,也就是提前对一些接下来可能要读写的磁盘文件,提前使用mapperByteBuffer的map函数完成映射,这样在后续使用的时候,就可以直接执行了。
(2)文件预热:在提前对一些文件进行映射完成之后,但是不会直接将数据加载到内存里面去,后续在读取commitlog或者consumeQueue的时候,可能会造成频繁的读取数据到内存中。所以,在执行完map函数之后,还会进行madvise系统调用,就是提前尽可能都的将磁盘文件加载到内存里去。
总结:在写文件的时候,都是先进入os cache中,保证写入高性能。同时尽可能多的通过map函数和madvise的映射后的预热机制,把磁盘文件里的数据尽可能多的加载到内存中去,从而来保证后续对commitlog文件和consumeQueue文件的读取是从内存中读取的。
网友评论