持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第18天,点击查看活动详情
1 CommitLog
Pro发送消息到Broker后,Master-Broker会将消息写入磁盘的一个日志文件-CommitLog,顺序写入文件末尾,CommitLog包含各种不同类型的Topic对应的消息内容。
消息主体及元数据的存储主体,存储Pro写入的消息主体内容。
消息存放的物理文件,每台broker上的commitlog被本机所有的ConsumeQueue共享,不做任何区分。
CommitLog文件每个限定最大1GB,Master-Broker收到消息后就将内容追加到文件末尾,若一个CommitLog写满1G,就会创建一个新CommitLog文件。
1.1 命名
文件名长度20位,左边补零,剩余为起始偏移量,如:
00000000000000000000
代表第一个文件,起始偏移量为0,文件大小为1G=1073741824;
当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。
消息主要是顺序写入日志文件,当文件满了,写入下一个文件。文件默认位置如下,可通过配置文件修改:
${user.home} \store\${commitlog}\${fileName}
CommitLog的消息存储单元长度不固定,文件顺序写,随机读。消息的存储结构如下,按照编号顺序以及编号对应的内容依次存储。
2 ConsumeQueue
消息的逻辑队列,类似字典的目录,指定消息在物理文件commitLog的位置,包含该MessageQueue在CommitLog中的:
- 起始物理位置偏移量offset
- 消息实体内容的大小
- Message Tag的哈希值
消息保存在MessageQueue,CommitLog和MessageQueue啥关系?每个Topic,它在某Broker所在机器都有一些MessageQueue,每个MessageQueue又有很多ConsumeQueue文件,这些ConsumeQueue文件里存储一条消息对应在CommitLog文件中的offset偏移量。
假设order_topic在Broker集群共有4个MessageQueue:queue1、queue2、queue3、queue4,均匀分布在两个Master-Broker,Pro选择queue1这个MessageQueue发了条“消息A”,则:
- 首先M-Broker接收到消息A后,将其内容顺序写入自己机器的CommitLog文件末尾
- 然后,M-Broker将消息A在CommitLog文件中的物理位置offset,写入queue1对应ConsumeQueue文件末尾
ConsumeQueue中存储的不只是消息在CommitLog的offset,还包含消息长度、tag hashcode等信息,一条数据20字节,每个ConsumeQueue文件能保存30万条数据,所以每个ConsumeQueue文件约5.72MB。当一个ConsumeQueue类型的文件写满了,则写入下一个文件。
实际物理存储,ConsumeQueue对应每个Topic和QueuId下面的文件。
可在配置中指定consumequeue与commitlog存储的目录。每个topic下的每个queue都有一个对应的consumequeue文件,如:
${rocketmq.home}/store/consumequeue/${topicName}/${queueId}/${fileName}
Consume Queue文件组织:
- 根据
topic和queueId来组织文件,图中TopicA有两个队列0,1:TopicA、QueueId=0组成一个ConsumeQueue,TopicA、QueueId=1组成另一个ConsumeQueue - 按Con的
GroupName分组重试队列。若Con消费失败,消息将被发往重试队列,如图中的%RETRY%ConsumerGroupA - 按Con的
GroupName分组死信队列,若Con消费失败,并重试指定次数后,仍失败,则发往死信队列,如图中的%DLQ%ConsumerGroupA
死信队列(Dead Letter Queue)一般用于存放由于某种原因无法传递的消息,如处理失败或已过期的消息。
消息消费的逻辑队列,作为消费消息的索引,保存指定Topic下的队列消息在CommitLog中的:
- 起始物理偏移量offset
- 消息大小size
- 消息Tag的HashCode值
存储单元
Consume Queue的存储单元是一个20字节定长的二进制数据,顺序写、顺序读:
consume queue文件存储单元格式:
- CommitLog Offset:这条消息在Commit Log文件中的实际偏移量
- Size:存储中消息的大小
- Message Tag HashCode存储消息的Tag的哈希值:用于订阅时的消息过滤(订阅时若指定Tag,会根据HashCode快速查找到订阅的消息)
而IndexFile(索引文件)只是为消息查询提供一种通过K或时间区间查询消息的方法(这种通过IndexFile查找消息的方法,不影响发送与消费消息的主流程)。
实际物理存储,ConsumeQueue对应每个Topic和QueuId下面的文件。单个文件大小约5.72M,每个文件由30W条数据组成,每个文件默认大小为600万个字节,当一个ConsumeQueue类型的文件写满了,则写入下一个文件。
IndexFile 消息的索引文件
所有消息都存在CommitLog,若实现根据K查询消息,为解决这种业务需求,就有IndexFile。
为生成的索引文件提供访问服务,通过消息K查询消息真正的实体内容。
实际物理存储,文件名以创建时的时间戳命名,固定的单个IndexFile文件大小约为400M,一个IndexFile可保存2000W个索引。
若一个消息包含K值,则会使用IndexFile存储消息索引。
存放位置
${rocketmq.home}/store/index/indexFile(年月日时分秒等组成文件名)
Index文件组织方式
Header中存储的信息:
// 文件中第一个、最后一个索引对应的消息的存储时间
private static int beginTimestampIndex = 0;
private static int endTimestampIndex = 8;
// 第一个和最后一个索引对应消息的offset的最大和最小值
private static int beginPhyoffsetIndex = 16;
private static int endPhyoffsetIndex = 24;
private static int hashSlotcountIndex = 32;
// 文件中的索引个数
private static int indexCountIndex = 36;
private final ByteBuffer byteBuffer;
private final AtomicLong beginTimestamp = new AtomicLong(0);
private final AtomicLong endTimestamp = new AtomicLong(0);
private final AtomicLong beginPhyOffset = new AtomicLong(0);
private final AtomicLong endPhyOffset = new AtomicLong(0);
private final AtomicInteger hashSlotCount = new AtomicInteger(0);
private final AtomicInteger indexCount = new AtomicInteger(1);
slotTable+indexLinkedList可理解成java的HashMap。每当放一个新的消息的index,首先取MessageKey的hashCode,然后用hashCode对slot总数取模,得到应该放到哪个slot中,slot系统默认500W个。只要取hash就有hash冲突问题,IndexFile也使用散列法,和HashMap区别是,slot中放最新index的指针。因为一般查询时,肯定优先查最近消息。
每个slot中放的指针值:索引在indexFile中的偏移量,如上图,每个索引20字节,所以根据当前索引是这个文件中的第几个(偏移量),就能定位到索引位置。然后每个索引都保存跟它同一个slot的前一个索引的位置,以此类推形成一个链表的结构。
索引文件主要用于据K查询消息:
- 根据查询的 K 的 hashcode%slotNum 得到具体的槽的位置(slotNum 是一个索引文件里面包含的最大槽的数目,如图中所示 slotNum=5000000)
- 根据 slotValue(slot 位置对应的值)查找到索引项列表的最后一项(倒序排列,slotValue 总是指向最新的一个索引项)
- 遍历索引项列表返回查询时间范围内的结果集(默认一次最大返回的 32 条记录)
MappedFileQueue
对连续物理存储的抽象封装类,源码中可以通过消息存储的物理偏移量位置快速定位该offset所在MappedFile(具体物理存储位置的抽象)、创建、删除MappedFile等操作。
MappedFile
文件存储的直接内存映射业务抽象封装类,源码中通过操作该类,可以把消息字节写入PageCache缓存区(commit),或原子性地将消息持久化刷盘(flush)。
MessageQueue
存储消息的偏移量。读消息先读MessageQueue,根据偏移量到commit log读消息本身。
创建Topic的关键参数,如下图,可在RocketMQ可视化工作台创建名order_topic的Topic,指定包含4个MessageQueue:
Topic、MessageQueue、Broker的关系
Topic是消息的逻辑分类,消息保存在MessageQueue中,MessageQueue分布在Master-Broker上,Slave-Broker从Master-Broker同步数据。
MessageQueue是个数据分片机制。如order_topic共1万条消息,可大致认为每个MessageQueue保存2500条消息。但并非绝对,需根据Pro写消息的策略。暂认为消息是在MessageQueue平均分配,然后MessageQueue也可能平均分布在Master-Broker: