RocketMQ第5讲——消费者(Consumer)

前两篇文章我们已经知道了消息是如何存储和发送的,这篇我们就学习下消费者消费的一些细节。

消息是存储在Broker上的,那么消费者是如何获取的呢?是Broker主动推给消费者的还是消费者主动去Broker上拉的呢?

一、推(push)VS 拉(pull)

先举个例子:

平时我们一般都是网上购物,快递到货后,一般取件的方式有两种:

  • 送货上门:这种就比较舒服,但是假设你当时没在家,快递被偷了咋办?再者要是买的东西比较贵重,那你在外也会提心吊胆的。

  • 暂存快递柜:这种就需要我们多跑一趟去取,但好处是随时都可以去取,也不用担心被偷。

那么上述两种方式对应的就是推和拉,下面我们消息队列中的情况。

1.1 推消息

这种就是Broker一旦收到消息,不管三七二十一,立马就把消息推给消费者,那么优缺点也很明显。

优点:

  • 消费者啥也不用做,等消息上门就行了,实现起来比较简单。

  • 时效性很高,Broker一收到消息就立马推给消费者。

缺点:

  • 假设消费者现在已经很忙了,Broker还一直推消息,很有可能把消费者的CPU给干冒烟了,这也违背了消息队列的“削峰添谷”的初衷。

那么有同学可能会想到,那Broker在推消息之前可以先看下消费者的处理情况,处理的快就多推点,处理的慢就少推点,反正Broker已经持久化消息了。

理论上是可行的,但是消费者可能很多,每个消费者的消费速率又不一样,那么Broker就需要维护住每个消费者的情况,这就大大增加了Broker的复杂度了,性能也会下降。就好比快递小哥在送之前先打个电话询问在不在家,如果快递很多,那岂不是很累。

1.2 拉消息

就是“解放”Broker,消费者可以根据自身的情况主动去Broker拉消息,这种的话优缺点也很明显:

优点:

消费者就比较灵活,如果现在很忙,那就等会再拉,如果现在很闲,那么就去Broker拉消息,而且可以根据自身情况选择一次性拉取的数量。

缺点:

消息消费有可能会延迟,这种拉模式的话一般就是定时任务,比如1分钟拉一次,发现没有消息,此时恰巧有一条消息再消费者拉取请求后存储到了Broker,那么即使消费者此时空闲,刚到的那条消息也得等一分钟后才能被消费。

如果把定时任务的周期调短一点呢,比如1s拉一次?这就会带来另一个问题,就是消息忙请求,假设几个小时内都没消息来,那么消费者也要持续性去请求Broker尝试拉取消息,白白浪费资源。

1.3 RocketMQ的长轮询

企业级消息队列有用拉的也有用推的,比如ActiveMQ就是用的推模式,不过现在基本没啥人在用了,而Kafka和RocketMQ则用的是拉模式,不过这种拉模式不是上面的介绍的拉模式,而是变种的“拉”“,他有一个好听的名词叫”长轮询“

何为长轮询?

消费者发送拉去请求到Broker,如果此时有消息,则Broker直接响应返回消息,如果没消息就hold住这个请求,比如等15s,在15s内如果有消息就立马响应返回消息,如果没消息就返回无消息。

这样既避免了忙请求的情况,也进一步提升了消息的实时性,是不是很巧妙😄

ps:不仅kafka和RocketMQ采用这种方式来获取消息,Nacos1.x版本也是采用长轮询的方式进行数据交互的。

二、消费者负载均衡

根据官方,消费者负载均衡分为队列粒度和消息粒度的负载均衡,重点了解下队列粒度!

2.1 队列粒度(RocketMQ 3.x/4.x 默认)

我们知道Topic下有队列的概念,消费者实际是去Topic下的某个队列获取消息的,假设有4个队列,1个消费者,那么这个消费者就需要消费4个队列的消息:

如果此时消费者组A来了个消费者-2,那么此时4个队列就会分2个给新来的消费者,这就是所谓的重平衡。也叫客户端的负载均衡:

如果再来俩消费者呢?那么每个消费者只负责一个队列就好:

