RocketMQ

1,137 阅读8分钟

一、文件读写IO

**1、HeapByteBuffer、DirectByteBuffer、**MappedByteBuffer

在JVM的垃圾回收器里,除了CMS,都是需要移动对象的;如果要把一个Java里的 byte[] 对象的引用传给native代码,让native代码直接访问数组的内容的话,就必须要保证native代码在访问的时候这个 byte[] 对象不能被移动。

在传输数据时(磁盘IO传输和Socket传输都属于fd),如果传入HeapByteBuffer,首先会把HeapByteBuffer 背后的 byte[] 的内容拷贝到一个 DirectByteBuffer,然后再发送DirectByteBuffer中的数据。如果直接使用DirectByteBuffer的话,就会少了一次HeapByteBuffer->DirectByteBuffer的拷贝。

但是使用DirectByteBuffer也是有代价的,DirectByteBuffer比HeapByteBuffer的创建开销更大,所以如果要使用DirectByteBuffer的话最好还是复用,避免过多的创建。

RocketMQ内部实现基于nio提供的java.nio.MappedByteBuffer,基于FileChannel的map方法得到mmap的缓冲区,会作为page cache的一部分来用

2、DirectByteBuffer和pagecache(MappedByteBuffer)读写分离消息

DirectByteBuffer(堆外内存)+PageCache的两层架构方式,这样子可以实现读写消息分离,写入消息时候写到的是DirectByteBuffer——堆外内存中,读消息走的是PageCache(对于,DirectByteBuffer是两步刷盘,一步是刷到PageCache,还有一步是刷到磁盘文件中),带来的好处就是,避免了内存操作的很多容易堵的地方,降低了时延,比如说缺页中断降低,内存加锁,污染页的回写。

3、Page Cache机制

页面缓存(Page Cache)是Linux内核中针对文件I/O的一项优化,Linux从内存中划出了一块区域来缓存文件页,如果要访问外部磁盘上的文件页,首先将这些页面拷贝到内存中,再进行读写。由于硬件结构限制,磁盘的I/O速度比内存慢很多,因此使用Page cache能够大大加速文件的读写速度。

在page cache占据大量内存前,使用linux内核中提供的posix_fadvise等缓存管理方法,主动释放掉无用的page cache,来缓解内存压力。

二、文件存储结构

消息的主要存储文件包括 CommitLog 文件、ConsumeQueue 文件、IndexFile 文件

所有 topic 的消息存储在 commitlog 文件中,commitlog 默认按 1G 分段,文件名按物理偏移量命名。而索引信息保存在 consumequeue/topic/queue 目录下,一个 entry 固定 20 字节,分别为 8 字节的 commitlog 物理偏移量、4 字节的消息长度、8 字节 tag hashcode。

broker 为消息的 UNIQ_KEY 和 topic + "#" + key 建立索引,index 文件的结构本质上是一个 hashmap,rocketMQ 写完 commitLog 后,写 consumeQueue 和 indexFile 是一个异步的过程

1、逻辑结构

2、物理结构

rocketmq

|--store

|-commitlog

| |-00000000000000000000

| |-00000000001073741824

|-config

| |-consumerFilter.json

| |-consumerOffset.json

| |-delayOffset.json

| |-subscriptionGroup.json

| |-topics.json

|-consumequeue

| |-SCHEDULE_TOPIX_XXX

| |-topicA

| |-topicB

| |-0

| |-1

| |-2

| |-3

| |-00000000000000000000

| |-00000000001073741824

|-index

| |-00000000000000000000

| |-00000000001073741824

|-abort

|-checkpoint

CommitLog 是消息存储的物理文件,所有消息主题的消息都存储在 CommitLog 文件中,每个 Broker 上的 CommitLog 被当前机器上的所有 ConsumeQueue 共享。CommitLog 中的文件默认大小为 1G, 当一个文件写满以后,会生成一个新的 CommitLog 文件。

ConsumeQueue 是消息消费的逻辑队列,消息达到 CommitLog 文件后将被异步转发到消息消费队列,供消息消费者消费,这里面包含 MessageQueue 在 CommitLog 中的物理位置偏移量 Offset,消息实体内容的大小和 Message Tag 的 hash 值,每个文件默认大小约为 600W 个字节,如果文件满了后会也会生成一个新的文件。

IndexFile 是消息索引文件,Index 索引文件提供了对 CommitLog 进行数据检索,提供了一种通过 key 或者时间区间来查找 CommitLog 中的消息的方法。在物理存储中,文件名是以创建的时间戳明明,固定的单个 IndexFile 大小大概为 400M,一个 IndexFile 可以保存 2000W 个索引

3、commit文件存储时序图

1)同步刷盘每次发送消息,消息都直接存储在 MapFile 的 mappdByteBuffer,然后直接调用 force() 方法刷写到磁盘,等到 force 刷盘成功后,再返回给调用方(GroupCommitRequest#waitForFlush)就是其同步调用的实现。

2)异步刷盘分为两种情况,是否开启堆外内存缓存池,具体配置参数:MessageStoreConfig#transientStorePoolEnable。

