前言
消息存储是消息队列中最基础也是最重要的一部分服务,将会对rocketmq存储的整体架构剖析,分析存储、索引服务如何协同工作的?
存储整体架构
消息存储架构图中主要有下面三个跟消息存储相关的文件构成。
(1) CommitLog:消息主体以及元数据的存储主体,存储Producer端写入的消息主体内容,消息内容不是定长的。单个文件大小默认1G ,文件名长度为20位,左边补零,剩余为起始偏移量,比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文件;
(2) ConsumeQueue:消息消费队列,引入的目的主要是提高消息消费的性能,由于RocketMQ是基于主题topic的订阅模式,消息消费是针对主题进行的,如果要遍历commitlog文件中根据topic检索消息是非常低效的。Consumer即可根据ConsumeQueue来查找待消费的消息。其中,ConsumeQueue(逻辑消费队列)作为消费消息的索引,保存了指定Topic下的队列消息在CommitLog中的起始物理偏移量offset,消息大小size和消息Tag的HashCode值。consumequeue文件可以看成是基于topic的commitlog索引文件,故consumequeue文件夹的组织方式如下:topic/queue/file三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。同样consumequeue文件采取定长设计,每一个条目共20个字节,分别为8字节的commitlog物理偏移量、4字节的消息长度、8字节tag hashcode,单个文件由30W个条目组成,可以像数组一样随机访问每一个条目,每个ConsumeQueue文件大小约5.72M;
(3) IndexFile:IndexFile(索引文件)提供了一种可以通过key或时间区间来查询消息的方法。Index文件的存储位置是:$HOME \store\index\${fileName},文件名fileName是以创建时的时间戳命名的,固定的单个IndexFile文件大小约为400M,一个IndexFile可以保存 2000W个索引,IndexFile的底层存储设计为在文件系统中实现HashMap结构,故rocketmq的索引文件其底层实现为hash索引。
Commitlog存储格式
Commitlog存储消息元数据,消息长度取决于消息内容body长度、Topic长度、拓展字段长度。Commitlog建立在MappedFile抽象了FileChannel以及MappedByteBuffer持久化能力的基础上,支持消息查询、根据offset查询等能力,并且利用AbstactMessageCallback拓展消息元数据持久化方式,使得持久化方式与MappedFile解耦。
// org.apache.rocketmq.store.CommitLog#calMsgLengthprotected static int calMsgLength(int bodyLength, int topicLength, int propertiesLength) { final int msgLen = 4 //TOTALSIZE + 4 //MAGICCODE + 4 //BODYCRC + 4 //QUEUEID + 4 //FLAG + 8 //QUEUEOFFSET + 8 //PHYSICALOFFSET + 4 //SYSFLAG + 8 //BORNTIMESTAMP + 8 //BORNHOST + 8 //STORETIMESTAMP + 8 //STOREHOSTADDRESS + 4 //RECONSUMETIMES + 8 //Prepared Transaction Offset + 4 + (bodyLength > 0 ? bodyLength : 0) //BODY + 1 + topicLength //TOPIC + 2 + (propertiesLength > 0 ? propertiesLength : 0) //propertiesLength + 0; return msgLen; }
在commitlog中,{ msg * * * }形式存储,如果新增msg消息导致文件不足以存放,那么设置BLANK_MAGIC_CODE说明是blank片段,并且记录下当前的时间戳;反之,如果足够存放,那么就会设置MESSAGE_MAGIC_CODE说明是msg片段。
如何更新消费队列或者索引文件?
消息存储服务启动时会加载消费队列目录,根据consumqueue最大的物理偏移计算reputOffset,如果迁移broker数据时未迁移consumqueue或者consumqueue目录磁盘损坏等情况,会重置 reputOffset ,也即会重建消费队列以及索引文件indexfile。reputMessageService 根据 reputOffset位置开始 对 消息元数据 解析生成 dispatchRequest已注册的CommitLogDispatcherBuildConsumeQueue以及 CommitLogDispatcherBuildIndex 建立consumqueue、index。
// org.apache.rocketmq.store.DefaultMessageStore// 注册解析器this.dispatcherList.addLast(new CommitLogDispatcherBuildConsumeQueue());this.dispatcherList.addLast(new CommitLogDispatcherBuildIndex());// 调度解析public void doDispatch(DispatchRequest req) { for (CommitLogDispatcher dispatcher : this.dispatcherList) { dispatcher.dispatch(req); } }
IndexFile存储格式
IndexFile索引文件为用户提供通过“按照Message Key查询消息”的消息索引查询服务,IndexFile文件的存储位置是:$HOME\store\index\${fileName},文件名fileName是以创建时的时间戳命名的,文件大小是固定的,等于40+500W\*4+2000W\*20= 420000040个字节大小。如果消息的properties中设置了UNIQ_KEY这个属性,就用 topic + “#” + UNIQ_KEY的value作为 key 来做写入操作。如果消息设置了KEYS属性(多个KEY以空格分隔),也会用 topic + “#” + KEY 来做索引。
索引数据包含了Key Hash/CommitLog Offset/Timestamp/NextIndex offset 这四个字段,一共20 Byte。NextIndex offset 即前面读出来的 slotValue,如果有 hash冲突,就可以用这个字段将所有冲突的索引用链表的方式串起来了。Timestamp记录的是消息storeTimestamp之间的差,并不是一个绝对的时间。整个Index File的结构如图,40 Byte 的Header用于保存一些总的统计信息,4\*500W的 Slot Table并不保存真正的索引数据,而是保存每个槽位对应的单向链表的头。20\*2000W 是真正的索引数据,即一个 Index File 可以保存 2000W个索引。
ConsumeQueue存储格式
consumequeue 组织方式采用topic/queue/file,可以在consumer针对主题消费时提高消费速度,而消费的进度会持久化在/store/config/consumerOffset.json 。
文件存储格式定长设计,8字节的commitlog物理偏移量、4字节的消息长度、8字节tag hashcode,可以快速读取topic下所有的offset,无须遍历commitlog再去筛选数据,提高消费的能力。
后续
本文分析了rocketmq 元数据文件、索引文件的存储格式以及这种混合存储的优势可以提高消费能力、按主题消费等,而这些都建立在通用的存储能力MappedFile 之上,后续会针对pagecache的原理进行剖析,可以持续关注一波!
你的关注和转发就是对我最大的支持!
近期热文:
异地多活场景下的数据同步之道
Linux 技巧:让进程在后台运行更可靠的几种方法
聊聊代码规范