图文并茂!深入了解RocketMQ的内存映射机制

欢少的成长之路

共 3602字,需浏览 8分钟

 · 2022-05-18

大家好,我是Leo。

今天聊一下RocketMQ的内存映射机制

传统文件IO机制

假如没有使用mmap技术的时候,使用最传统和基本的普通文件进行io操作会产生数据多拷贝问题。比如从磁盘上把数据读取到内核IO缓冲区里面,然后再从内核IO缓冲区中读取到用户进程私有空间里去,然后我们才能拿到这个数据。

如上图显示,可以明显的看出数据被拷贝了两次,这样肯定对磁盘读性能是有影响的。同样的如果想给磁盘中写内容,也是得先从用户进程的私有空间把数据拷贝到内核IO缓冲区,然后从内核IO缓冲区再拷贝到磁盘文件。

内存映射机制

优化

后续虽然基于磁盘的顺序写可以极大提高 IO 的写效率,但是基于文件的存储采用多拷贝方式。在性能提升上会很有限,RocketMQ 引入了内存映射,将磁盘文件映射到内存中,以操作内存的方式操作磁盘,性能又提升了一个档次。

所谓的这个内存映射是什么意思呢。看见这个内存映射有的人可能就会认为是把磁盘文件的数据读取到内存里面,其实这不完全正确。因为在刚开始建立映射的时候,并没有任何的数据拷贝操作,其实磁盘文件还是停留在哪里,只不过就是它把 物理磁盘上的一些地址和用户进程私有空间的一些虚拟内存地址进行了一个映射。这个映射的过程就是JDK下面的NIO包里面的MapperByteBuffer的map函数干的事情,底层就是基于mmap技术实现的。

这个mmap技术在地址映射的过程中对文件的大小是有限制的,在1.5G~2G之间,所以RocketMQ就会把单个的commitLog文件大小控制在1GB,consumeQueue文件大小控制在5.72MB,这样就在读写的时候,方便的进行内存映射了。

在 JAVA 中可通过 FileChannel 的 map 方法创建内存映射文件。如下图所示

在 Linux 服务器中由该方法创建的文件使用的就是操作系统的 pagecache,即页缓存。

Linux 操作系统中的内存使用策略时会尽可能地利用机器的物理内存,并常驻内存中,就是所谓的页缓存。在操作系统的内存不够的情况下,采用缓存置换算法,例如 LRU 将不常用的页缓存回收,即操作系统会自动管理这部分内存。

如果 RocketMQ Broker 进程异常退出,存储在页缓存中的数据并不会丢失,操作系统会定时将页缓存中的数据持久化到磁盘,做到数据安全可靠。不过如果是机器断电等异常情况,存储在页缓存中的数据就有可能丢失。

实现

磁盘文件和用户进程私有空间的一些虚拟内存地址进行映射之后,我们就可以对这个已经映射到内存里的磁盘文件进行读写了,比如要写入commitLog文件,就会先把commitlog文件通过MapperByteBuffer的map函数映射起地址到你的虚拟内存地址。接着可以对这个mapperByteBuffer执行写入操作,写入的时候会直接写入到pagecache中,然后过一段时间,由os的线程异步刷入到磁盘中。

此时我们看上面的图是不是就会发现只有一次数据的拷贝,这样是不是性能就有所提高了呢。这就是基于mmap技术相比传统io操作的一个性能优化。接着如果我们从磁盘上读取数据的时候,就会先在pagecache中判断一些缓冲中是否有数据,如果有就直接读取了。比如刚刚把数据写入commiglog中,此时consumer就从里面直接消费数据,那基本pagecache中肯定会有该数据,哪怕如果没有,此时就会从磁盘文件中加载数据到pagecache中,而且pagecache在加载数据的时候,还会把你要 加载的数据块临近的其他数据块也一起加载到pagecache里去。

源码

RocketMQ的内存映射机制的几个重要类

  • MappedFile 映射文件
  • MappedFileQueue 映射文件队列
  • AllocateMappedFileService 创建MappedFile核心类

