rocketmq-各模块-实现原理(简化的实现思想构建高效的系统)

设计理念

  • 追求简单和速度
  1. NameServer简化,减少复杂度,提高速度;
  2. 高效的io存储,
  3. 简化系统,不保证消息只被消费一次,需要消费者自己做幂等;

核心模块

NameServer

NameServer采用了极简设计,节点之间不进行数据同步,因为即使NameServer不是强一致性,客户端也会通过重试避免故障。
整体工作流程如下:

  1. broker向所有NameServer注册路由元数据,每30秒更新一次
  2. NameServer每10秒发送一次心跳检测,如果超过120秒未收到反馈则移除该broker相关元数据
  3. NameServer收到broker发送的更新元数据,进行路由元数据和时间戳更新

消息发送

发送方式:同步、异步、oneway,其中oneway发送出去就返回,不能设置回调函数

整体流程

  1. 生产者创建并与MQClientInstance关联,后者负责同server之间的网络通信
  2. 从NameServer更新topic路由信息,并且每30秒定时刷新
  3. 如果没有该topic会请求默认topic的路由信息,在发送消息给broker的时候,broker会初始化该topic
  4. 发送前会检查消息体大小不能超过4M
  5. 选择可写的消息队列进行发送,同步消息发送失败可以进行重试,发送失败会更新broker的不可用延时,从而选择可用的broker进行发送,增加发送成功率
  6. 非oneway消息发送支持回调处理
  7. 支持批量消息发送

延时消息发送

  1. rocketmq只支持不同等级的延迟消息,不能支持精度的延时消息
  2. 生产者发送消息的delay-level>0,broker会更改主题为schedual_topic_xxx
  3. 然后根据延时等级,存入对应的ConsumeQueue
  4. 通过异步定时线程将消息全部取出,恢复消息的原主题,重新写入commitlog
  5. 实现粗略定时发送的目的

消息存储

整体工作流程如下:

  1. 存储消息的入口DefaultMessageStore::putMessage
  2. 校验:如果broker不支持写入,或者消息主题长度超过256个字符、属性长度超过65526个字符则拒绝写入
  3. 如果是延迟消息则更新消息的主题为延迟消息主题和队列为对应的延迟等级
  4. 获取当前可写入的commitlog,名称为当前偏移量,大小为1G
  5. 申请putMessageLock,串行化
  6. 如果第一次写入,则以偏移量0创建文件
  7. 写入MappedFile
  8. 生成消息id:4字节ip、4字节端口、8字节消息偏移量
  9. 获取消息的队列偏移量
  10. 计算消息的字节长度
  11. 如果消息+空闲字节数>commitlog空闲空间,则新建commitlog文件存储
  12. 更新消息逻辑队列的偏移量
  13. 释放putMessage锁
  14. 根据同步异步策略进行刷盘
关键技术
  • 内存映射技术MappedByteBuffer
    避免磁盘文件拷贝进入内核io缓冲区的开销
    缺点:
  1. 但是由于mmap映射产生的堆外内存,rockmq通过反射Cleaner进行释放
    由于受到虚拟内存限制,一般大小设置在1.5~2G以内
  2. 文件关闭不确定
  • PageCache
    操作系统的PageCache机制,在操作系统读取磁盘信息到内存页时,会提前预取部分数据到内存中,这也是rocketmq磁盘顺序写的优势。一切看起来都很美好,但是由于swap机制(内存不够的情况下,操作系统将磁盘空间的swap分区当成内存使用,其访问速度远低于内存)等,导致PageCache可能无法达到预期的效果,rocketmq通过一下多种优化手段来避免这个问题:
  1. 预分配MappedFile
    每次新生成commitlog会进行MappedFile预分配
  2. mlock
    通过调用本地c语言代码,开启mlock,尽量避免swap内存交换
  3. 文件预热
    在分配文件时会进行一次madvise(提前将磁盘读取到PageCache中)调用,将尽可能多的文件数据加载到内存中
封装模型

rocketmq封装为3个模型,统一提供commitlog/consumequeue/index使用,分别为:

  1. MappedFileQueue: 相当于文件目录,其管理了目录下所有的文件,例如commitlog目录、consumeQueue目录
  2. MappedFile: 相当于目录下具体的文件,例如一个commitlog文件或者一个index文件
存储文件
commitlog

文件名为20位,为起始消息的偏移量,文件大小默认1G。commitlog中的每条消息数据格式如下:

消息长度 消息
4字节 消息长度确定
ComsumeQueue

消息消费的逻辑队列, 单个文件大小约5.72M。ComsumeQueue中的每条消息格式如下:

commitlog offset size tag hashcode
8字节 4字节 8字节

默认包含30w条记录,因此30w * 20byte 约为5.72M。ConsumeQueue支持按照offset读取和按照时间戳开始读取2种

IndexFile

根据消息key生成的索引文件,大小约400M,可以生成2000w个消息的索引。索引文件的数据格式如下(可以自己计算下文件大小):

文件头 hash槽 index数据
40字节 500w个槽,每个槽4个字节 2000w个数据,每个数据20字节

文件头
数据格式为:

beginTimestamp endTimestamp beginPhyoffset endPhyoffet hashSlotCount indexCount
8字节 8字节 8字节 8字节 4字节 4字节
  1. beginTimestamp:索引第一条数据写入时间
  2. endTimestamp: 索引最后一条数据写入时间
  3. beginPhyoffset: 索引开始物理地址
  4. endPhyoffset:索引结束物理地址
  5. hashSlotCount: hash槽的数量
  6. indexCount: 索引中的数据量

hash槽
先解释下hash槽,数据结构中有一种结构叫做Hash表,其思想是根据hash值计算数据需要映射到的地址,其效率很高是O(1)时间复杂度,但是由于数据量很大,很可能会存在多个元素映射到同一个槽位,这时需要将相同槽位的数据通过链表来关联。
hash槽就是每个tag的hashcode对应的index下标,什么是index下标?可以就是index数据对应的写入的顺序。

index数据
index由一条条的索引数据组成,相当于一个大数组,每个元素的数据格式如下:

hashcode phyOffset timedif preIndexPos
4字节 8字节 4字节 4字节
  1. hashcode: key的hash值
  2. phyOffset: 消息对应的物理偏移量,用于拿到commitlog中的地址
  3. timedif: index-header中的beginTimestamp到该条数据写入的时间差
  4. preIndexPos: 同样hash槽的index数据上一个数据的下标

那么我们现在来看下这个indexFile是怎么查询数据的呢?大概的流程如下(省略了很多细节):

  1. 通过tag进行hash计算,找到对应的hash槽
  2. 根据hash槽指定的index下标,找到对应的index数据
  3. 然后根据index数据的preIndexPos找到下一条数据

我们知道了查找,写入的也很容易明白,这里就不赘述了。

checkpoint

记录各个文件的刷盘时间点,数据格式如下:

physicMsgTimestamp logicMsgTimestamp indexMsgTimestamp
8字节 8字节 8字节
  1. physicMsgTimestamp: commitlog刷盘时间点
  2. logicMsgTimestamp: comsumeQueue文件刷盘时间点
  3. indexMsgTimestamp: index文件刷盘时间点

其他

  • 消费队列与索引文件构建
    rocketmq创建了一个ReputMessageService线程,在不停的从commitlog获取数据,向comsumeQueue文件和indexFile文件写入
  • 消息刷盘
    有同步刷盘和异步刷盘2种策略,通过调用MappedByteBuffer.force来实现变动数据刷盘
  • 过期文件删除
    文件达到设置的过期时间会进行定期删除、在磁盘空间不足的情况也会触发文件过期删除检查,如果不能删除,会拒绝写入数据

消息消费

消费者支持2种模式,push模式和pull模式,通常为了实现消息驱动型消费者模式,采用push模式。

  • push模式的实现
    这里的push模式实际上就是依靠PullMessageService线程不断的拉取消息实现。

整体流程:

  1. 消费者实例创建,并注册到MQClientInstance中,后者负责跟broker进行通信
  2. 消费者启动会向所有的broker注册消费组信息和消费者id
  3. 消费者通过RebalanceService每隔20s会进行一次负载均衡,根据5种不同的负载策略获取对应消息队列进行消费
  4. 拿到需要消费的消息队列后,消费者通过线程向broker发送拉取请求
  5. broker判断消息存在则会立即返回数据,不存在则会进行挂起该请求,一段时间后进行重新检查,超时未找到对应的消息,则返回消息不存在
  6. 消费者拿消息后,存入ProcessQueue中,然后通过异步线程池来消费,实现消息拉取和消费的解耦
  7. 消费者在消费失败的时候,会将消息主题进行修改,重新发送broker,如果发送broker成功,则认为已经成功消费了改下消息,会提交offset
  8. 不同的消费模式,其offset持久化的方式是不同的,广播模式是存储在消费者本地,集群模式存储在broker端

5种负载均衡策略

  1. 平均分配(比较常用)
  2. 循环分配
  3. 一致性hash分配
  4. 根据配置分配
  5. 根据机房分配

顺序消费

  1. 如果是顺序消费者,需要向broker申请锁
  2. broker为每个consumeGroup保存锁
  3. 消费者拿到锁后,提交线程池运行,但是由于需要申请消费队列对象锁,所以只有一个线程可以进行消费,从而实现单消费者顺序执行
  4. 多个消费者之间由于负载均衡产生消息队列重分配给另一个消费者的情况下,消费者向broker申请锁会申请失败,所以可以达到跨进程的顺序消费

消息过滤

rocket-mq支持3种不同的过滤方式:

  • tag过滤
  1. 生产者发送带有tag的消息
  2. broker将消息记录到commitlog中,然后异步分发到ConsumeQueue上,ConsumeQueue中记录消息tag的hashcode
  3. 消费者通过订阅是传入tag过滤,broker收到请求会根据tag的hashcode选择消息发送给消费者
  4. 由于是通过hashcode当然这里可能会存在不符合情况,客户端再进行一次过滤
  • sql92过滤
  1. 消费者订阅式创建sql相关过滤规则,通过心跳上报给broker
  2. broker创建messagefilter在拉取commitlog消息时进行过滤
  • class过滤
  1. broker启动的时候,根据配置可以创建跟broker本地机器上创建filterServer进程
  2. broker通过心跳将filterserver相关信息上报给nameserver
  3. 消费者在订阅消息是,可以注册自己的过滤类,发送filterserver
  4. filterserver会进行编译生成对应的过滤器
  5. 消费者拉取消息直接请求filterserver,filterserver作为代理,再去broker拉取消息,并且通过consumer设置的过滤器进行数据过滤

主从机制

流程

  1. slave节点与master节点建立tcp长连接
  2. 定时上报slave的commitlog的最大offset
  3. master节点根据收到slave的定时心跳来确定发送对应的消息数据
  4. slave通过响应线程接收数据,进行消息的同步
  • slave的作用
  1. 作为高可用的一部分,在broker节点宕机时提供消费者继续消费的服务
  2. 在master节点压力较大的时候,slave节点会提供一定的负载能力
  3. master节点宕机恢复后,会从slave节点同步消费commit位置

事务实现机制

rocketmq通过2阶段提交实现事务消息

流程

  1. 事务生产者发送一阶段prepare消息给broker
  2. 如果发送失败,本地事务直接失败
  3. 如果发送成功,broker将prepare消息的topic进行重命名,并保存原topic,也就是将消息发送到half的主题上
  4. 事务生产者执行本地事务,如果执行成功,向broker发送二阶段commit消息,broker从op_half(已经处理过消息会进入该主题的消息队列)主题上查找是否存在,如果不存在(说明消息未被处理),获取对应的消息,恢复消息的原主题重新提交到commitlog中,并且发送消息到op_half队列,用来标记该消息已经处理
  • 对于生产者二阶段提交失败怎么办?
    broker提供定时检查线程:
  1. 检查half队列中的消息,对于不存在op_half队列中的消息,通知生产者进行回查
  2. 生产者通过本地事务检查,本地如果事务已经提交,则发送二阶段commit消息给broker;如果本地事务失败,则发送二阶段rollback消息给broker

参考

存储技术
事务实现原理

你可能感兴趣的:(java,mq,分布式,队列)