如果再来一个呢?重平衡后,新来的消费者还是空闲的,因为每个队列都已经有负责的消费者了,没有更多个队列给新来的消费者了:

这就涉及一个面试题:消息堆积时,增加消费者有用吗?

不一定。如果这个消费者组的消费者数量比Topic的队列数量小,那么增加消费者在一定程度上确实可以缓解消费堆积,如果此时消费者数量已经大于等于队列数量,那再加多少都是没用的。

那么重平衡触发的时机呢?

  • 消费者启动的时候,

  • 定时任务,每20s主动重平衡一下。

  • 如果有消费者下线了,由于消费者和Broker建立了连接,所以此时Broker也会通知所有消费者进行重平衡。

这样一来就实现了动态的负载均衡功能,可以灵活的操作消费者上下线。

不过这种模式在提升消费能力的同时,也带来一些问题:

  • 消费暂停:在只有一个消费者时,它负责所有队列;在新增一个消费者后会触发重平衡。此时原消费者就需要暂停部分队列的消费,等新的消费者分配好队列后,才能继续消费。

  • 消费重复:消费者在消费新的队列时,比如按照之前消费者的消费进度消费,在默认情况下这些offset是异步提交的,这就可能导致提交到Broker的offset跟实际消费进度不一致,从而导致重复消费。

  • 消费突刺:由于重平衡可能导致重复消费,如果需要消费的消息过多,或者因为重平衡暂停时间过长(可能达到秒级),就会导致消息积压,在重平衡后瞬间就需要消费很多消息。

2.2 消息粒度(RocketMQ 5.x 默认)

ps:这个可以简单了解一下,目前业界用的不多。

针对上述“队列粒度重平衡”机制导致的重复消费和消费延迟等问题,RocketMQ 5.0提出了消息粒度的负载均衡机制。

在这个策略中,同一个队列的消息可以被同一组内的多个消费者消费,服务端会确保消息不重不漏的被客户端消费到:

消息粒度的负载均衡机制,是基于内部的单条消息确认语义实现的,消费者在获取到某条消息后,服务端会将该消息加锁,保证这条消息对其它消费者不可见,直到该消息消费成功或消费超时

因此,即使多个消费者同时消费同一队列的消息,服务端也可以保证消息不会被多个消费者重复消费。

同时RocketMQ在消息维度的负载均衡的基础上也实现了顺序消费的语义:不同消费者处理同一个消费组内的消息时,会严格按照先后顺序锁定消息状态,确保同一消费组的消息串行消费:

如上图所述,队列Queue1中有4条顺序消息,这4条消息属于同一消息组G1,存储顺序由M1到M4。在消费过程中,前面的消息M1、M2被消费者Consumer A1处理时,只要消费状态没有提交,消费者A2是无法并行消费后续的M3、M4消息的,必须等前面的消息提交消费状态后才能消费后面的消息。

三、如何拉消息

重平衡后,每个消费者都知道它负责了哪个队列,下面的任务就是去Broker拉取对应队列的消息就行,那么具体如何拉呢?

最简单的办法就是一个队列启一个线程去拉,但是如果队列很多,那么对应的线程就很多,我们知道频繁的线程上下文切换是有损耗的,而且重平衡机制会使得消费者者负责的队列数量也变化,队列变,那么对应的线程也会变,这时候就得上线程池了。

但实际不用那么麻烦。

RocketMQ只用了一个线程(PullMessageService)来执行拉消息的操作,所有拉取消息的动作都被封装成PullRequest,然后扔到pullRequestQueue这个阻塞队列里面:

 然后PullMessageService会不断地从pullRequestQueue拉取PullRequest,最后根据pullRequest里面的内容构建请求去对应的Broker里拉消息:

pullRequest里面包含消费者组的信息、消费点位、Topic、队列id等,当上一个pullRequest拉取到结果后,里面会根据响应构建新的pullRequest塞到pullRequestQueue里,以此来达到消息的不间断拉取。

四、消息位点(offset)

前面文章我们提到过,每个消费者组都要维护Topic下的每个队列被消费的点位,那么这个点位该怎么保存呢?