transientStorePoolEnable = true
消息在追加时,先放入到 writeBuffer 中,然后定时 commit 到 FileChannel,然后定时flush。

transientStorePoolEnable=false(默认)
消息追加时,直接存入 MappedByteBuffer(pageCache) 中,然后定时 flush。

三、队列文件格式

commitLog rocketmq存储消息的文件

consumeQueue 存储的是commitLog的索引,代表的是一个单独的队列

messageQueue 消息的逻辑队列,messageQueue会为每一个来消费的consumeGroup保存一个 offset,记录他的consumeGroup在这个consumeQueue上消费到一个位置,这样就可以让每个人都以自己的offset来消费,但是队列都是相同的。

1、commitlog

字段简称

字段大小(字节)

字段含义

msgSize

4

代表这个消息的大小

magiccode

4

MAGICCODE = daa320a7

body crc

4

消息体BODY CRC 当broker重启recover时会校验

queueId

4

flag

4

queueoffset

8

这个值是个自增值不是真正的consume queue的偏移量,可以代表这个consumeQueue队列或者tranStateTable队列中消息的个数,若是非事务消息或者commit事务消息,可以通过这个值查找到consumeQueue中数据,QUEUEOFFSET * 20才是偏移地址;若是PREPARED或者Rollback事务,则可以通过该值从tranStateTable中查找数据

physicaloffset

8

代表消息在commitLog中的物理起始地址偏移量

sysflag

4

指明消息是事物事物状态等消息特征,二进制为四个字节从右往左数:当4个字节均为0(值为0)时表示非事务消息;当第1个字节为1(值为1)时表示表示消息是压缩的(Compressed);当第2个字节为1(值为2)表示多消息(MultiTags);当第3个字节为1(值为4)时表示prepared消息;当第4个字节为1(值为8)时表示commit消息;当第3/4个字节均为1时(值为12)时表示rollback消息;当第3/4个字节均为0时表示非事务消息

bornTIMESTAMP

8

消息产生端(producer)的时间戳

bornHOST

8

消息产生端(producer)地址(address:port)

storeTIMESTAMP

8

消息在broker存储时间

storeHOSTADDRESS

8

消息存储到broker的地址(address:port)

reconsumetimes

8

消息被某个订阅组重新消费了几次(订阅组之间独立计数),因为重试消息发送到了topic名字为%retry%groupName的队列queueId=0的队列中去了,成功消费一次记录为0;

PreparedTransaction Offset

8

表示是prepared状态的事物消息

messagebodyLength

4

消息体大小值

messagebody

bodyLength

消息体内容

topicLength

1

topic名称内容大小

topic

topicLength

topic的内容值

propertiesLength

2

属性值大小

properties

propertiesLength

propertiesLength大小的属性数据

2、consumequeue

3、IndexFile

四、生产和消费

1、消息拉取和重平衡的时序逻辑

入口方法:DefaultMQPushConsumerImpl#start()方法

核心类:RebalanceService和PullMessageService

两个核心方法:

  • RebalanceImpl#rebalanceByTopic

① 获取topic主题下所有的队列

② 获取topic主题下group组下所有的消费者id

③ 排序队列和消费者id(同一个broker的会排在一起,然后按队列id排序),并委托strategy进行队列分配,保证同一个消费组内的消费者分配到的队列是不同的

④ 将分配给当前消费者的队列传入,进行队列的新增、删除操作(RebalanceImpl#updateProcessQueueTableInRebalance

⑤ 处理消费队列分配发生了变化后的逻辑(RebalanceImpl#messageQueueChanged方法

  • PullMessageService#pullMessage

获取处理队列ProcessQueue,调用pullAPI方法来拉取消息

2、生产端

DefaultMQProducer:默认实现。没有实现发送事务消息的方法TransactionMQProducer:继承自DefaultMQProducer,实现了发送事务消息的方法

Producer 的接口 MQProducer 中众多接口主要分三类: Oneway:发送消息后立即返回,不处理响应,不关心是否发送成功; Sync:发送消息后等待响应; Async:发送消息后立即返回,在提供的回调方法中处理响应。

入口方法:DefaultMQProducerImpl#start()方法

五、事务

RocketMQ 的事务消息也可以被认为是一个两阶段提交,简单的说就是在事务开始的时候会先发送一个半消息给 Broker。

半消息的意思就是这个消息此时对 Consumer 是不可见的,而且也不是存在真正要发送的队列中,而是一个特殊队列。

发送完半消息之后再执行本地事务,再根据本地事务的执行结果来决定是向 Broker 发送提交消息,还是发送回滚消息。

1、生产者发送事务消息给 broker时序

2、broker发送消息给生产者询问本地事务是否执行成功,生产者告知broker本地事务执行结果