RocketMQ主要存储文件包含消息文件(commitLog)、消息消费队列文件(consume-queue)、Hash索引文件(IndexFile)、检测点文件(checkpoint)、abort(关闭异常文件)、单个消息存储文件、消息消费队列文件、Hash索引文件长度固定以便使用内存映射机制进行文件的读写操作。RocketMQ组织文件以文件的起始偏移量来命名文件,这样根据偏移量能快速的定位到真实的物理文件。RocketMQ基于内存映射文件机制提供了同步刷盘与异步刷盘两种机制,异步刷盘是指在消息存储时先追加到内存映射文件,然后启动专门的刷盘线程定时将内存中的数据刷盘到磁盘。
commitLog,消息存储文件,RocketMQ为了保证消息发送的高吞吐量,采用单一文件存储所有主题的消息,保证消息存储是完全的顺序写,但这也给文件读取带来了不便,RocketMQ为了方便消息消费构建了消息消费队列文件,基于主题与队列进行组织,同时RocketMQ为消息实现了Hash索引,可以为消息设置索引键,根据索引能够快速从commitLog文件中检索出消息。
当消息到达CommitLog文件后,会通过RequestMessageService线程接近实时的将消息转发给消息消费队列文件好索引文件,为了安全起见,RocketMQ引入了abort文件,记录Broker的停机是正常关闭还是异常关闭,在重启Broker时为了保证CommitLog文件,消息消费队列文件与Hash索引文件的正确性,分别采取不同的策略来恢复文件。
RocketMQ不会永久存储消息文件、消息消费队列文件,而是启用文件过期机制,并在磁盘空间不足或默认在凌晨四点删除过期文件,文件默认保存72小时并且在删除文件时并不会判断该消息文件上的消息是否被消费。
一、消息发送存储流程:
RocketMQ将所有主题的消息存储在同一个文件中,确保消息发送时顺序写文件,尽最大的能力确保消息发送的高性能与高吞吐量。因为基于消费主题进行订阅消费,所以为了提高消费效率,引入了消息消费队列文件,每个消息主题包含多个消息消费队列,每一个消息队列有一个消息文件。IndexFile索引文件为了加速消息的检索性能,根据消息的属性快速的从CommitLog中检索出来,
1、如果当前Broker停止工作或者Broker为Slave角色或当前Rocket不支持写入则拒绝消息写入,如果消息主题长度超过256个字符,消息属性长度超过65536个字符将拒绝该消息写入。
2、如果消息的延迟级别大于0,将消息的原主题名称与原消息队列ID存入消息属性中,用延迟消费主题SCHEDULE_TOPIC
、消息队列ID更新原先消息的主题与队列,这是并发消息消费重试关键的一步。
3、获取当前可以写入的CommitLog文件,每一个文件默认 1 G,一个文件写满后再创建另一个,MappedFileQueue可以看做是***/store/commitLog文件夹,而MappedFile则对应该文件下一个个的文件。
4、在写入CommitLog之前,先申请pushMessageLock,这说明将消息写入到CommitLog文件中是串行的。
5、设置消息的存储时间,如果马佩佩的File为空,表明上图中的目录下不存在任何文件,说明消息是第一次发送,用偏移量0创建第一个Commit文件,如上图,如果创建失败,抛出CREATE_MAPPEDFILE_FAILED,很有可能是磁盘空间不足或者权限不够。
6、将消息追加到MappedFile中,首先先获取MappedFile当前写指针,如果currentPos大于或者等于文件大小则表明文件已写满,抛出异常,如果currentPos小于文件大小,通过slice()方法创建一个与MappedFile的共享内存区并设置position为当前指针。
7、创建全局唯一消息ID,消息ID有16字节,消息ID。
8、获取该消息在消息队列的偏移量。CommitLog中保存了当前所有消息队列的当前待写入偏移量。
9、根据消息体的长度,主题的长度,属性的长度,结合消息存储格式计算消息的总长度。
10、如果消息长度+最小文件剩余长度 大于>commitLog文件的剩余空间返回响应状态,然后Broker重新建一个新的CommitLog文件。
11、将消息内容存储到ByteBuffer中,然后创建AppendMessageResult。这里只是将消息存储在MappedFile对应的内存映射Buffer上,并没有刷写到磁盘。
12、更新消息队列逻辑偏移量。
13、处理完消息追加逻辑后将释放putMessageLock锁。
14、DefaultAppendMessageCallBack#doAppend只是将消息追加在内存中,需要根据是同步刷盘还是异步刷盘方式,将内存中的数据持久化到磁盘。
二、存储文件组织与内存映射:
RocketMQ通过使用内存映射文件来提高IO访问性能,无论是commitLog、consumerQueue、还是IndexFile,单个文件都被设计为固定长度,如果一个文件写满以后在创建一个新文件,文件名就为该文件第一条消息对应的全局物理偏移量。
1、MappedFileQueue映射文件队列:
MappedFileQueue是MappedFile的管理容器,MappedFileQueue是对应存储目录的封装。
public class MappedFileQueue {
private static final int DELETE_FILES_BATCH_MAX = 10;
//存储目录
private final String storePath;
//单个文件的存储大小
private final int mappedFileSize;
//MappedFile 文件集合
private final CopyOnWriteArrayList mappedFiles = new CopyOnWriteArrayList();
//创建MappedFile文件服务类
private final AllocateMappedFileService allocateMappedFileService;
//当前刷盘指针,表示该指针之前的所有数据全部持久化到磁盘
private long flushedWhere = 0;
//当前提交数据指针,内存中ByteBuffer当前的指针,该值大于等于flushedWhere。
private long committedWhere = 0;
private volatile long storeTimestamp = 0;
}
2、MappedFile内存映射文件:
public class MappedFile extends ReferenceResource {
//操作系统每页大小 默认 4K
public static final int OS_PAGE_SIZE = 1024 * 4;
protected static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME);
//当前JVM实例中的MappedFile的虚拟内存
private static final AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY = new AtomicLong(0);
//当前JVM实例中MappedFile对象个数。
private static final AtomicInteger TOTAL_MAPPED_FILES = new AtomicInteger(0);
//当前该文件的写指针[从0开始内存映射文件的写指针]
protected final AtomicInteger wrotePosition = new AtomicInteger(0);
//ADD BY ChenYang
//当前文件的提交指针,如果开启【transitStorePoolEnable】则数据会存储在
//TransitionStorePool中,然后提交到内存映射ByteBuffer中,在刷写到磁盘。
protected final AtomicInteger committedPosition = new AtomicInteger(0);
//刷写到磁盘指针,该指针之前的数据持久化到磁盘中
private final AtomicInteger flushedPosition = new AtomicInteger(0);
//文件大小
protected int fileSize;
//文件通道
protected FileChannel fileChannel;
/**
* Message will put to here first, and then reput to FileChannel if writeBuffer is not null.
*/
//堆内存ByteBuffer如果不为空数据首先存储在该Buffer中,然后提交到MappedFile
//对应的内存映射文件Buffer,transitStorePoolEnable为true时不为空。
protected ByteBuffer writeBuffer = null;
//堆内存池
protected TransientStorePool transientStorePool = null;
//文件名称
private String fileName;
//该文件的初始偏移量
private long fileFromOffset;
//物理文件
private File file;
//物理文件对应的内存映射Buffer
private MappedByteBuffer mappedByteBuffer;
//文件最后一次内容写入时间
private volatile long storeTimestamp = 0;
//是否是MappedFileQueue队列中第一个文件
private boolean firstCreateInQueue = false;
}
三、存储文件:
1、CommitLog文件:
RocketMQ存储文件夹:
2、ConsumeQueue文件:
第一级目录为消息主题,第二级目录为主题的消息队列。
3、Index索引文件
4、checkPoint文件。
四、消息队列与索引文件恢复:
由于RocketMQ存储首先将消息全量存储在CommitLog文件中,然后异步生成转发任务更新ConsumeQueue、Index文件。如果消息成功存储到CommitLog文件中,转发任务未成功执行,此时消息服务器基于某个原因宕机,导致CommitLog、ConsumeQueue、IndexFile文件不一致。
1、Broker正常停止文件恢复:
2、Broker异常停止文件恢复。
五、文件刷盘机制:
1、Broker同步刷盘:
指的是消息追加到内存映射文件的内存中后,立即将数据从内存刷写到磁盘文件,由CommitLog的handDiskFlush方法实现。
2、Broker异步刷盘:
五、过期文件删除机制:
1、三个配置属性的含义:
2、继续执行删除文件操作: