RocketMQ存储路径为${ROCKET_HOME}/store,如图所示:
- commitlog:消息存储目录。
- config:运行期间一些配置信息,主要包括下列信息。
- consumerFilter.json:主题消息过滤信息。
- consumerOffset.json:集群消费模式消息消费进度。
- delayOffset.json:延时消息队列拉取进度。
- subscriptionGroup.json:消息消费组配置信息。topics.json:topic配置属性。
- consumequeue:消息消费队列存储目录。
- index:消息索引文件存储目录。
- abort:如果存在abort文件说明Broker非正常关闭,该文件默认启动时创建,正常退出之前删除.
- checkpoint:文件检测点,存储commitlog文件最后一次刷盘时间戳、consumequeue最后一次刷盘时间、index索引文件最后一次刷盘时间戳。
CommitLog
ConsumeQueue
RocketMQ基于主题订阅模式实现消息消费,消费者关心的是一个主题下的所有消息,但由于同一主题的消息不连续地存储在commitlog文件中,试想一下如果消息消费者直接从消息存储文件(commitlog)中去遍历查找订阅主题下的消息,效率将极其低下,RocketMQ为了适应消息消费的检索需求,设计了消息消费队列文件(Consumequeue),该文件可以看成是Commitlog关于消息消费的“索引”文件,consumequeue的第一级目录为消息主题,第二级目录为主题的消息队列.
为了加速ConsumeQueue消息条目的检索速度与节省磁盘空间,每一个Consumequeue条目不会存储消息的全量信息.
单个ConsumeQueue文件中默认包含30万个条目,单个文件的长度为30w×20字节,单个ConsumeQueue文件可以看出是一个ConsumeQueue条目的数组,其下标为Consume-Queue的逻辑偏移量,消息消费进度存储的偏移量即逻辑偏移量。ConsumeQueue即为Commitlog文件的索引文件,其构建机制是当消息到达Commitlog文件后,由专门的线程产生消息转发任务,从而构建消息消费队列文件与下文提到的索引文件。
Index索引文件
消息消费队列是RocketMQ专门为消息订阅构建的索引文件,提高根据主题与消息队列检索消息的速度,另外RocketMQ引入了Hash索引机制为消息建立索引,HashMap的设计包含两个基本点:Hash槽与Hash冲突的链表结构。
checkpoint文件
checkpoint的作用是记录Comitlog、ConsumeQueue、Index文件的刷盘时间点,文件固定长度为4k,其中只用该文件的前面24个字节,其存储格式如下:
physicMsgTimestamp:commitlog文件刷盘时间点。 logicsMsgTimestamp:消息消费队列文件刷盘时间点。 indexMsgTimestamp:索引文件刷盘时间点。
实时更新消息消费队列与索引文件
消息消费队列文件、消息属性索引文件都是基于CommitLog文件构建的,当消息生产者提交的消息存储在Commitlog文件中,ConsumeQueue、IndexFile需要及时更新,否则消息无法及时被消费,根据消息属性查找消息也会出现较大延迟。RocketMQ通过开启一个线程ReputMessageServcie来准实时转发CommitLog文件更新事件,相应的任务处理器根据转发的消息及时更新ConsumeQueue、IndexFile文件。
Broker服务器在启动时会启动ReputMessageService线程,并初始化一个非常关键的参数reputFfomOffset,该参数的含义是ReputMessageService从哪个物理偏移量开始转发消息给ConsumeQueue和IndexFile。如果允许重复转发,reputFromOffset设置为CommitLog的提交指针;如果不允许重复转发,reputFromOffset设置为Commitlog的内存中最大偏移量。
ReputMessageService线程每执行一次任务推送休息1毫秒就继续尝试推送消息到消息消费队列和索引文件。
消息消费队列转发任务实现类为:CommitLogDispatcherBuildConsumeQueue,内部最终将调用putMessagePositionInfo方法,依次将消息偏移量、消息长度、tag hashcode写入到ByteBuffer中,并根据consumeQueueOffset计算ConumeQueue中的物理地址,将内容追加到ConsumeQueue的内存映射文件中(本操作只追击并不刷盘),ConumeQueue的刷盘方式固定为异步刷盘模式。
消息队列与索引文件恢复(数据一致性)
由于RocketMQ存储首先将消息全量存储在Commitlog文件中,然后异步生成转发任务更新ConsumeQueue、Index文件。如果消息成功存储到Commitlog文件中,转发任务未成功执行,此时消息服务器Broker由于某个原因宕机,导致Commitlog、ConsumeQueue、IndexFile文件数据不一致。如果不加以人工修复的话,会有一部分消息即便在Commitlog文件中存在,但由于并没有转发到Consumequeue,这部分消息将永远不会被消费者消费。那RocketMQ是如何使Commitlog、消息消费队列(ConsumeQueue)达到最终一致性的呢?
Step1:判断上一次退出是否正常。其实现机制是Broker在启动时创建${ROCKET_HOME}/store/abort文件,在退出时通过注册JVM钩子函数删除abort文件。如果下一次启动时存在abort文件。说明Broker是异常退出的,Commitlog与Consumequeue数据有可能不一致,需要进行修复。
Step2:加载Commitlog文件,加载${ROCKET_HOME}/store/commitlog目录下所有文件并按照文件名排序。如果文件大小与配置文件的单个文件大小不一致,将忽略该目录下所有文件,然后创建MappedFile对象。
Step3:加载消息消费队列,调用DefaultMessageStore#loadConsumeQueue,其思路与CommitLog大体一致,遍历消息消费队列根目录,获取该Broker存储的所有主题,然后遍历每个主题目录,获取该主题下的所有消息消费队列,然后分别加载每个消息消费队列下的文件,构建ConsumeQueue对象,主要初始化ConsumeQueue的topic、queueId、storePath、mappedFileSize属性。
Step5:加载存储检测点,检测点主要记录commitlog文件、Consumequeue文件、Index索引文件的刷盘点。
Step6:加载索引文件,如果上次异常退出,而且索引文件上次刷盘时间小于该索引文件最大的消息时间戳该文件将立即销毁。
Step7:根据Broker是否是正常停止执行不同的恢复策略,分为异常停止、正常停止。
Step8:恢复ConsumeQueue文件后,将在CommitLog实例中保存每个消息消费队列当前的存储逻辑偏移量,这也是消息中不仅存储主题、消息队列ID还存储了消息队列偏移量的关键所在。