MappedFileQueue 是MappedFile的管理容器,MappedFileQueue是对存储目录的封装,该目录下会存在多个内存映射文件(mappedFile)。

  • storePath:存储目录
  • mappedFileSize:单个文件的存储大小
  • mappedFiles:MappedFile文件集合
  • allocateMappedFileService:创建MappedFile服务类
  • flushedWhere:当前刷盘指针,表示该指针之前的所有数据全部持久化到硬盘
  • committedWhere:当前数据提交指针,内存中byteBuffer当前的写指针,该值大于等于】flushedWhere

MappedFile初始化(init)

transientStorePoolEnable为true 表示内容先存储在堆外内存,然后通过 Commit 线程将数据提交到内存映射Buffer中,再通过 Flush 线程将内存映射 Buffer 中的数据持久化到磁盘中

MappedFile提交(commit)

内存映射文件的提交动作由 MappedFile的commit 方法实现:执行提交操作, commitLeastPages 为本次提交最小的页数,如果待提交数据不满 commitLeastPages ,则不执行本次提交操作,待下次提交。writeBuffer 如果为空,直接返回 wrotePosition 指针 ,无须执行 commit 操作, 表明commit 操作主体是 writeBuffer.

判断是否执行commit操作。如果文件已满返回true;如果commitLeastPages大于0,则比较 wrotePosition (当前 writeBuffe 的写指针)与上一次提交的指针(committedPosition) 的差值,除以 OS_ PAGE_ SIZE 得到当前脏页的数量,如果大于 commitLeastPages 则返回 true ;如果 commitLeastPages小于0 表示只要存在脏页就提交

MappedFile刷盘(flush)

刷盘是指将内存中数据刷写到磁盘,永久存储到磁盘中,其具体实现由mappedFile的flush方法实现:刷写磁盘,直接调用 mappedByteBuffer或者 fileChannel的 force 方法将内存中的数据持久化到磁盘,那么 flushedPosition 应该等于 MappedByteBuffer 中的写指针;如果 writeBuffer 不为空, 则flushedPosition 应等于上一次 commit 指针;因为上一次提交的数据就是进入到 MappedByteBuffer 中的数据;如 writeBuffer 空,数据是直接进入到 MappedByteBuffer, wrotePosition 代表的是 MappedByteBuffer 中的指针,故设置 flushedPosition 为wrotePosition。

获取MappedFile最大读指针(getReadPositon)

RockketMQ文件的一个组织方式是内存映射方式,预先申请一块连续的固定大小的内存,需要一套指针标识当前最大有效数据 的位置。获取当前文件最大的可读指针。如果 writeBuffer为空, 则直接返回当前的写指针;如果 writeBuffer 不为空, 则返回上一次提交的指针。在MappedFile 设计中,只有提交了的数 据(写入到 MappedByteBuffer FileChannel 中的数据 )才是安全的数据。

MappedFile销毁

关闭mappedFile,释放掉资源,并关闭文件通道,删除物理文件。

TransientStorePool

TransientStorePool:短暂的存储池。RocketMQ单独创建一个MappedByteBuffer内存缓存池,用来临时存储数据,数据先写入该内存映射中,然后由commit线程定时将数据从该内存复制到与目的物理文件对应的内存映射中。RokcetMQ引入该机制主要的原因是提供一种内存锁定,将当前堆外内存一直锁定在内存中,避免被进程将内存交换到磁盘。

往期推荐

2022年文章目录整理

RocketMQ性能提升

图文并茂!深入理解RocketMQ的刷盘机制

图文并茂!深入了解RocketMQ的过期删除机制

结尾

本篇文件介绍了RocketMQ的内存映射机制,从传统的多拷贝模式,后来的磁盘顺序写的优化。最终采用 mmap+ page cache 技术实现高性能的文件读写。

学习途中感谢 简书作者:G__yuan 和 简书作者:mingxungu

非常欢迎大家加我个人微信有关后端方面的问题我们在群内一起讨论! 我们下期再见!

欢迎『点赞』、『在看』、『转发』三连支持一下,下次见~



浏览 24
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报