本文深入的分析了RocketMQ的Rebalance机制,主要包括以下内容:
-
Rebalance必要的元数据信息的维护
-
Broker协调通知机制;
-
消费者/启动/运行时/停止时Rebalance触发时机
-
单个Topic的Rebalance流程
-
分区分配策略
-
RocketMQ与Kafka Rebalance机制的区别,
Rebalance(再均衡)机制指的是:将一个Topic下的多个队列(或称之为分区),在同一个消费者组(consumer group)下的多个消费者实例(consumer instance)之间进行重新分配。
Rebalance机制本意是为了提升消息的并行处理能力。例如,一个Topic下5个队列,在只有1个消费者的情况下,那么这个消费者将负责处理这5个队列的消息。如果此时我们增加一个消费者,那么可以给其中一个消费者分配2个队列,给另一个分配3个队列,从而提升消息的并行处理能力。如下图:
但是Rebalance机制也存在明显的限制与危害。
Rebalance限制:
由于一个队列最多分配给一个消费者,因此当某个消费者组下的消费者实例数量大于队列的数量时,多余的消费者实例将分配不到任何队列。Rebalance危害:
除了以上限制,更加严重的是,在发生Rebalance时,存在着一些危害,如下所述:-
消费暂停:考虑在只有Consumer 1的情况下,其负责消费所有5个队列;在新增Consumer 2,触发Rebalance时,需要分配2个队列给其消费。那么Consumer 1就需要停止这2个队列的消费,等到这两个队列分配给Consumer 2后,这两个队列才能继续被消费。
-
重复消费:Consumer 2 在消费分配给自己的2个队列时,必须接着从Consumer 1之前已经消费到的offset继续开始消费。然而默认情况下,offset是异步提交的,如consumer 1当前消费到offset为10,但是异步提交给broker的offset为8;那么如果consumer 2从8的offset开始消费,那么就会有2条消息重复。也就是说,Consumer 2 并不会等待Consumer1提交完offset后,再进行Rebalance,因此提交间隔越长,可能造成的重复消费就越多。
-
消费突刺:由于rebalance可能导致重复消费,如果需要重复消费的消息过多;或者因为rebalance暂停时间过长,导致积压了部分消息。那么都有可能导致在rebalance结束之后瞬间可能需要消费很多消息。
|
1 队列信息变化 |
典型场景:
|
|
2 消费者组信息变化 |
典型场景:
|
这些管理器,内部实现都是一个Map。其中:
-
队列信息:由TopicConfigManager维护。Map 的key是Topic名称,Value是TopicConfig。Broker通过实时的或者周期性的上报自己的Topic配置信息给NameServer,在NameServer组装成Topic的完整路由信息。消费者定时向NameServer定时拉取最新路由信息,以实现间接通知,当发现队列信息变化,触发Rebalance。
-
消费者组信息:由ConsumerManager、ConsumerOffsetManager、SubscriptionGroupManager三者共同维护。ConsumerManager维护了消费者组订阅信息,以及消费者组下当前的消费者实例信息,当消费者组的订阅信息或者实例发生变化,Broker都会主动给所有消费者实例发送通知,触发Rebalance。而在Rebalance时,消费者需要从ConsumerOffsetManager查询应该从那个位置继续开始消费。SubscriptionGroupManager主要是维护消费者组的一些附加信息,方便运维。
2.1 队列信息变化
队列信息通过Broker内的TopicConfigManager来维护,每个Broker都会将自己的信息上报给NameServer,由NameServer组装成完整的Topic路由信息。 通常情况下,一个Topic下的队列数量不会频繁的变化,但是如果遇到,Topic队列数量扩/缩容,、broker日常运维时的停止/启动或者broker异常宕机,也有可能导致队列数量发生变化。 这里我们重点讲一下为什么broker异常停止/宕机会导致数量变化。一些读者可能会认为创建Topic时,已经明确指定了队列的数量,那么之后不论怎样,队列的数量信息都不会发生变化,这是一种典型误解。 下图展示了一个RocketMQ集群双主部署模式下,某个broker宕机后,Topic路由信息的变化。2.2 消费者组信息变化
Rebalance的另外一个条件:消费者组信息,Broker端通过以下三个组件共同维护:-
ConsumerManager:维护消费者实例信息和订阅信息
-
C onsumerOffsetManager:维护offset进度信息
-
SubscriptionGroupManager:运维相关操作信息维护
数据添加:
客户端通过发送HEART_BEAT请求给Broker,将自己添加到ConsumerManager中维护的某个消费者组中。需要注意的是,每个Consumer都会向所有的Broker进行心跳,因此每个Broker都维护了所有消费者的信息。数据删除:
客户端正常停止时,发送UNREGISTER_CLIENT请求,将自己从ConsumerManager移除;此外在发生网络异常时,Broker也会主动将消费者从ConsumerManager中移除。数据查询:
消费者可以向任意一个Broker发送GET_CONSUMER_LIST_BY_GROUP请求,来获得一个消费者组下的所有消费者实例信息。 我们可以通过mqadmin命令行工具的consumerConnection子命令,来查看某个消费者的信息,如:输出主要分为2个部分:
-
消费者组实例信息:展示了groupA下当前有2个消费者,以及对应的详细信息,包括:消费者id,消费者ip/port,消费者语言,消费者版本。
-
消费者组订阅信息:包括订阅的Topic,过滤条件,消费模式,以及从什么位置开始消费等。
-
Kafka:会在消费者组的多个消费者实例中,选出一个作为Group Leader,由这个Group Leader来进行分区分配,分配结果通过Cordinator(特殊角色的broker)同步给其他消费者。相当于Kafka的分区分配只有一个大脑,就是Group Leader。
-
RocketMQ:每个消费者,自己负责给自己分配队列,相当于每个消费者都是一个大脑。
此时,我们需要思考2个问题:
问题1:每个消费者自己给自己分配,如何避免脑裂的问题呢?
因为每个消费者都不知道其他消费者分配的结果,会不会出现一个队列分配给了多个消费者,或者有的队列分配给了多个消费者。问题2:如果某个消费者没有收到Rebalance通知怎么办?
每个消费者都会定时触发Rebalance,以避免Rebalance通知丢失。2.2.2 ConsumerOffsetManager
事实上,通过ConsumerManager已经可以获得Rebalance时需要的消费者所有必要信息。但是还有一点,Rebalance时,如果某个队列重新分配给了某个消费者,那么必须接着从上一个消费者的位置继续开始消费,这就是ConsumerOffsetManager的作用。 消费者发送UPDATE_CONSUMER_OFFSET请求给Broker,来更新消费者组对于某个Topic的消费进度。发送QUERY_CONSUMER_OFFSET请求,来查询消费进度。 通过mqadmin命令行工具的consumerProgress子命令,来可以看到Topic每个队列的消费进度,如:2.2.3 SubscriptionGroupManager 订阅组配置管理器,内部针对每个消费者组维护一个SubscriptionGroupConfig。主要是为了针对消费者组进行一些运维操作,这里不做过多介绍,感兴趣的读者自行查阅源码。 3 Consumer Rebalance触发时机 前面分析Broker在Rebalance过程中起的是协调通知的作用,可以帮忙我们从整体对Rebalance有个初步的认知。但是Rebalance的细节,却是在Consumer端完成的。 在本节中,我们将着重讨论单个consumer的Rebalance流程。 需要说明的是,RocketMQ的consumer分配pull和push两种模式,二者的工作逻辑并不相同。这里主要以push模式的默认实现类DefaultMQPushConsumer为例进行讲解。 在前文,我们提到Broker会主动通知消费者进行Rebalance,但是从消费者的角度来看,整个生命过程的各个阶段,都有可能触发Rebalance,而不仅仅是收到通知后才进行Rebalance。 具体来说,Consumer在启动/运行时/停止时,都有可能触发Rebalance,如下图所示:
-
在启动时,消费者会立即向所有Broker发送一次发送心跳(HEART_BEAT)请求,Broker则会将消费者添加由ConsumerManager维护的某个消费者组中。然后这个Consumer自己会立即触发一次Rebalance。
-
在运行时,消费者接收到Broker通知会立即触发Rebalance,同时为了避免通知丢失,会周期性触发Rebalance;
-
当停止时,消费者向所有Broker发送取消注册客户端(UNREGISTER_CLIENT)命令,Broker将消费者从ConsumerManager中移除,并通知其他Consumer进行Rebalance。
DefaultMQPushConsumerImpl的start方法显示了一个消费者的启动流程,如下图所示:
可以看到Consumer启动主要分为5个步骤,其中步骤2、4、5是我们分析的重点。:
-
步骤1:启动准备工作,这里使用{...}表示省略,以更清楚看清整个流程
-
步骤2:从nameserver更新topic路由信息,收集到了Rebalance所需的队列信息
-
步骤3:检查consumer配置(主要是为了功能兼容,例如consumer要使用SQL92过滤,但是broker并没有开启,则broker会返回错误)
-
步骤4:向每个broker发送心跳信息,将自己加入消费者组
-
步骤5:立即触发一次rebalance,在步骤2和4的基础上立即触发一次Rebalance
步骤2 :更新订阅的topic路由信息
上述代码步骤2,调用updateTopicSubscribeInfoWhenSubscriptionChanged()方法,从NameServer更新topic路由信息,由于一个消费者可以订阅多个topic,因此这个Topic都需要更新,如下:步骤4 向broker发送心跳信息
在上述启动流程中的第4步,调用sendHeartbeatToAllBrokerWithLock方法,给每个Broker都发送一个心跳请求。this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
当Broker收到心跳请求后,将这个消费者注册到ConsumerManager中,前文提到,当Consumer数量变化时,Broker会主动通知其他消费者进行Rebalance。
而心跳的数据,这些数据是在MQClientInstance类的prepareHeartbeatData方法来准备的。我们在前文通过mqadmin命令行工具的consumerConnection 自命令查看到的消费者订阅信息,在这里都出现了,如下图红色框所示:
步骤5:立即触发一次Rebalance
消费者启动流程的最后一步是调用以下方法立即触发一次rebalance:this.mQClientFactory.rebalanceImmediately();
这个方法内部实际上,是通过唤醒一个RebalanceService,来触发Rebalance:
public void rebalanceImmediately() { this.rebalanceService.wakeup(); }
这里我们并不着急分析RebalanceService的内部具体实现,因为所有的Rebalance触发都是以这个类为入口,我们将在讲解完运行时/停止时的Rebalance触发时机后,统一进行说明。
3.2 运行时触发
消费者在运行时,通过两种机制来触发Rebalance:
-
监听broker 消费者数量变化通知,触发rebalance
-
周期性触发rebalance,避免Broker的Rebalance通知丢失。
下面分别进行说明:
1 监听broker 消费者数量变化通知,触发rebalance
RocketMQ支持双向通信机制,在客户端通过ClientRemotingProcessor的processRequest方法来处理Broker发起的通知请求,如下:2 周期性触发rebalance,避免Rebalance通知丢失
为了避免Broker的Rebalance通知丢失问题,客户端还会通过RebalanceService定时的触发Rebalance,默认间隔是20秒,如下图:3.3 停止时触发
最后,消费者在正常停止时,需要调用shutdown方法,这个方法的工作逻辑如下所示:4 Consumer Rebalance流程
前面花了大量的篇幅,讲解了Rebalance元数据维护,Broker通知机制,以及Consumer的Rebalance触发时机,目的是让读者有一个更高层面的认知,而不是直接分析单个Consumer Rebalance的具体步骤,避免一叶障目不见泰山。 4.1 Rebalance流程整体介绍 不同的触发机制最终底层都调用了MQClientInstance的doRebalance方法,而在这个方法的源码中,并没有区分哪个消费者组需要进行Rebalance,只要任意一个消费者组需要Rebalance,这台机器上启动的所有其他消费者,也都要进行Rebalance。相关源码如下所示:MQClientInstance#doRebalance
-
DefaultMQPushConsumerImpl
-
DefaultMQPullConsumerImpl
RebalanceImpl#doRebalance
-
1 获得Rebalance元数据信息
-
2 进行队列分配
-
3 分配结果处理
如以下源码截图红色框中所示:
下面对每一个步骤进行详细说明。
4.2.1 获得Rebalance元数据
消费者在Rebalance时需要获得:Topic的队列信息和消费者组实例信息。对于队列信息:
会从之前的缓存的Topic路由信息中获取;Topic路由信息会定时的进行更新。
对于消费者组实例信息:
前面我们提到过Broker通过ConsumerManager维护了所有的消费者信息,findConsumerIdList方法内部会会发送GET_CONSUMER_LIST_BY_GROUP给请求给任意一个Broker进行获取。
4.2.2 进行队列分配
RocketMQ的分配策略使用AllocateMessageQueueStrategy接口表示,并提供了多种实现:-
AllocateMessageQueueAveragely:平均分配,默认
-
AllocateMessageQueueAveragelyByCircle:循环分配
-
AllocateMessageQueueConsistentHash:一致性哈希
-
AllocateMessageQueueByConfig:根据配置进行分配
-
AllocateMessageQueueByMachineRoom:根据机房
-
AllocateMachineRoomNearby:就近分配
-
对Topic队列,以及消费者各自进行排序
-
每个消费者需要使用相同的分配策略。
sh bin/mqadmin allocateMQ -i ip1,ip2,ip3 -t TopicA -n localhost:9876
这个工具可以将模拟分配的结果进行json格式展示。
4.2.3
队列分配结果处理
处理队列变更
消费者计算出分配给自己的队列结果后,需要与之前进行比较,判断添加了新的队列,或者移除了之前分配的队列,也可能没有变化。-
对于新增的队列,需要先计算从哪个位置开始消费,接着从这个位置开始拉取消息进行消费;
-
对于移除的队列,要移除缓存的消息,并停止拉取消息,并持久化offset。
可参考updateProcessQueueTableInRebalance()实现。
其他处理
调用messageQueueChanged方法进行额外后续处理:对于push模式:
主要是进行一些流控参数的更新。
对于pull模式:
是回调用户自定义的MessageQueueListener。
长按二维码,关注我,加好友,进群交流