RocketMQ存储层原理

元数据管理

1.1 topic、订阅组group、消费进度consumerOffset
1.2 多个配置文件config,故障恢复的存储Checkpoint和Filelock
1.3 记录主备备份Epoch/SN

元数据管理与优化

topic对应多个逻辑分区(Partition),称为队列(MessageQueue)
001 rocketmq topic逻辑分区.jpg 其中ConsumeOffset用于管理订阅组消费进度(如Map<String/* topicName@groupId */, Map<Integer /QueueId/, Long /Offset/>>)
RocketMQ采用定时任务对上面Map做checkPoint检查(检查周期5秒 - 所以当服务端主备切换或者正常发布时,都会有秒级的消息重复)

消息数据管理

存储核心是极致优化顺序写盘(append only将新消息追加文件末尾)。实现如下

  1. RocketMQ通过MappedByteBuffer实现 mmap 映射虚拟地址关联减少 数据区在缓存区来回拷贝

1.1 限制:mmap 文件不能太大(RocketMQ 1G,通过链表串联成逻辑队列 MappedFileQueue)
002 MappedFileQueue结构图.jpg

单挑消息存储格式

RocketMQ采用复杂存储编码将对象序列化,并写入文件(存储格式包括索引队列编号、位置)

  1. 单消息一般占用大小 = 消息元数据(91B+部分属性) + 消息payload(>2k) > 2k

003 单条消息存储格式.jpg

多条消息连续写

特性:顺序写+写快点
004 消息写入.jpg

独占锁实现顺序写

怎么确保单机存储写commitLog顺序性

  1. 加独占锁(目前采用ReentrantLock 或 SpinLock)

1.1 ReentrantLock 或 SpinLock使用时机

1. ReentrantLock底层AQS抢不到会休眠,而SpinLock抢不到会CPU忙等待
2. ReentrantLock适合**同步持久化**,等待较久的。而SpinLock由于会一直抢占,适合等待加锁时间段的**异步持久化**
  1. 获取锁之后处理

2.1 预计算索引的位置ConsumeQueueOffset(需保证严格递增)
2.2 计算commitLog存储位置,physicalOffset 物理偏移量,找到文件位置
2.3 记录存储时间戳,保证消息投递时间顺序性

成组提交与可见性

大部分系统将操作日志缓存在内存中,然后满足 超过一定大小或超过一定时间 就会自动刷盘持久化。但异常重启会丢失部分数据
但消息系统比较特殊,针对金融场景下要求数据异常丢失少、延迟低。RocketMQ可配置为单主异步持久化(单master,并将数据同步到slave节点)

扫描二维码关注公众号,回复: 14353881 查看本文章
1. RoekctMQ单主异步持久化存在的问题:宕机master会丢失部分消息,并且消费者可能已经收到消息数据,
当这种模式宕机恢复时会重放到某个节点,但只能读取新写入的消息,读取不到之前消费过的消息
2. 对应1解决方案:过半slave副本确认后RocketMQ消息才可被消费者可见,但这样吞吐量会降低

持久化机制

持久化主要看时机、同步还是异步。RocketMQ提供三种模式

1. 同步持久化(GroupCommitService)
2. 异步持久化(FlushRealTimeService)
- 是否开启TransientStorePool缓存
  1. 同步持久化

1.1 处理过程:写入线程将消息转给存储线程,10ms检查一次将page cache数据刷入磁盘
1.2 宕机或断电处理:未落盘数据不影响生产者(非oneway调用),发送者未收到成功响应会重试
2. 异步持久化
有两种模式 固定频率、非固定频率
1.1 固定频率-处理过程:500ms(可配置)flush一次(不足16k则本次不处理,下次处理,超过10s直接执行flush)
1.2 非固定频率-处理过程:新消息过滤都会换新刷盘(数据量大的时候性能不好,适合数据量小)
005 消息持久化同步异步模式.jpg

读写分离

RocketMQ可配置异步持久化并开启缓冲池(默认初始化 5 块(参数 transientStorePoolSize 决定)堆外内存(DirectByteBuffer)循环利用)
具体是开启了缓存后。写入通过DirectByteBuffer,然后再异步批量写入page cache;读取通过page cache直接获取数据
缺点:数据可靠性降低,重启进程就可能会丢数据
006 读写分离.jpg

宕机与故障恢复

磁盘没问题的话,MQ重启即可从commitLog、consumeQueue加到数据到内存,然后HA协商,再初始化netty server提供服务

避免存储抖动

快速失败

  1. 为什么要快速失败:消息被服务端IO线程读取后进入阻塞队列中,但Broker节点会受GC、IO抖动等造成存储写失败,从而导致请求排队堆积,进而OOM
  2. 怎么解决:线程检查队列,剔除超过200ms消息并立即给客户端返回失败响应,然后客户端会重试在其他副本组
  3. 其他快速失败机制:写入超过一秒快速失败

007 写入快速失败.jpg

预分配与文件预热

RocketMQ需要确保commitLog写满之后快速切换到新的,所以会开启线程异步创建新文件并内存锁定(还有一个额外文件预热开关)
优点:文件预热生成,降低写IO。另外RocketMQ定位于业务级消息这种小数据块/高频率的 IO 传输,选用更低的延迟 mmap 更合适

冷数据读取

冷数据读取涉及两种

  1. 副本数据转存拷贝(第一种):在RocketMQ低版本,需要大量拷贝commitLog情况下(备磁盘宕机或上线), 主用DMA拷贝通过网络复制给备机会造成大量缺页中断阻塞io线程,进而影响Netty处理新情求。 在实现上可以用第二个端口处理,但本质上没解决阻塞问题(只能从其他备拷贝 或 madvice建议os读取避免影响主的消息写入)
  2. 消费者消费几小时前数据(第二种):让备机分摊压力 或 从转存后的二级介质读取

索引数据管理

以下重温一下几个概念:

- MessageQueue: 消息逻辑队列
-- MessageQueue = 多个连续 ConsumeQueue 索引 + CommitLog 文件
-- 仅保存一个topic分区索引(物理偏移量+消息长度+tag hashcode    共20B大小)
- ConsumeQueue: 消息对应的物理索引文件
-- 多种存储策略可选并可混合存储

客户端pull请求拉取消息流程

1. 根据 Tag 的 Hash 值查询 ConsumeQueue 文件(由 physicOffset + size + Tag HashCode 组成)
2. 根据 ConsumeQueue 拿到 physicOffset + size
3. 根据 physicOffset 查询 CommitLog 文件(上文的 MappedFileQueue ) 获得消息

008 commit与comsume.jpg

参考链接:mp.weixin.qq.com/s/PzDO-UCLz…

猜你喜欢

转载自juejin.im/post/7115737131514871844