四、RocketMQ消息消费总览

1.消费者启动

解析this.defaultMQPushConsumerImpl.start()方法

step1:构建主题订阅信息SubscriptionData 并加入到Rebalancelmpl 的订阅消息中。订阅关系来源主要有两个。

  • a.通过调用DefaultMQPushConsumerlmpl#subscrib巳( String topic, String subExpression)方法。
  • b.订阅重试主题消息。从这里可以看出, RocketMQ 消息重试是以消费组为单位,而不是主题,消息重试主题名为%RETRY%+消费组名。消费者在启动的时候会自动订阅该主题,参与该主题的消息队列负载。

Step2:创建or获取MQClientInstance,并存放本地缓存key:ip@pid

Step3:初始化RebalanceImpl,如属性消费者负载策略-轮询

Step4 : 初始化消息进度。如果消息消费是集群模式,那么消息进度保存在Broker 上;

  • 如果是广播模式,那么消息消费进度存储在消费端

Step5 :根据是否是顺序消费,创建消费端消费线程服务。ConsumeMessageService 主要负责消息消费,内部维护一个线程池。

  • 开启定时任务:cleanExpireMsg-Ack卡进度解决方案。线程ConsumeMessageConcurrentlyService

Step6 :向MQClientlnstance 注册消费者,并启动MQClientlnstance , 在一个JVM 中的所有消费者、生产者持有同一个MQClientlnstance, MQClientlnstance 只会启动一次。

2.消息消费总览

  • 1.概述:
  • 消费者消费模式:

消息消费以组的模式开展, 一个消费组内可以包含多个消费者,每一个消费组可订阅多个主题,消费组之间有集群模式与广播模式两种消费模式。集群模式,主题下的同一条消息只允许被其中一个消费者消费。广播模式,主题下的同一条消息将被集群内的所有消费者消费一次。

  • 消费者消息拉取模式:

消息服务器与消费者之间的消息传送也有两种方式:推模式、拉模式。所谓的拉模式,是消费端主动发起拉消息请求,而推模式是消息到达消息服务器后,推送给消息消费者。RocketMQ 消息推模式的实现基于拉模式,在拉模式上包装一层,一个拉取任务完成后开始下一个拉取任务。消费者使用spring监听方式,不需要开发者手动拉取消息,可以理解为推模式;而消费者所在应用,主动取拉取消息方式,称为拉模式。

  • 集群模式下,消费者如何对消息队列进行负载呢?

