【RocketMQ长文 从入门到精通(中)】工作原理

消息生产

生产过程

  1. Producer发送消息前,向NameServer发出获取Topic的路由信息的请求
  2. NameServer返回该Topic的路由表及Broker列表
  3. 通过特定的Queue选择算法选出要存储消息的队列
  4. 对消息做特殊处理
  5. Producer向Queue所在的Broker发送RPC请求,将消息发送到选择的队列里

路由表:Map数据结构,key为Topic名称,value为QueueData实例列表。一个QueueData对应 一个Broker中与key对应的Topic 里的所有的Queue。QueueData包含BrokerName

路由表在 NameServer 中的核心数据结构可以简化理解为一个 Map<String, TopicRouteData>

  • Key:Topic 名称(表示该路由信息所属的消息主题)
  • ValueTopicRouteData 对象(包含该 Topic 在整个集群中的完整路由信息)

举个更直观的例子:如果 Topic OrderTopic 分布在 broker-1broker-2 两个 Broker 上,那么路由表中:

  • Key 为 OrderTopic
  • Value(TopicRouteData)中的 QueueData 列表包含两个元素:
    • 元素 1:brokerName=broker-1,记录 broker-1 上该 Topic 的队列配置
    • 元素 2:brokerName=broker-2,记录 broker-2 上该 Topic 的队列配置

每个 QueueData 包含以下关键属性(核心字段):

属性名 数据类型 含义说明
brokerName String 所属 Broker 的名称(唯一标识一个 Broker 节点或一组主从节点共享的逻辑名称)
readQueueNums int 该 Broker 上该 Topic 的读队列数量(消费者从这些队列拉取消息)
writeQueueNums int 该 Broker 上该 Topic 的写队列数量(生产者向这些队列发送消息)
perm int 队列权限标识(通过位运算表示,如:6=2+4表示同时拥有读和写权限)
topicSynFlag int Topic 同步标识(用于主从复制场景,标识该 Topic 的队列是否需要同步到从节点)

Broker列表:Map数据结构。key为BrokerName,value为BrokerData。BrokerData即为BrokerName相同的Broker主从集群

Queue选择算法

  • 轮询算法
    • 实现简单,能基本保证消息在队列间的均匀分布,避免单个队列负载过高
    • 未考虑 Broker 实际负载(如网络延迟、处理能力差异),可能导致负载不均(例如某 Broker 性能较差但仍被分配等量消息)
  • 最小投递延迟算法
    • 能动态适应 Broker 性能变化,将更多消息分配到处理速度快、延迟低的 Broker / 队列,提升整体发送效率。
    • 实现复杂,需要维护延迟统计并处理统计偏差(如网络抖动导致的瞬时延迟);首次发送时无历史数据,可能需要退化到轮询等基础算法
    • 极端情况导致分配不均,有的消费者对应的队列消息过多,消费能力下降,导致消息堆积

消息的存储

RocketMQ中的消息存储在本地文件系统中,这些相关文件默认在当前用户主目录下的store目录中。

  • abort:该文件在Broker启动后会自动创建,正常关闭Broker,该文件会自动消失。若在没有启动 Broker的情况下,发现这个文件是存在的,则说明之前Broker的关闭是非正常关闭。
  • checkpoint:其中存储着commitlog、consumequeue、index文件的最后刷盘时间戳
  • commitlog:其中存放着commitlog文件,而消息是写在commitlog文件中的
  • config:存放着Broker运行期间的一些配置数据
  • consumequeue:其中存放着consumequeue文件,队列就存放在这个目录中
  • index:其中存放着消息索引文件indexFile
  • lock:运行期间使用到的全局资源锁

commitlog

commitlog目录中存放很多mappedFile文件 ,当前Broker中所有的消息都是落盘到这些mappedFile文件中。

一个Broker中只有一个commitlog目录 ,broker中无论是什么类型的topic 都会被按顺序写入mappedFile。

mappedFile文件是顺序读写的文件,所以其访问效率很高。文件名由20位十进制数构成,表示 当前文件的第一条消息的起始位移偏移量。

mappedFile由多个消息单元构成。

consumequeue

consumequeue文件是commitlog的索引文件,可以根据consumequeue定位到具体的消息。

consumequeue文件名也由20位数字构成,表示当前文件的第一个索引条目的起始位移偏移量。与 mappedFile文件名不同的是,其后续文件名是固定的。因为consumequeue文件大小是固定不变的。

文件读写

消息写入:

  • Broker根据queueId获取该消息对应索引条目要在consumequeue目录写入的偏移量
  • 将queueId,queueOffset等数据与消息一起封装为消息单元
  • 将消息单元写入commitlog,同时形成消息索引条目
  • 将消息索引条目发放到consumequeue

消息拉取:

  • Consumer获取准备消费的消息所在Queue的消费偏移量offset,计算出其要消费消息的消息offset
  • Consumer向Broker发送拉取请求,请求中包含要拉取消息的Queue,消息offset以及消息Tag
  • Broker计算消息在该consumequeue中的queueOffset(queueOffset = 消息offset * 索引条目字节数)
  • 从该queueOffset出发向后寻找第一个指定Tag的索引条目
  • 解析该索引条目,定位消息在commitlog中的commitlog offset
  • 从对应的commitlog offset读取消息单元并发送给Consumer

indexFile

RocketMQ提供了根据Key查询消息的功能,索引数据是包含了key的消息被发送到Broker中被写入的,如果没有包含key则不写。

索引条目结构

每个Broker都包含一组indexFile,每个indexFile以其被创建时的时间戳命名。 每个indexFile文件由三部分构成:indexHeader,slots槽位,indexes索引数据。每个 indexFile文件中包含500w个slot槽。而每个slot槽又可能会挂载很多的index索引单元。

indexHeader固定40个字节,其中存放着如下数据:

  • beginTimestamp:该indexFile中第一条消息的存储时间
  • endTimestamp:该indexFile中最后一条消息存储时间
  • beginPhyoffset:该indexFile中第一条消息在commitlog中的偏移量commitlog offset
  • endPhyoffset:该indexFile中最后一条消息在commitlog中的偏移量commitlog offset
  • hashSlotCount:已经填充有index的slot数量(并不是每个slot槽下都挂载有index索引单元,这里统计的是所有挂载了index索引单元的slot槽的数量)
  • indexCount:该indexFile中包含的索引单元个数(统计出当前indexFile中所有slot槽下挂载的所有index索引单元的数量之和)

slots是固定的,但indexs索引数据是变化的,如果在每个slot后面存放其对应的索引数据,其所需的空间是无法确定的。因此统一将索引数据放到slots之后,那么用来存储索引数据的空间是无穷的。

为了便于理解,slots和indexs对应关系如图:

index索引单元默认20个字节,其中存放着以下四个属性:

  • keyHash:消息中指定的业务key的hash值
  • phyOffset:当前key对应的消息在commitlog中的偏移量commitlog offset
  • timeDiff:当前key对应消息的存储时间与当前indexFile创建时间的时间差
  • preIndexNo:当前slot下当前index索引单元的前一个index索引单元的indexNo

keyhash值 % 500w的结果即为slot槽位,然后将该slot值修改为该index索引单元的indexNo,根据这个indexNo可以计算出该index单元在indexFile中的位置。

indexNo是一个在indexFile中的流水号,从0开始依次递增。indexNo在index索引单元中是没有体现的,其是通过在indexes中依次数出来的

在每个index索引单元中增加了preIndexNo,用于指定该slot中当前index索引单元的前一个index索引单元。而slot中始终存放的是其下最新的index索引单元的indexNo。

文件名

根据业务key进行查询时,查询条件除了key之外,还需要指定一个要查询的时间戳,表示要查询不大于该时间戳的最新的消息。

时间戳文件名可以简化查询,提高查询效率。

indexFile创建时机:

  • 当第一条带key的消息发送来后,系统发现没有indexFile,此时会创建第一个indexFile文件
  • 当一个indexFile中挂载的index索引单元数量超出2000w个时,会创建新的indexFile。当带key的消息发送到来后,系统会找到最新的indexFile, 并从其indexHeader的最后4字节中读取到 indexCount。若indexCount >= 2000w时,会创建新的indexFile。

查询流程

​ ​

推拉消费模型

pull:

  • Consumer主动从Broker中拉取消息
  • 实时性低
  • 拉去时间间隔由用户指定,若设置不当:间隔太短,空请求比 例会增加;间隔太长,消息的实时性太差

push:

  • Broker收到数据后会主动推送给Consumer
  • 实时性高
  • 采用了发布-订阅模式,Consumer向其关联的queue注册了监听器, 一旦发现有新的消息到来就会触发回调的执行。

消费模式

广播模式:

相同Consumer Group的每个Consumer实例都接收同一个Topic的全量消息,每条消息都会被发送到Consumer Group中的每个Consumer。

消费模型为pull模型,每个consumer端各自保存自己的消费进度。

集群消费:

相同Consumer Group的每个Consumer实例平均分摊同一个Topic的消息,每条消息只会被发送到Consumer Group中的某个Consumer。

消费模型为push模型,消费进度保存在broker里,由consumer group中的consumer共享

Rebalance机制

将一个Topic下的多个Queue在同一个Consumer Group中多个Consumer之间重新分配的过程。

例如,⼀个Topic下5个队列,在只有1个消费者的情况下,这个消费者将负责消费这5个队列的消息。如果此时我们增加⼀个消费者,那么就可以给其中⼀个消费者分配2个队列,给另⼀个分配3个队列,从而提升消息的并行消费能力。

缺点:

  • 消息暂停:

只有一个Consumer时,他负责全部的队列。当新增一个Consumer时,原来的不得不停止消费一些队列,然后将这些队列分给新的Consumer,这些停止的队列才能继续消费

  • 消费重复:

Consumer消费新队列时是根据之前Consumer提交的消费进度offset 继续消费的,但offset提交默认为异步提交,就有可能导致新的Consumer接到消息但没有收到offset,因此重新消费部分信息

同步提交:consumer提交了其消费完毕的一批消息的offset给broker后,需要等待broker的成功 ACK。当收到ACK后,consumer才会继续获取并消费下一批消息。在等待ACK期间,consumer 是阻塞的。

异步提交:consumer提交了其消费完毕的一批消息的offset给broker后,不需要等待broker的成 功ACK。consumer可以直接获取并消费下一批消息。

  • 消费突刺:

由于需要重复消费的消息过多,或者Rebalance暂停时间过长,导致消息挤压。在Rebalance释放瞬间需要消费很多消息。

Rebalance场景

Queue数量发生变化的场景:

  • Broker扩容或缩容
  • Broker升级运维
  • Broker与NameServer间的网络异常
  • Queue扩容或缩容

消费者数量发生变化的场景:

  • Consumer Group扩容或缩容
  • Consumer升级运维
  • Consumer与NameServer间网络异常

Rebalance过程

在Broker中维护着多个Map集合:

  • TopicConfigManager:key是topic名称,value是TopicConfig。TopicConfig中维护着该Topic中所 有Queue的数据。
  • ConsumerManager:key是Consumser Group Id,value是ConsumerGroupInfo。 ConsumerGroupInfo中维护着该Group中所有Consumer实例数据。
  • ConsumerOffsetManager:key为Topic与订阅该Topic的Group的组合,即topic@group, value是一个内层Map。内层Map的key为QueueId,内层Map的value为该Queue的消费进度 offset。

Broker 一旦发现消费者所订阅的Queue数量发生变化 ,或消费者组中消费者的数量发生变化 ,立即向Consumer Group中的每个实例发出Rebalance通知

Consumer接到通知,采用Queue算法获取到对应的Queue,自主进行Rebalance。

Queue分配算法

平均分配策略

编辑