在不同模式下,保存的方式不同:

  • 广播模式:消费点位就存储在消费者本地磁盘上,因为广播模式是将消息广播给每个消费者,它不需要有个统一的地方来管理这个位置,每个消费者自己维护就行。

  • 集群模式:这就不能本地维护了,因为集群模式下,同个消费者组内的消费者是“互帮互助”的关系,如果某个消费者下线了,就需要另一个消费者顶上,这时候顶上来的消费者就需要知道下线的消费者的消费进度,所以在集群模式下,消费点位是存储在Broker上的,这样新顶上来的消费者就可以从Broker来获取消费点位。

那消费者是如何告诉Broker自己的消费点位的呢?

其实也简单,就是去拉消息的时候会顺带把此时的消费点位提交给Broker,这样Broker就能得知最新的消费进度了。但是有一点需要提一下:那假设消费完了,还没来得及去拉消息(最新的消费点位没提交给Broker),消费者就挂了,那么此时如果发生重平衡,新的消费者顶上来,就会导致重复消费消息!

所以,消费者也无法保证消息只会被消费一次,这里也是重复消费的问题,我们能做的就是做好消费者端的幂等

五、消费消息

消费者是如何消费消息的呢?

有两种,一种是并发消费,另一种则是顺序消费,一般情况下都是并发消费。那假设消费失败了咋办?重试!重试还是失败呢?如果一直重试失败就会阻塞住后面的消息了。

所以RokcetMQ的实现是将消费失败的消息,返回发送给Broker,这个消息会被发送到特定的重试Topic:“%RETRY%+consumerGroup,这样就不会阻塞原Topic上的其它消息了;当然如果重试多次还是失败的话,就会把它打入到死信队列中,这时候就需要人工介入去处理这些消息了。

ps:默认16次重试失败后,打入死信队列。

 End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。

### RocketMQ 消息持久化机制与流程详解 #### 1. CommitLog 文件的作用 当生产者发送消息至 Broker 后,Broker 将这些消息追加写入到 `CommitLog` 文件中。每个 `CommitLog` 文件大小固定为大约 1GB 左右[^2]。 ```python # Python伪代码展示消息写入过程 def write_message_to_commitlog(message): with open('commitlog', 'a') as commit_log_file: commit_log_file.write(message.serialize()) ``` #### 2. ConsumeQueue 的作用 为了提高消费者读取消息的速度,RocketMQ 设计了一种索引结构——`ConsumeQueue`。每当有新消息被写入 `CommitLog` 中时,会同步更新对应的 `ConsumeQueue` 条目。这样做的好处是可以让 Consumer 更加快捷地找到所需的消息位置而无需遍历整个 `CommitLog` 文件。 每条记录只占用 20 字节空间,其中包括物理偏移量 (8B),消息长度 (4B), Tag HashCode(4B) 和时间戳 (4B): | 物理偏移量 | 消息长度 | Tag HashCode | 时间戳 | |------------|----------|--------------|--------| | 8 | 4 | 4 | 4 | #### 3. IndexFile 提供更高效的查询支持 除了上述两种文件外,还有第三类重要的元数据文件叫做 `IndexFile`。这类文件用于加速基于 Key 或 Timestamp 进行查找操作。通过哈希表的方式保存 key-value 对应关系以及时间范围内的映射信息,使得即使面对海量数据也能保持较低的时间复杂度完成检索工作。 #### 4. 完整的消息持久化进程 综上所述,完整的 RocketMQ 消息持久化流程如下所示: 1. 生产者向服务端提交一批次或多批次消息; 2. Broker 接收到这批消息后将其顺序追加到当前正在使用的 `CommitLog` 文件末尾; 3. 更新相应的 `ConsumeQueue` 表项来反映新增加的内容; 4. 如果启用了索引功能,则还会创建或修改对应主题下的 `IndexFile`; 5. 当前日志达到设定容量上限时自动切换到下一个新的 `CommitLog` 实例继续执行相同的操作; 以上就是关于 RocketMQ 如何实现高效可靠的消息持久化的详细介绍.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橡 皮 人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值