消息队列负载机制遵循一个通用的思想: 一个消息队列同一时间只允许被一个消费者消费,一个消费者可以消费多个消息队列。rocketMQ是如何实现的?ReblanceService负责多个消费者负载该主题下的多个消费队列实现。消息队列分配算法详见线程ReblanceService

  • 顺序消息消费:rocketMQ 支持局部顺序消息消费,也就是保证同一个消息队列上的消息顺序消费。不支持消息全局顺序消费, 如果要实现某一主题的全局顺序消息消费, 可以将该主题的队列数设置为1 ,牺牲高可用性。
  • 消息过滤模式:表达式( TAG 、SQL92 )与类过滤模式。
    • 图解:

    • 1.线程ReblanceService:
      • 唯一性:一个MQClientInstance持有一个RebalanceService实现,并随着MQC!ientlnstance 的启动而启动。每个DefaultMQPushConsumerlmpl 都持有一个单独的Rebalancelmpl对象,该方法主要是遍历订阅信息对每个主题的队列进行重新负载
      • 1.作用:
        • a.Rebalanceservice线程每隔20s对消费者订阅的主题进行一次队列重新分配,每一次分配都会获取主题的所有队列、从 Broker 服务器实时查询当前该主题该消费组内消费者列表,对新分配的消息队列会创建对应的 PullRequest 对象。在一个 JVM 进程中,同一个消费组同一个队列只会存在一个 PullRequest 对象 。
        • b.负责多个消费者负载该主题下的多个消费队列,并且当有新的消费者加入或原消费者下线时,负责消息队列的重新分布。由于每次进行队列重新负载时会从 Broker 实时查询出当前消费组内所有消费者,并且对消息队列、消费者列表进行排序,这样新加入的消费者就会在队列重新分布时分配到消费队列从而消费消息。
      • 2.触发RebalanceServcie.doRebalance操作两个场景:1.该线程的自旋20s;2.当消费者下线(发送心跳超时或直接挂掉)或新增消费者时,broker向该消费者分组的所有消费者发出通知,分组内消费者重新分配队列继续消费;
      • 3.RocketMQ消息队列分配算法:
      • RocketMQ 默认提供5 种分配算法。

        1)AllocateMessageQueueAveragely :平均分配,推荐指数为5 颗星。

        举例来说,如果现在有8 个消息消费队列ql , q2 , q3 , 俐, q5 , 币,q7 ,币,有3 个消费者cl,c2 , c3 ,那么根据该负载算法,消息队列分配如下:

        c1: q l ,q2,q3

        c2:q4 ,q5,q6

        c3:q7 ,q8

        2)AllocateMessageQueueAveragelyByCircle :平均轮询分配,推荐指数为5 颗星。举例来说,如果现在有8 个消息消费队列咐,q2 ,币, 俐, q5 , 币,q7 ,币, 有3 个消费者cl , c2,

        c3 ,那么根据该负载算法,消息队列分配如下:

        cl : ql,q4, q7

        c2 : q2,q5,q8

        c3: q3,q6

        3)AllocateMessageQueueConsistentHash : 一致性hash 。不推荐使用,因为消息队列负载信息不容易跟踪。

        4)AllocateMessageQueueByConfig :根据配置,为每一个消费者配置固定的消息队列。

        5)AllocateMessageQueueByMachineRoom :根据Broker 部署机房名,对每个消费者负责不同的Broker 上的队列。

        消息负载算法如果没有特殊的要求,尽量使用AllocateMeseQueueAveragely 、AllocateMessageQueueAveragelyByCircle ,因为分配算法比较直观。消息队列分配遵循一个消费者可以分配多个消息队列,但同一个消息队列只会分配给一个消费者,故如果消费者个数大于消息队列数量,则有些消费者无法消费消息。

    • 2.线程PullMessageService:
      • 1.作用:获取到PullRequest对象后,从broker默认每次拉取32条消息,按消息的队列偏移量顺序放在ProcessQueue中,线程PullMessageService然后将消息提交到消费者消费线程池,无论消费成功or失败,都将拉取到的消息从ProcessQueue中移除。
      • 2.当从broker拉取过消息过来放入到PullRequest的treemap中,如果消费不及时,treemap会不会被oom?其实不会,因为有限流:被限流后,方法返回,不会再从broker取消息数据。
      • 3.保证线程的轻量级,io线程与业务线程分离。
        • a.broker侧收到consuer通过长连接的请求,通过netty的io线程方法(channelRead),交给业务线程池处理(默认是NettyClientPublicExecutor);业务线程处理完后,通过netty的io线程方法(writeAndFlush)发送给客户端
        • b.consumer亦是如此
      • 4.上报消费进度的方式
        • 1.每个5s,周期性上报(defaultMQPushConsumerImpl.getOffsetStore().updateOffse);
        • 2.在拉取消息时,通过入参也会上报进度
      • 3.线程ConsumeMessageConcurrentlyService
      • 1.开启定时任务:cleanExpireMsg-Ack卡进度解决方案。遍历所消费的队列对应的ProcessQueue每个ProcessQueue的消息缓存(TreeMap),取该队列最小偏移量的消息,若该消息自消费开始至现在,超过设置的超时时间,则写回broker,并从缓存中移除该消息。
      • 2. processConsumeResult方法:
        • 1.对于消费者客户端消费失败的消息,首先同步重新写回broker。若同步写回失败,则从缓存中(TreeMap)移除失败消息,同时交给线程ConsumeMessageConcurrentlyService再次处理,相当于ack确认。

          2. 并发消费,无论消费成功与否,删除本条消息,返回最早的offset,相当于ack确认,不卡消费进度。先将该messagequeeu对于的offset保存至offsetTable中。然后由MQClientInstance的定时任务-持久化消费者消费进度向brokerName

  • 3.消息服务端broker组装消息

    • 1.根据consumeQueue从comitlog的堆外内存获取消息时,是逐个循环索引,逐个取comitlog中消息。为什么不是批量从commitlog中获取?
      • 因为consumeQueue中的相邻的索引对应的消息在commitlog中不相邻。所以commitlog文件的消息读取是随机读。虽然是随机读,仍然高效是因为有pageCahe存在,当读取范围超越了pageCache范围后,才会到磁盘。
    • 2.读取消息时,是否都会命中pagecache?
      • 不会的。当读取消息的物理偏移量与最新消息的物理偏移量,超过内存的40%,将会建议从片读消息。原因是:所有的commitlog文件都是从pagecahce中读取,但pagecache会有操作系统级别的置换算法的,没有命中的话,会产生缺页中断,会去磁盘上加载到pagecache中,这个由操作系统实现。
    • 3.当成功获取消息后,将消息作为响应写回给客户端的两种方式?
      • 由参数this.brokerController.getBrokerConfig().isTransferMsgByHeap()决定,Y:需要拷贝到用户态下的heap,然后再copy到内核态的socket,需要开销。N:zero copy-FileRegion。