avg = QueueCount / ConsumerCount。整除的话直接平均分配;不能整除将多余的按Consumer的顺序依次分配

环形平均策略

不用事先计算,根据消费者的顺序,依次在由queue队列组成的环形图中逐个分配。

一致性hash策略

编辑

该算法会将consumer的hash值作为Node节点存放到hash环上,然后将queue的hash值也放到hash环上,通过顺时针方向,距离queue最近的那个consumer就是该queue要分配的consumer。

同机房策略

该算法会根据queue的部署机房位置和consumer的位置,过滤出当前consumer相同机房的queue。然后按照平均分配策略或环形平均策略对同机房queue进行分配。如果没有同机房queue,则按照平均分配策略或环形平均策略对所有queue进行分配。

对比

一致性hash算法复杂,分配效率低,分配的结果可能不均

但其 可以有效减少由于消费者组扩容或缩容所带来的大量的Rebalance。

可以看到,平均分配算法在扩容后会连带从头开始的Queue的分配,Rebalance的比例很高

但一致性hash无论扩容还是缩容,只会影响变化的Queue附近的Consumer的分配。

至少一次原则

RocketMQ规定每条消息必须要被成功消费一次。

Consumer在消费完消息后会向其消费进度记录器提交其消费消息的offset, offset被成功记录到记录器中,那么这条消费就被成功消费了。

对于广播模式,Consumer自身就是消费进度记录器

对于集群模式,Broker是消费进度记录器

​ ​

订阅关系的一致性

订阅关系的一致性指的是,同一个消费者组下所有Consumer实例所订阅的Topic与Tag及对消息的处理逻辑必须完全一致。否则,消息消费的逻辑就会混乱,甚至导致消息丢失。

错误订阅关系

  • 订阅了不同的topic
  • 订阅了不同的tag
  • 订阅了不同数量的topic

offset管理

消费进度offset是用来记录每个Queue的不同消费者组的消费进度

本地管理模式

消费模式为广播消费 ,消费进度offset被每个消费者自己管理

Consumer在广播消费模式下offset相关数据以json的形式持久化到Consumer本地磁盘文件中

远程管理模式

消费模式为集群消费,所有consumer共享offset,由Broker管理

Consumer在集群消费模式下offset相关数据以json的形式持久化到Broker磁盘文件

Broker启动时会加载这个文件,并写入到一个双层Map(ConsumerOffsetManager) 。外层map的key 为topic@group,value为内层map。内层map的key为queueId,value为offset。当发生Rebalance时, 新的Consumer会从该Map中获取到相应的数据来继续消费。

当消费完一批消息后,Consumer会提交其消费进度offset给Broker,Broker在收到消费进度后会将其更新到那个双层Map(ConsumerOffsetManager)及consumerOffset.json文件中,然后向该Consumer进行ACK ,而ACK内容中包含三项数据:当前消费队列的最小offset(minOffset)、最大 offset(maxOffset)、及下次消费的起始offset(nextBeginOffset)。

重试队列:消费异常时,将消费异常的offset提交到Broker中的重试队列。系统在发生消息消费异常时会自动创建topic@group重试队列。

同步提交与异步提交

同步提交:从ack中获取 nextBeginOffset

异步提交:从Broker中直接获得 nextBeginOffset

消费幂等

概念

重复消费和消费一次的结果是一样的:f(x) = f(f(x))

消息重复的场景

  • 发送消息时重复:

当一条消息已被成功发送到Broker并完成持久化,此时出现了网络闪断,从而导致Broker对Producer应答失败。 如果此时Producer意识到消息发送失败并尝试再次发送消息,消息重复。

  • 消费时重复:

消息已投递到Consumer并完成业务处理,当Consumer给Broker反馈应答时网络闪断,Broker没有接收到消费成功响应。为了保证消息至少被消费一次的原则,Broker将在网络恢复后再次尝试投递之前已被处理过的消息。

  • Rebalance时消息重复:

Rebalance时Consumer可能会收到曾经被消费过的消息

