RocketMQ通过使用内存映射文件来提高IO访问性能,无论是CommitLog、ConsumeQueue还是IndexFile,单个文件都被设计为固定长度,如果一个文件写满以后再创建一个新文件,文件名就为该文件第一条消息对应的全局物理偏移量。内存映射区域不得超过Integer.MAX_VALUE
(约2GB),超大文件需分段映射。
RocketMQ使用MappedFile、MappedFileQueue来封装存储文件,其关系如下:
MappedFileQueue映射文件队列
MappedFileQueue是MappedFile的管理容器,MappedFileQueue是对存储目录的封装,例如CommitLog文件的存储路径${ROCKET_HOME}/store/commitlog/,该目录下会存在多个内存映射文件(MappedFile)。
下面让我们一一来介绍MappedFileQueue的核心属性。
-
String storePath:存储目录。
-
int mappedFileSize:单个文件的存储大小。
-
CopyOnWriteArrayList mappedFiles:MappedFile文件集合。
-
AllocateMappedFileService allocateMappedFileService:创建MappedFile服务类。
-
long flushedWhere = 0:当前刷盘指针,表示该指针之前的所有数据全部持久化到磁盘。
-
long committedWhere = 0:当前数据提交指针,内存中ByteBuffer当前的写指针,该值大于等于flushedWhere。
MappedFile查询消息的核心方法
根据消息存储时间戳来查找MappdFile。从MappedFile列表中第一个文件开始查找,找到第一个最后一次更新时间大于待查找时间戳的文件,如果不存在,则返回最后一个MappedFile文件。
根据消息偏移量offset查找MappedFile。根据offet查找MappedFile直接使用offset%-mapped FileSize是否可行?答案是否定的,由于使用了内存映射,只要存在于存储目录下的文件,都需要对应创建内存映射文件,如果不定时将已消费的消息从存储文件中删除,会造成极大的内存压力与资源浪费,所有RocketMQ采取定时删除存储文件的策略,也就是说在存储文件中,第一个文件不一定是00000000000000000000,因为该文件在某一时刻会被删除,故根据offset定位MappedFile的算法为(int)((offset /this.mappedFileSize)-(mappedFile.getFileFromOffset()/this.MappedFileSize))。
获取存储文件最小偏移量,从这里也可以看出,并不是直接返回0,而是返回Mapped-File的getFileFormOffset()。
获取存储文件的最大偏移量。返回最后一个MappedFile文件的fileFromOffset加上MappedFile文件当前的写指针。
返回存储文件当前的写指针。返回最后一个文件的fileFromOffset加上当前写指针位置。
MappedFile的核心属性
- int OS_PAGE_SIZE:操作系统每页大小,默认4k。
- AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY:当前JVM实例中Mapped-File虚拟内存。
- AtomicInteger TOTAL_MAPPED_FILES:当前JVM实例中MappedFile对象个数。
- AtomicInteger wrotePosition:当前该文件的写指针,从0开始(内存映射文件中的写指针)
- AtomicInteger committedPosition:当前文件的提交指针,如果开启transientStore-PoolEnable,则数据会存储在TransientStorePool中,然后提交到内存映射ByteBuffer中,再刷写到磁盘。
- AtomicInteger flushedPosition:刷写到磁盘指针,该指针之前的数据持久化到磁盘中。
- int fileSize:文件大小。
- FileChannel fileChannel:文件通道。
- ByteBuffer writeBuffer:堆内存ByteBuffer,如果不为空,数据首先将存储在该Buffer中,然后提交到MappedFile对应的内存映射文件Buffer。transientStorePoolEnable为true时不为空。
- TransientStorePool transientStorePool:堆内存池,transientStorePoolEnable为true时启用。
- String fileName:文件名称。
- long fileFromOffset:该文件的初始偏移量。
- File file:物理文件。
- MappedByteBuffer mappedByteBuffer:物理文件对应的内存映射Buffer。
- volatile long storeTimestamp = 0:文件最后一次内容写入时间。
- boolean firstCreateInQueue:是否是MappedFileQueue队列中第一个文件。
MappedFile的初始化
初始化fileFromOffset为文件名,也就是文件名代表该文件的起始偏移量,通过RandomAccessFile创建读写文件通道,并将文件内容使用NIO的内存映射Buffer将文件映射到内存中。
根据是否开启transientStorePoolEnable存在两种初始化情况。transientStorePoolEnable为true表示内容先存储在堆外内存,然后通过Commit线程将数据提交到内存映射Buffer中,再通过Flush线程将内存映射Buffer中的数据持久化到磁盘中。
如果transientStorePoolEnable为true(默认为false),则初始化MappedFile的writeBuffer,该buffer从transientStorePool.
transientStorePool机制
RocketMQ 的 TransientStorePool
是用于提升消息写入性能的临时内存池组件,主要针对 异步刷盘(ASYNC_FLUSH) 场景设计。其核心作用是通过 堆外内存(Direct Buffer)的预分配和复用,减少对操作系统页缓存(Page Cache)的依赖,从而在高并发写入场景下降低写入延迟、提升吞吐量
默认存储机制的问题
- 默认存储方式:
RocketMQ 的存储层默认使用 内存映射文件(MappedFile) ,通过MappedByteBuffer
将文件映射到虚拟内存,依赖操作系统的页缓存机制实现高效读写。 - 性能瓶颈:
在高并发写入场景下,消息写入会频繁触发操作系统的 页缓存管理(如缺页中断、脏页回刷),导致写入线程因等待刷盘而阻塞,影响整体吞吐量
TransientStorePool 的优化思路
-
堆外内存缓冲池:
- 预分配一定数量的 堆外内存(DirectByteBuffer) 形成内存池。
- 消息写入时,先写入堆外内存缓冲,再异步提交到文件通道(FileChannel),最后刷盘。利用com.sun.jna.Library类库将该批内存锁定,避免被置换到交换区,提高存储性能。
-
解耦写入与刷盘:
-
写入线程仅操作内存缓冲,避免直接与磁盘 I/O 交互,减少线程阻塞。
-
由后台线程(CommitLog 的
CommitRealTimeService
)负责将内存数据提交到文件系统。
-
同步刷盘(SYNC_FLUSH)场景下无法发挥优势,因为仍需等待刷盘完成。
如果Broker异常启动,在文件恢复过程中,RocketMQ会将最后一个有效文件中的所有消息重新转发到消息消费队列与索引文件,确保不丢失消息,但同时会带来消息重复的问题,纵观RocktMQ的整体设计思想,RocketMQ保证消息不丢失但不保证消息不会重复消费,故消息消费业务方需要实现消息消费的幂等设计。