4. 消息进度管理

  • 1.进度存储:
    • b.集群模式:同一个消费组内的所有消息消费者共享消息主题下的所有消息, 同一条消息(同一个消息消费队列)在同一时间只会被消费组内的一个消费者消费,并且随着消费队列的动态变化重新负载,所以消费进度存储文件存放在消息服务端Broker。
    • a.广播模式:同一个消费组的所有消息消费者都需要消费主题下的所有消息,也就是同组内的消费者的消息消费行为是独立的,互相不影响,故消息进度需要独立存储,最理想的存储地方应该是与消费者绑定
  • 2.消费进度思考
    • a.消费者线程池每处理完一个消息消费任务( ConsumeRequest)时会从 ProceeQueue中移除本批消费 的消息 ,并返回 ProcessQueue 中最小的偏移量,用该偏移量更新消息队列消费进度,也就是说更新消费进度与消费任务中的消息没什么关系,会带来一个潜在的重复问题?可能出现ProcessQueue的msgTreeMap的最小的偏移量一直消费失败,但后面的消息偏移量都已消费成功,并且消费失败的消息写回broker也失败,造成该最小偏移量的消息ACK卡进度,消费进度无法向前推进?
    • rocketmq的解决方案
      • 会定期扫描主题下的所有小覅额,达到这个timeout的那些消息,就会触发sendBack操作以达到ack的目的,推进消费进度。

5.顺序消息

RocketMQ 支持局部消息顺序消费,可以确保同一个消息消费队列中的消息被顺序消费,如果需要做到全局顺序消费则可以将主题配置成一个队列,例如数据库BinLog 等要求严格顺序的场景。根据并发消息消费的流程,消息消费包含如下4 个步骤: 消息队列负载、消息拉取、消息消费、消息消费进度存储。

1.消费者启动

根据是否是顺序消费,创建消费端消费线程服务(ConsumeMessageOrderlyService),并启动定时任务。

代码实现详见消费者启动(黄色背景)

2.消息队列负载

RocketMQ 首先需要通过RebalanceService 线程实现消息队列的负载, 集群模式下同一个消费组内的消费者共同承担其订阅主题下消息队列的消费, 同一个消息消费队列在同一时刻只会被消费组内一个消费者消费, 一个消费者同一时刻可以分配多个消费队列。

如果经过消息队列重新负载(分配)后,分配到新的消息队列时,首先需要尝试向Broker 发起锁定该消息队列的请求,如果返回加锁成功则创建该消息队列的拉取任务,否则将跳过,等待其他消费者释放该消息队列的锁,然后在下一次队列重新负载时再尝试加锁顺序消息消费与并发消息消费的第一个关键区别: 顺序消息在创建消息队列拉取任务时需要在Broker 服务器锁定该消息队列。