通用解决方案

  • 幂等令牌:通常指唯一业务标识的字符串
  • 唯一性处理:服务端采用一定的算法策略,保证同一个业务逻辑不会被重复执行成功多次

实现

将消息的Key设置为订单号,作为幂等处理的依据。

消费者收到消息时可以根据消息的Key即订单号来实现消费幂等

消息堆积与消费延迟

概念

如果Consumer的消费速度跟不上Producer的发送速度,MQ中未处理的消息会越来越多(进的多出的少),这部分消息就被称为堆积消息。消息出现堆积进而会造成消息的消费延迟。

  • 业务上下游能力不匹配,且无法自行恢复
  • 对消费实时性要求高

原因

Consumer 使用长轮询Pull模式消费消息时,分为以下两个阶段:

  1. 消息拉取:长轮询Pull模式拉取消息,吞吐量很好,不会成为瓶颈
  2. 消息消费:消费能力就完全依赖于消息的消费耗时和消费并发度。如果由于业务处理逻辑复杂等原因,导致处理单条消息的耗时较长,则整体的消息吞吐量肯定不会高,此时就会导致Consumer本地缓冲队列达到上限,停止从服务端拉取消息。造成消息堆积

因此,堆积与否取决于消费能力,消费能力由消费耗时和消费并发度决定

保证消费耗时的合理性前提下考虑消费并发度

消费耗时

代码逻辑中可能会影响处理时长代码主要有两种类型: CPU内部计算型代码和外部I/O操作型代码。

内部计算耗时相对外部I/O操作来说几乎可以忽略,外部IO型代码是影响消息处理时长的主要原因所在。

外部IO操作型代码举例:

  • 读写外部数据库,例如对远程MySQL的访问
  • 读写外部缓存系统,例如对远程Redis的访问

消费并发度

消费者端的消费并发度由 单节点线程数(单个Consumer包含的线程数量) 和 节点数量(Consumer Group中Consumer的数量) 共同决定( 单节点线程数 * 节点 数量 )

优先调整单节点的线程数,若单机资源达到上限,则扩展节点数量

单机线程数计算:

理想环境下单节点的最优线程数计算模型为:C *(T1 + T2)/ T1。

  • C:CPU内核数
  • T1:CPU内部逻辑计算耗时
  • T2:外部IO操作耗时

如何避免

梳理消息的消费耗时

  • 消息消费逻辑的计算复杂度是否过高,代码是否存在无限循环和递归等缺陷。
  • 消息消费逻辑中的I/O操作是否是必须的,能否用本地缓存等方案规避。
  • 消费逻辑中的复杂耗时的操作是否可以做异步化处理。

设置消费并发度

  • 逐步调大单个Consumer节点的线程数,并观测节点的系统指标,得到单个节点最优的消费线程数和消息吞吐量。
  • 根据上下游链路的流量峰值计算出需要设置的节点数

​ ​

相关推荐
用户9446814013504 小时前
【RocketMQ长文 从入门到精通(上)】基础概念
rocketmq
若水不如远方3 天前
RocketMQ消费流程深度解析:从原理到实践
后端·rocketmq
腾讯云中间件5 天前
TDMQ CKafka 版客户端实战指南系列之二:消费消息最佳实践
kafka·消息队列
腾讯云中间件5 天前
TDMQ CKafka 版客户端实战指南系列之一:生产最佳实践
kafka·消息队列·腾讯
AscentStream5 天前
某大型银行跨区域 Pulsar 集群网络问题分析报告
消息队列
Apache_RocketMQ5 天前
Apache RocketMQ 打破锁性能瓶颈之道
云原生·消息队列·rocketmq
TF男孩8 天前
ARQ:一款低成本的消息队列,实现每秒万级吞吐
后端·python·消息队列
往事随风去8 天前
架构师必备思维:从“任务队列”到“事件广播”,彻底吃透消息队列两大设计模式
消息队列·rabbitmq
阿里云云原生8 天前
海量接入、毫秒响应:易易互联携手阿里云构筑高可用物联网消息中枢
rocketmq