深入浅出消息队列----【订阅关系一致性】
本文仅是文章笔记,整理了原文章中重要的知识点、记录了个人的看法
文章来源:编程导航-鱼皮【yes哥深入浅出消息队列专栏】
什么是订阅关系
所谓的订阅关系,讲白了就是记录某个 consumer 订阅了某主题(含过滤条件如 tag),然后 consumer 去 Broker 拉取消息的时候,根据订阅关系就能拉取到正确的消息。
实际生产上,都是由多个 consumer 组成消费组来消费消息的,这样一来对不熟悉订阅关系或者说不了解原理的人,在使用上就很容易出现错误。
RocketMQ 中,订阅关系是按照消费组和主题 + tag 粒度设计的,也就是说一个订阅关系=某消费组+某主题+某tag。
举个栗子,一个 Topic-A,两个 ConsumerGroup 1 和 2。
ConsumerGroup-1 订阅了 Topic-A、Tag-1,这是一个订阅关系。
ConsumerGroup-2 订阅了 Topic-A、Tag-2,这是一个订阅关系。
这两个订阅关系之间没有关联,它们也不会相互影响。
再举个例子,比如现在有一个 Topic-A、Topic-B,一个 ConsumerGroup 1。
ConsumerGroup-1 订阅了 Topic-A、Tag-1,这是一个订阅关系。
ConsumerGroup-1 也订阅了 Topic-B、Tag-2,这是一个订阅关系。
这两个订阅关系之间没有关联,它们也不会互相影响。
这就是我上面说的订阅关系是按照消费组和主题+tag 粒度设计的,即一个订阅关系 = 某消费组 + 某主题 + 某 tag。
什么是订阅关系一致性
既然两个订阅关系之间是没有关联的,那么哪来的什么一致性呢?
这里的一致性其实指代的是同一个消费组内不同消费者对同一个订阅关系的一致性问题。
下面两个常见的订阅关系不一致错误:
比如现在有一个 Topic-A、Topic-B,一个 ConsumerGroup-1,里面有 consumer-1、consumer-2 两个消费者。
consumer-1 订阅了 Topic-A。
consumer-2 订阅了 Topic-B。
这就发生了一个消费组内不同消费者订阅了不同 Topic 的问题。
我们仅从定义上的理解就感觉这样是不对的,因为订阅关系是以消费组和主题 + tag 为粒度的,而不是以某个消费者为粒度的。
组里面不同消费者的订阅都不一致,消费者-1 想要 TopicA 的消息,而不想要 TopicB 的消息,消费者-2 则反过来,且消费又是以组的维度去消费的消息的,组内都没统一意见,如何对外顺利地服务?
这就是所谓的订阅关系不一致!
除了不同 Topic 的问题,Tag 过滤不一致的问题也是初学者常犯的一个错误。
比如现在有一个 Topic-A,分别有 Tag-1、Tag-2,一个 ConsumerGroup-1,里面有 consumer-1、consumer-2 两个消费者。
consumer-1 订阅了 Topic-A、Tag-1。
consumer-2 订阅了 Topic-A、Tag-2。
一个消费组内不同消费者虽然订阅了同样的 Topic,但是 Tag 不一样,这也发生了订阅关系不一致!
所以,订阅关系一致性指的是同一个消费者组下所有消费者实例所订阅 Topic、Tag 必须完全一致。
如果发送订阅关系不一致会怎样?
可能会导致报错、消息不被消费甚至消息的丢失。
为什么订阅关系不一致会产生问题?
消费组内的消费者们是合力来消化整个消费组的消息,它们之间的关系是互帮互助。
那为什么不能让组内的消费者-1 仅消费 Topic-A,消费者-2 仅消费 Topic-B 呢?
这样不也是互帮互助了吗?
简单举例,假设 Topic-A 每天百万消息量,Topic-B 每天 10 条消息,这样来看能算互帮互助吗?
消费者-2 就是个打酱油的!
所以 RocketMQ 的设计当然需要尽可能地满足负载均衡,而不是简单的用 Topic 来划分负责的工作。
回到订阅关系一致性问题上来,要理解为什么会产生问题,需要明白 RocketMQ 的心跳、消息拉取、重平衡等机制的原理。
心跳机制
消费者会定时上报自己的订阅关系给 Broker,而 Broker 是以消费组为单位保存这个订阅关系。
消息拉取
消费者会根据自己的订阅关系,定时地构建拉取请求,去 Broker 拉取对应的消息。
重平衡机制
消费者会定时重平衡,即客户端负载均衡,也及时瓜分整个消费组负责的 Topic 对应的队列。
重平衡会获取 Topic 下的所有队列,比如现仅订阅了一个 Topic,下面有队列-1、队列-2、队列-3、队列-4,紧接着获取消费组下的消费者信息,比如消费者-1,消费者-2。
然后排个序:
排好序后,就平分:
这样消费者-1 只会去拉取队列1、2 的消息,消费者-2 只会去拉取队列-3、4,从而达到负载均衡。
这个操作每 20s 就会发生一次。
回顾好了,紧接着我们来分析订阅关系不一致可能会出现的问题。
报错
消费者的订阅关系会随着心跳定时上报给 Broker。
同样举个例子,现有一个 Topic-A、Topic-B,一个 ConsumerGroup-1,里面有 consumer-1、consumer-2 两个消费者。
consumer-1 订阅了 Topic-A。
consumer-2 订阅了 Topic-B。
consumer-1 发送心跳给 Broker,Broker 一看,ConsumerGroup-1 订阅了 Topic-A。
紧接着 consumer-2 也发送心跳给 Broker,Broker 一看,ConsumerGroup-1 订阅了 Topic-B,此时要注意,发生了覆盖,也就是之前 consumer-1 心跳里的订阅关系,被这次心跳覆盖了。
究其原因,因为 Broker 是以消费组为单位来存储订阅关系的,可以理解订阅关系的存储就是一个 map,更新订阅关系就是 ,map.put (消费组, 订阅关系),这样就会发生覆盖。
然后这时候 consumer-1 构建了 Topic-A 的拉取请求,Broker 一查订阅关系,好家伙,你没订阅 Topic-A 啊,来拉取 Topic-A 的关系干啥?
于是就报错来了。
消息不被消费
我们都知道,消费者实际是取消费 Topic 下的每个队列的消息,而队列又会因为重平衡分配各消费者内不同的消费者。
顺延上面的例子,假设 Topic-A 下面有 4 个队列,那么按照默认的重平衡算法,consumer-1、consumer-2 会各自被分配到两个队列。
问题来了,consumer-1 订阅了 Topic-A、consumer-2 订阅了 Topic-B。
而 consumer-1 仅能拉取队列-1、2 的消息,队列 3、4 的消息它无法获取。
而 consumer-2 没订阅 Topic-A,因此它不会去拉取消费 Topic-A 队列3、4 的消息。
这就导致部分 Topic-A(队列3、4)的消息不被消费,因为它被 consumer-2 占着茅坑不拉屎。
消息的丢失
消息的丢失跟消息不消费不一样。
消息不消费的例子中,只要 consumer-2 重启,再还没上线的时候,Topic-A 队列3、4就有机会被重平衡到 consumer-1,然后被消费掉,本质上不消费其实就是消息存在队列中,没有被拉取消费。
而丢失,就是丢了,即拉取到了,跳过了,不消费。
拿 Topic-A 举例,此时有 Tag-1、Tag-2,同样还是 ConsumerGroup-1,里面有 consumer-1、consumer-2 两个消费者。
consumer-1 订阅了 Topic-A,Tag-1。
consumer-2 订阅了 Topic-A,Tag-2。
同样重平衡分配后,各自消费的队列如下图:
consumer-1 负责两个队列没问题,但问题是它不要 tag-2 的消息,于是乎,队列1、2中所有 tag-2 的消息会被过滤,也就是被丢失了。
同理 consumer-2 不要 tag-1 的消息,队列3、4中所有 tag-1 的消息会被过滤,也被丢了。
这就造成了消息的丢失。
因此,订阅关系不一致导致的问题很严重,在使用中,必须要保持订阅关系的一致性。