一、背景
在RocketMQ的使用过程中,偶尔会遇到当前Group下的消息订阅关系不一致的问题,当出现订阅关系不一致时,可能会出现消息丢失、堆积等问题。下文对出现此问题的原因进行详细解析。
二、原因解析
1、官方文档解析
为什么会出现订阅关系不一致呢?官方解释订阅关系一致指的是同一个消费者Group ID下所有Consumer实例所订阅的Topic、Tag必须完全一致。如果订阅关系不一致,消息消费的逻辑就会混乱,甚至导致消息丢失。
对于大多数分布式应用来说,同一个消费者Group ID往往会挂载多个Consumer实例,而RocketMQ的订阅关系是由Topic+Tag共同组成的,因此,想要订阅关系一致,相同的Group ID下所有的Consumer实例必须满足以下几点:
- 订阅的Topic必须相同。举个栗子:Consumer1订阅了TopicA、TopicB,Consumer2也必须订阅TopicA和TopicB,不能只TopicA或者只订阅TopicB或者订阅TopicB、TopicC。
- 订阅的同一个Topic中的Tag必须一致,包括Tag的数量和顺序。举个栗子:Consumer1订阅了TopicA,对应的Tag是Tagα|Tagβ,Consumer2订阅TopicB的Tag也必须是Tagα|Tagβ。
2、正确的订阅关系示意图例子
a)、订阅一个Topic且订阅一个Tag。
如上图所示,Group ID1下的3个Consumer实例C1、C2、C3都订阅了TopicA,并且Tag都是Tag1,这样符合订阅关系一致原则。
b)、订阅一个Topic且订阅多个Tag
如上图所示,同一Group ID下的三个Consumer实例C1、C2和C3分别都订阅了TopicB,订阅TopicB的Tag也都是Tag1和Tag2,表示订阅TopicB中所有Tag为Tag1或Tag2的消息,且顺序一致都是Tag1||Tag2,符合订阅关系一致性原则。
c)、订阅多个Topic且订阅多个Tag
如上图所示,同一Group ID下的三个Consumer实例C1、C2和C3分别都订阅了TopicA和TopicB,且订阅的TopicA都未指定Tag,即订阅TopicA中的所有消息,订阅的TopicB的Tag都是Tag1和Tag2,表示订阅TopicB中所有Tag为Tag1或Tag2的消息,且顺序一致都是Tag1||Tag2,符合订阅关系一致原则。
3、不正确的订阅关系示意图例子
a)、同一Group ID下的Consumer实例订阅的Topic不同
从上图可以看出,同一Group ID下的三个Consumer实例C1、C2和C3分别订阅了TopicA、TopicB和TopicC,订阅的Topic不一致,不符合订阅关系一致性原则。
b)、同一Group ID下的Consumer实例订阅的Topic相同,但订阅Topic的Tag不同
上图可以明显看到,同一Group ID下的三个Consumer实例C1、C2和C3分别都订阅了TopicA,但是C1订阅TopicA的Tag为Tag1,C2和C3订阅的TopicA的Tag为Tag2,订阅同一Topic的Tag不一致,不符合订阅关系一致性原则。
c)、同一Group ID下的Consumer实例订阅的Topic及Topic的Tag都相同,但订阅的Tag顺序不同
从上图可以看到,同一Group ID下的三个Consumer实例C1、C2和C3分别都订阅了TopicA和TopicB,并且订阅的TopicA都没有指定Tag,订阅TopicB的Tag都是Tag1和Tag2,但是C1订阅TopicB的Tag为Tag1||Tag2,C2和C3订阅的Tag为Tag2||Tag1,顺序不一致,不符合订阅关系一致性原则。
4、具体原理解析
上述讲了导致订阅关系不一致的规则,那么为什么订阅关系不一致就会导致消息丢失呢?先看下图的CommitLog和ConsumeQueue关系:
从上图可以看出ConsumeQueue包含的内容如下:
- 前8个字节存记录消息在CommitLog的偏移量。
- 中间4个字节记录当前消息的大小。
- 最后8个字节记录消息中tag的hashcode。
tag的主要作用是用来过滤消息的,假设一个Consumer订阅了Topic1中的TagA,那么Consumer拉取消息时,首先从Name Server里获取订阅关系,得到当前Consumer订阅的所有tag的hashcode集合codeSet。每次从ConsumerQueue里获取一条消息,就会判断最后8个字节的tag hashcode是否在codeSet里,如果不在codeSet里,那么就会被过滤掉。
具体例子如下图所示。Group ID 1消费 Topic1 中的消息时,Consumer1 通过 ConsumeQueue1 进行消费,Consumer2通过 ConsumeQueue2 和 ConsumeQueue3进行消费,如果 Consumer1 订阅了 Tag1,Consumer2 订阅了Tag2,那Consumer1从 ConsumeQueue1消费消息时,就会把 Tag2 中的消息过滤掉,这样即使 Consumer2 订阅了Tag2,也不能消费到 ConsumeQueue1里Tag2中的消息了。
四、解决方案
当出现MQ消费未达到预期的效果时,首先需要查看业务日志是否对消息进行了正常的消费,如果正常消费可能就是业务异常,那么可以通查看日志或者debug解决。
如果MQ消息并未正常消费,则需要去MQ管理后台查看消费轨迹或者确认是否有堆积情况。如果消息有堆积,考虑是否因为消息太多或者消费端线程池打满等压力过大导致消费进度缓慢,也可以查看是否因为订阅关系不一致导致的问题。如果出现消息订阅关系不一致,可以根据上述不正确的订阅关系情况进行排查及解决。
五、总结
本文先介绍背景,分析官方文档中的一些场景,包括正确和错误的订阅关系,然后对出现订阅关系不一致进行原来解析,最后对MQ消息出现问题的场景做出应对解决方案。