RocketMQ主要存储文件包括,commitLog、consumeQueue、indexFile,所有主题消息都顺序存储在一个文件中,以确保消息的顺序写;同时,RocketMQ又引入了consumeQueue,每个主题包含多个消费队列,每个消费对了对应一个文件,如下图:
CommitLog:消息存储文件,所有消息主题的消息都存储在CommitLog文件中。
ConsumeQueue消息消费队列,消息到达CommitLog文件后,将异步转发到消息消费队列,供消息消费者消费。
3. IndexFile索引文件,为了快速检索CommitLog中的数据。
消息发送存储流程
入口是DefaultMessageStore的的putMessage方法,如图:
核心步骤
获取当前可以写入的commitLog文件
获取锁(这里可以看出commitLog的写入是串行的)
判断是否需要创建commitLog文件
将消息追加到commitLog中
释放锁
将内存数据持久化到磁盘
执行主从复制
跟踪putMessage方法过程中,会进入CommitLog的doAppend方法,如图:
类CommitLog记录了每个消息队列最后写入的消息偏移量,此时了找到此消息应该存储的磁盘位置,然后将消息存储到byteBuffer中(此时并未持久化到磁盘)
存储文件组织与内存映射
RocketMQ通过内存映射来提高IO性能,无论是commitLog、indexFile还是consumeQueue都被设计成固定大小,一个写满了就再创建另一个,另一个的文件名是第一条消息的全局物理偏移量,如图:
在代码里使用MappedFile和MappedFileQueue来组织文件,对应关系如下图:
MappedFile是RocketMQ内存映射文件的具体实现
其中MappedByteBuffer来是java的nio包,使用内存映射,节省了数据在用户空间到内核空间的来回copy,大大的提高了IO性能
内存映射
先来了解一下分页存储和虚拟存储:
分页存储
操作系统在运行程序时,需要为每一个进程分配内存。比如A进程需要200M,B进程需要300M,c进程需要100M。那么操作系统应该如何为他们分配这些内存呢?
这里将程序的地址空间分为多个页,把物理空间也分为和页一样大小的物理块,为了保证进程能够正确运行,即能够在内存中找到每个页面所对应的物理块,系统为每一个进程建立了一张页面映像表,简称页表。在进程地址空间内的所有页,依次在页表中有一页表项,其中记录了相应页在内存中的物理块号。
如下图:
虚拟存储
我们玩的游戏比如Dota2,大小有150G左右,但我们电脑的内存也就8或G者12G,那么电脑是咋运行的游戏呢?答案就是虚拟存储;
所有现代操作系统都使用虚拟内存。虚拟内存意为使用虚假(或虚拟)地址取代物理(硬件RAM)内存地址。这样做好处颇多,总结起来可分为两大类:
一个以上的虚拟地址可指向同一个物理内存地址。
虚拟内存空间可大于实际可用的硬件内存。
应用程序在运行之前没必要全部装入内存,只要一部分就行,其余的部分留在磁盘上,如果程序需要访问的页面没有载入内存,就会产生缺页中断,此时操作系统就会将该页调入内存(swap),以便程序继续运行;
IO原理
有了上面的介绍我们再来看一下操作系统中的IO过程,如图:
进程发起read系统调用
内核随机向磁盘控制器发出信号,要求进行IO操作
DMA将磁盘数据读取到内核缓冲区
内核缓冲区将数据拷贝到用户空间缓冲区
内存映射
上面我们看到操作系统的IO需要将数据在内核空间和用户空间进行拷贝,而内存映射文件则省去了该步骤,利用上面说的虚拟存储技术,可以用多个虚拟地址指向同一块物理地址,如下图:
这样一来,DMA就可以填充对内核空间和用户空间都可见的内存缓冲区了;
优势:
1.再也不需要read和write系统调用了
2.省去了数据从内核空间到用户空间的拷贝
3.用户如果修改了映射的内存空间数据,数据会被标记为脏页,自动刷新到磁盘
RocketMQ正是使用了内存映射代替传统的IO过程,来提高IO性能;