1、背景
最近一周一来,总是有项目组中反馈遇到了消息重复推送的问题,连续出现单条消息短时被多个消费者消费的问题:
同时给出了证据,相同的msgId的消息被打印了两条,并且两者相隔7s。
讲真,由于最近负责的事情比较多,并且RocketMQ又无法保证消息被重复消费,所以一开始并未引起我的充分重视,而是简单的阐述了RocketMQ的设计原理不保证重复消费,需要消费端实现幂等。
但就是这个疏忽,后面集中爆发,整个集群中消费组消费绝大部分都出现了严重的抖动,造成消费组频繁积压,开启了一波紧张的问题排查与线上止血操作,同时并引发了我寻根究底之旅。
关于为什么会出现整个MQ集群消费组消费抖动,在这里先丢一个悬念,本文想和大家先来聊聊RocketMQ哪些设计会导致消息消费的重复消费。
2、探究RocketMQ重复消费
在RocketMQ的设计理念中,消息发送、消息消费端的相关机制都会带来消息重复消费,接下来我们分别探讨。
2.1 消息发送时重试造成消息重复发送
在RocketMQ中,由于消息发送者无法实时感知Broker路由变化,为了保证消息发送的高可用性,RocketMQ在消息发送时引入了重新机制,消息在客户端的发送简要流程如下图所示:
也就是如果发送消息的过程中,如果一台broker发送抖动导致消息发送超时,发送超时并不代表消息没有成功写入到Broker,也有可能是写入到Broker耗时比较长罢了,但客户端等不及,判定发生失败,则尝试再找另外一台Broker发送一次消息,这样就会导致发送了两条消息,并且这两条消息的msgId,消息体都一样,但在消息集群中存在了两条消息,也就是这两条消息的msgId相同,但offsetMsgId不同。
在这里我普及一下RocketMQ中两个消息id的含义:
- msgId 消息id,由发送者创建,全局唯一
- offsetMsgId 消息偏移,由Broker创建,记载了服务端的地址与消息的物理偏移量。
如果出现消息发送者发送了两条消息,就会出项文章开头那种情况,日志中会出现两条相同的日志。
那进一步,那如何判断出现消息消费重复是否是消息发送端的问题呢?
其实非常简单,我们只需要在打印消息体的日志中再增加如下三个属性就可以轻而易举的进行判别,这些属性如下:
- brokerName 消息存储的broker的名称
- queueId 消息消费队列ID
- queueOffset 消息消费队列偏移量
如果打印出来的消息,这几个参数都相同,那就可以断定是消息发送时重复发送了消息。
2.2消息消费时重复消费
介绍完消息发送时可能造成重复消费后,我们再来探究一下消息消费端是否会造成重复消费。
纵观我对RocketMQ的理解,RocketMQ消息消费时造成消息消费重复主要有如下三个原因:
- 最小位点提交机制
- 消费位点批量提交
- 消费组重平衡
接下来我们简单说明一下,详细的解读建议参考我最近新上市的书籍《RocketMQ实战》,目前5折优惠。
2.2.1最小位点提交机制
RocketMQ消息消费时采取了最小位点提交机制,说明如下图所示:
例如消息消费线程池中的三个消费线程t1,t2,t3分别在处理消息偏移量1,2,3的消息,由于是并发消费,如果t3线程将msg3先消费完成,此时向服务端提交位点,是上报msg3的偏移量为最新的消费进度吗?
显然是不行的,因为一旦将3设置为最新的位点,但此时偏移量1,2的消息还未成功消费,一旦消费者重启,那下一次将从偏移量为3的消息开始消费,这样msg1,msg2这两条消息将丢失。
所以RocketMQ为了确保消息消费端不发生消息丢失,采取了最小位点提交机制,也就是虽然线程t3将消息3消费完成,它要做的是在处理队列中首先将msg3移除,然后选择处理队列中最小的偏移量作为位点进行上报,也就是t3线程消费完msg3后,会向服务端提交msg1的偏移量。
这样也会带来另外一个问题,消息重复消费,例如t3将msg3消费完成后,提交的位点是msg1的位点,如果消费者重启,则会继续从msg1开始消费,msg3就会再次被消费。
消息重复消费,消息丢失,两害取其轻,确保消息不丢失是头等大事,估在设计上就必然会出现消息重复消费。
2.2.2 消费为位点批量提交机制
消息这在消费完消息后,需要向服务端汇报位点,但如果每消费完一条消息,就向服务端发送位点提交请求,服务端性能肯定抗不住,故RocketMQ为了确保服务端的稳定性,采取了批量提交位点的机制,具体如下图所示:
客户端会首先在本地缓存位点信息,然后每隔5s向服务端发起一次位点提交,服务端收到客户端位点提交机制后,只是会更新内存中的我点数据,然后每隔5s将内存中的数据持久到磁盘中。
由于位点信息同步的延迟,如果客户端重启,也会造成一部分位点信息丢失,同样会出现重复消费。
2.2.3 重平衡导致消费重复消费
RocketMQ消费者在消费之前,首先需要对队列进行负载均衡,RocketMQ进行队列负载的一个基本原则:一个队列在同一个时间只会分给一个消费者,一个消费者可以消费多个队列,当消费者个数或者队列个数发生变化时,就会触发重平衡,示意图如下所示:
首先,q3队列是由c2负载消费的,但当新增一个消费者时,q3队列分配给了c3,也就是c2消费者在处理q3一部分消息后,该队列被丢弃,位点提交失败,这样部分消息在c3消费者上又被消费一次,同样造成重复消费。
3、总结
关于RocketMQ造成消息重复消费的几种情况进行了一一解读,那亲爱的读者朋友,你可以思考一下,集群大面积消息重复,消息消费抖动,你觉得会是哪种原因造成的呢?