代码实现详见消费者消费(黄色背景)

3.消息拉取

如果消息处理队列未被锁定,则延迟3s 后再将PullRequest 对象放入到拉取任务中,如果该处理队列是第一次拉取任务,则首先计算拉取偏移量,然后向消息服务端拉取消息。代码实现详见消费者消费(黄色背景)

4.消息消费

a. ConsumeMessageOrderlyService 启动方法:ConsumeMessageOrderlyService.this.lockMQPeriodically()

如果消费模式为集群模式,启动定时任务,默认每隔20s 执行一次锁定分配给自己的消息消费队列。通过-Drocketmq. client.rebalance. locklnterval=20000 设置间隔,该值建议与一次消息负载频率设置相同。从上文可知,集群模式下顺序消息消费在创建拉取任务时并未将ProcessQu巳ue 的locked 状态设置为true ,在未锁定消息队列之前无法执行消息拉取任务, ConsumeM巳ssageOrderlyService 以每2 0s 的频率对分配给自己的消息队列进行自动加锁操作,从而消费加锁成功的消息消费队列。

b. ConsumeMessageOrderlyService 提交消费任务

Stepl :如果消息处理队列为丢弃, 则停止本次消费任务。

Step2 :根据消息队列获取一个对象。然后消息消费时先申请独占obj Lock 。顺序消息消费的并发度为消息队列。也就是一个消息消费队列同一时刻只会被一个消费线程池中一个线程消费。

Step3 :如果是广播模式的话,直接进入消费,无须锁定处理队列,因为相互直接无竞争; 如果是集群模式,消息消费的前提条件是proceessQueu巳被锁定并且锁未超时。思考一下,会不会出现当消息队列重新负载时,原先由自己处理的消息队列被另外一个消费者分配,此时如果还未来得及将Pro c eeQueue 解除锁定,就被另外一个消费者添加进去, 此时会存储多个消息消费者同时消费一个消息队列?答案是不会的,因为当一个新的消费队列分配给消费者时, 在添加其拉取任务之前必须先向Broker 发送对该消息队列加锁请求,只有加锁成功后,才能添加拉取消息,否则等到下一次负载后,只有消费队列被原先占有的消费者释放后,才能开始新的拉取任务。集群模式下,如果未锁定处理队列,则延迟该队列的消息消费。

Step4 : 顺序消息消费处理逻辑,每一个ConsumeRequest 消费任务不是以消费消息条数来计算的,而是根据消费时间,默认当消费时长大于MAX TIME CONSUMECONTINUOUSLY ,默认60s 后,本次消费任务结束,由消费组内其他线程继续消费。

Step6 : 每次从处理队列中按顺序取出consumeB atchSize 消息,如果未取到消息, 则设置continueConsume 为false ,本次消费任务结束。顺序消息消费时,从ProceessQueue 中取出的消息,会临时存储在ProceeQueue 的consumingMsgOrderlyTre 巳Map 属性中。

Step7 :申请消息消费锁,如果消息队列被丢弃,放弃该消息消费队列的消费,然后执行消息消费监听器,调用业务方具体消息监听器执行真正的消息消费处理逻辑,并通知RocketMQ 消息消费结果。

Step8 :执行消息消费钩子函数, 计算消息消费过程中应用程序抛出异常,钩子函数的后处理逻辑也会被调用。

Step9 :如果消息消费结果为Co nsum巳Orderl yStatus .SUCCE SS ,执行ProceeQueue 的commit 方法,并返回待更新的消息消费进度。

提交,就是将该批消息从ProceeQueue 中移除,维护msgCount (消息处理队列中消息条数)并获取消息消费的偏移量offset ,然后将该批消息从msgTreeMapTemp 中移除,并返回待保存的消息消费进度( offset+ 1 ),从中可以看出o ffset 表示消息消费队列的逻辑偏移量, 类似于数组的下标,代表第n 个ConsumeQueue 条目。

