走进 RocketMQ(五)高性能文件读写
前言
Halo,我是白裤。
上一次我们学习了 RocketMQ 的网络通信的机制与设计,今天将带着大家一起了解 RocketMQ 的文件读写方面的设计与优化,看看 RocketMQ 中对于文件的读写跟普通的文件读写有什么区别。
传统内存读写
我们先来看看传统的内存数据读写是怎样的一个流程,下面给出一张用户空间向内核空间到磁盘的一个数据请求过程图。
1.用户空间向内核空间发起读请求,进行 read()系统调用,这个时候触发一次空间切换,从用户态切换到内核态。
2.内核空间的页缓存中没有需要的数据,内核空间向磁盘请求数据,通过 DMA(直接内存访问)将数据从磁盘拷贝到内核空间的页缓存,这个时候涉及到到一次数据拷贝。
3.数据从页缓存将数据拷贝到用户空间缓冲区,这里涉及到一次空间切换,一次数据拷贝。
4.用户空间缓冲区向内核空间 socket 缓冲区发起数据的写请求并且将数据拷贝至 socket 缓冲区中,这里涉及到一次空间切换,一次数据拷贝。
5.数据经由目的地关联的 socket 缓冲区通过 DMA(直接内存访问)拷贝到网卡,此时 socket 缓冲区收到响应之后将写请求结果返回响应给用户空间,这里涉及到一次数据拷贝,一次空间切换。
我们可以看到整个过程发生四次空间切换即上下文切换,四次数据拷贝。
零拷贝读写
我们可以看到数据用传统的方式进行传输是会发生四次上下文切换,四次数据拷贝,这中间的成本是比较大的,效率比较低下,所以大家都使用一些相对于传统方式来说,效率上高一些的新的方式,比如零拷贝。
我们可以仔细看下传统方式的过程,在整个过程中,其实数据由内核空间页缓存拷贝到用户空间缓冲区,再由用户空间缓冲区拷贝到内核空间 socket 缓冲区这两个步骤是没啥必要的,数据完全可以从内核空间页缓存直接拷贝至 socket 缓冲区,这样只要一次数据拷贝即可,大大的降低了复杂性以及成本。
零拷贝就是实现了这种机制,数据从内核空间直接从页缓存拷贝到 socket 缓冲区,这样就消除了这个阶段与用户空间的来回拷贝过程,零拷贝也是指的这个实现,当前比较主流的零拷贝技术实现有 mmap、sendfile(在 Linux2.4+之后,sendfile 可以直接将内核空间的页缓存数据借助 DMA 的 Gather 拷贝至网卡,没有了 CPU 拷贝,实现了真正的零拷贝)。
RocketMQ 文件读写
接下来我们来看看在 RocketMQ 中,对于文件读写的设计以及优化机制,是如何做的。
页缓存与顺序读写
在系统中,为了加速对于文件的读写,会有页缓存的机制对文件进行读写操作,减少磁盘的 I/O。当数据读取的时候,会先到页缓存中进行读取,如果页缓存中有该数据,则直接返回,如果没有该数据,则从磁盘中进行加载,并且会顺序的对其他相邻的数据进行预读取;当数据写入的时候,会将数据先写入到页缓存中,然后由系统的线程去将脏数据页进行刷盘。
RocketMQ 对于文件的读写会基于顺序的方式去读写,这样的方式在页缓存的机制下,其实跟内存读写速度是几乎接近的。
内存映射机制
MappedByteBuffer
RocketMQ 在进行文件的读写时,除了上面所说的页缓存与顺序读写之外,还使用了 MappedByteBuffer 对文件进行读写,这里我们简单了解一下 MappedByteBuffer,MappedByteBuffer 是 Java NIO 中操作大文件的一种方式,它可以将大文件整个映射到虚拟内存中,简化了对于大文件的读写操作,在获取 MappedByteBuffer 的时候,RocketMQ 是通过 Java NIO 中 FileChannel 的 map()方法来获取的,其中使用的 mmap 技术,也就是我们上面所讲到的零拷贝的机制,在这种机制下,数据读写性能比传统的读写要高,map()方法最终底层调用原生的方法 map0()完成文件的映射从而获得文件对应的数据内存地址,通过内存地址,就可以不用通过调用 read 或 write 将文件数据拷贝来拷贝去,而是直接通过内存地址就可以操作文件,实现文件读写的优化。
内存管理与优化
我们知道,在计算机的内存管理中,内存其实分为虚拟内存和物理内存,物理内存指的就是我们计算机中内存条的内存,而虚拟内存指的是计算机管理内存的一种混合技术,虚拟内存是物理内存碎片和磁盘存储的混合内存,那么计算机为什么要将内存分为物理内存和虚拟内存呢?假如你想创建一个 1G 的数据区域内存,但是你的物理内存即你的内存条大小才 256M,那么这个时候怎么办呢,此时只能加载 256M 的数据到物理内存中,其余的暂时放置在磁盘中,等有需要的时候才从磁盘中加载到物理内存。
当我们通过获取到的虚拟内存地址第一次去访问对应物理内存数据时,会发生页中断现象,也就是说数据没在页缓存中,这个时候系统会将数据加载到物理内存中。而当物理内存不够用的时候,你现在需要从磁盘中加载需要的数据,那怎么办呢,这个时候系统会有一种页失效的功能,我们知道,虚拟内存地址空间会被进行分页,而物理内存地址空间会被进行分页帧,页和页帧是会一一对应进行映射的,当物理内存中的页帧都被使用而没有内存空间再加载另外的虚拟内存数据时,物理内存中会将最少使用的页帧失效,然后将失效的页帧数据刷入磁盘,然后把虚拟内存中需要使用的页数据放到页帧中对应映射,这样就解决了有些虚拟内存页的加载映射问题,为了尽量能将文件一次性映射到内存中,所以 RocketMQ 也将文件设置成了固定大小,比如 CommitLog 默认是 1G。
文件预分配与预热
通过我们上面所说的内存映射机制中可以知道,通过虚拟内存地址映射访问物理内存数据时会有一些问题发生,比如第一次访问,会产生页中断现象,而物理内存满了又会发生页失效或者 swap 情况等问题,所以在 RocketMQ 中也使用了一些机制去优化以及降低这些问题。
在 RocketMQ 中,如果文件大小快超过限制的时候,那么会先预分配一个新文件等待写入,这样就不用等在文件写满的时候才去分配新文件,减少在写入过程中还要去分配新文件的时间。除此之外,RocketMQ 还会进行文件的预热,在文件完成内存映射后,此时只是建立了虚拟内存地址空间,还没有真正分配虚拟内存地址空间对应的物理内存,RocketMQ 会写入假值 0 来让系统分配物理内存空间,防止在写入的时候发生缺页异常情况。
小结
好了,今天 RocketMQ 的文件读写优化就学习到这里,接下来我们做个小结吧。
首先我们在了解 RocketMQ 的文件读写设计之前,我们先了解了传统内存读写与零拷贝读写,知道了系统在读写数据时的一个过程,然后我们学习了 RocketMQ 的文件读写优化,包括了顺序读写、内存映射、文件预分配与预热等优化方式,至此,我们已经大概知道了 RocketMQ 在文件读写这块的优化以及设计,好了,今天就学习到这吧,下一次我们来聊聊 RocketMQ 的事务消息设计。
版权声明: 本文为 InfoQ 作者【白裤】的原创文章。
原文链接:【http://xie.infoq.cn/article/df4ba2b355cb58f349bc96612】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论