1 )检查消息的重试次数。如果消息重试次数大于或等于允许的最大重试次数,将该消息发送到Broker 端, 该消息在消息服务端最终会进入到DLQ (死信队列),也就是RocketMQ 不会再次消费,需要人工干预。如果消息成功进入到DLQ 队列,checkReconsumeTimes 返回false ,该批消息将直接调用ProcessQueue#commit 提交, 表示消息消费成功,如果这批消息中有任意一条消息的重试次数小于允许的最大重试次数,将返回true ,执行消息重试。

RocketMQ 序消息消如果消息重次数达到允的最大重次数并且向Broker ACK 消息返回成功也就是成功将消息存入到RocketMQ DLQ 列中即认为是消息消成功,继续该消息消费队列后消息的消

2 )消息消费重试,先将该批消息重新放入到ProcessQueue 的msgTreeMap , 然后清除consumingMsgOrderlyTreeMap , 默认延迟ls 再加入到消费队列中,并结束此次消息消费。可以通过DefaultMQPushConsumer# setSuspendCurrentQueueTimeMillis 设置当前队列重试挂起时间。如果执行消息重试, 因为消息消费进度并未向前推进,故本次视为无效消费,将不更新消息消费进度。

SteplO : 存储消息消费进度。

5.消息队列锁实现

顺序消息消费的各个环节基本都是围绕消息消费队列( MessageQueue )与消息处理队列( ProceeQueu巳) 展开的。消息消费进度拉取,消息进度消费都要判断ProceeQueue 的locked 是否为true ,设置ProceeQueu巳为true 的前提条件是消息消费者( cid )向Broker 端发送锁定消息队列的请求并返回加锁成功。服务端关于MessageQueue 加锁处理类: org.apache.rocketmq.broker.client.rebalance.RebalanceLockManager 。

6.问题

1.集群内多个消费者是如何负载主题下的多个消费队列, 并且如果有新的消费者加入时,消息队列又会如何重新分布?见消息消费总览-线程ReblanceService作用。消费者消费队列重新分布的策略有哪些?见消息消费总览-消息队列分配算法

2.rocketMQ的消费者客户端是如何实现ack确认的?

并发消费见线程ConsumeMessageConcurrentlyService,既然是通过(上报进度/失败消息写回broker)实现ack确认,但存在一种可能,上报的进度跟本批消费的消息无关,存在卡进度的场景,是如何解决的?详见消息进度管理 + ConsumeMessageConcurrentlyService

3.并发消费vs顺序消费下,消费者拉取到本地缓存(TreeMap)的消息,针对缓存(TreeMap)使用的差异?

并发消费时,消费者端将拉取的数据放入缓存,并使用pullResult中数据调用消费者监听消费,将消费失败的消息写回broker。不管消费成功与否,都将缓存中的对应的消息清理,并上报消费进度;

顺序消费时,消费者消息时,关注processQueue已锁定下,直接消费缓存中的消息,若消费不成功,将消费失败的消息再次放入缓存,并上报消费进度;消费次数超过16次,将写入死信队列,即认为是消息消费成功,并上报消费进度;

4. 消息者订阅的重试主题为%RETRY%+消费组名,重试时候,订阅消费组名下的所有topic?

ans:同一个消费组,不同消费者监听订阅信息要求相同,因为broker侧根据心跳,获取订阅信息是通过消费组获取的;若不同消费者订阅信息不同,则会出现相互覆盖。

同一个消费组内的消费者监听,可以订阅两个topic,那么在消费监听的内,根据topic的不同,进行不同逻辑。

5. consumer的listener数量 > broker的consumemequeue数量时,listener是如何分配broker队列的?抢占式?

3.RocketMQ消息队列分配算法,故如果消费者个数大于消息队列数量,则有些消费者无法消费消息。

6.新生成的内存映射文件,为何需要预热?见预热

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值