1 负载均衡实现组件分析
1.1 负载均衡整体概述
-
概述
所谓的负载均衡指的就是,在集群消费模式下,一个消费组里面有多个消费者订阅了一个主题,此主题有多个消息队列(MessageQueue),负载均衡组件就将这些消息队列平均分给消费组里面的消费者。 -
使用注意点
同一个消费组内的消费者要保持一样主题订阅,否则会导致某些消费队列没有消费者进行消费。 -
组件分析(核心类和策略类)
负载均衡只对集群消费模式的消费者有效,对广播模式的消费者无效的。RocketMQ的负载均衡是在消费者客户端完成的。
首先抛开其他复杂的关系调用,逻辑,单纯的从负载均衡实现组件上来分析。负载均衡的组件中是RebalanceImpl这个抽象类,它的实现者有RebalancePushImpl、RebalanceLitePullImpl、RebalancePullImpl分别供几个消费者客户端使用。下图是类结构图。

抽象类中有一个重要的方法doRebalance(),这个方法是对外使用的。即使用的方式就是创建一个负载均衡实现类的实例,然后调用它的doRebalance()方法即可进行。
1.2 一次负载均衡的流程分析
干的核心事情:
-
将主题的消息队列应用分配策略分配到消费组里面的消费者
-
丢弃删减的消息队列对应的ProcessQueue,创建新增的消息队列的PullRequest和ProcessQueue。这些动作与消费者拉取消息消费相关。
主要流程说明:
-
1 从
ConcurrentMap<String/*主题*/, Set<MessageQueue> /*主题的消息队列*/> topicSubscribeInfoTable这个map中获取主题对应的MessageQueue集合,这个map会被一个定时任务(定时的从NameServer获取已订阅主题的路由信息)进行更新;主题的消息队列集合 -
2 获取该消费者所属消费者组下的所有消费者集合;消费者集合
-
3 将消息队列集合和消费者集合按照某个分配策略进行分配;
-
4 分配策略完成分配,当前消费者获得新的消费队列集合或者删减了某些消息队列,接下来将新的消费队列和旧的进行对比
-
5 如果消费者分配到了一个之前没有的mq,那么将会创建一个PullRequest给PullMessageService实现对消息的拉取,如果之前有但是现在没有的那么将会调用里面的ProcessQueue的droped方法来销毁,即不再拉取消息。其中创建新的PullRequest将会计算一次消息拉取偏移量
this.computePullFromWhere(mq);
public void doRebalance(final boolean isOrder) {
// 获取订阅数据
Map<String, SubscriptionData> subTable = this.getSubscriptionInner();
// 遍历订阅数据
for (final Map.Entry<String, SubscriptionData> entry : subTable.entrySet()) {
// 获取主题
final String topic = entry.getKey();
// 根据主题进行负载均衡
this.rebalanceByTopic(topic, isOrder);
}
this.truncateMessageQueueNotMyTopic();
}
private void rebalanceByTopic(final String topic, final boolean isOrder) {
switch (messageModel) {
case BROADCASTING: {
...
}
case CLUSTERING: {
// 同样是先获取主题对应的消息队列列表
// 这个map是会被一个从NameServer拉取路由信息来更新的
Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
// 找到主题对应的该消费组内所有的消费者ID
List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
if (mqSet != null && cidAll != null) {
List<MessageQueue> mqAll = new ArrayList<MessageQueue>();
mqAll.addAll(mqSet);
AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;
List<MessageQueue> allocateResult = null;
// 调用具体的负载均衡策略进行负载
// 负载的含义:将该主题的mq在该消费组的所有的消费者进行分配
allocateResult = strategy.allocate(
this.consumerGroup,
this.mQClientFactory.getClientId(),
mqAll,
cidAll);
Set<MessageQueue> allocateResultSet = new HashSet<MessageQueue>();
if (allocateResult != null) {
allocateResultSet.addAll(allocateResult);
}
// 看当前主题当前消费者的分配到的mq是否变化了
// 下面这个函数很重要,如果消费者分配到了一个之前没有的mq,那么将会创建一个PullRequest给PullMessageService实现对消息的拉取,如果之前有但是现在没有的那么将会调用里面的ProcessQueue的droped方法来销毁,即不再拉取消息。
boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);
if (changed) {
log.info(
"rebalanced result changed. allocateMessageQueueStrategyName={}, group={}, topic={}, clientId={}, mqAllSize={}, cidAllSize={}, rebalanceResultSize={}, rebalanceResultSet={}",
strategy.getName(), consumerGroup, topic, this.mQClientFactory.getClientId(), mqSet.size(), cidAll.size(),
allocateResultSet.size(), allocateResultSet);
this.messageQueueChanged(topic, mqSet, allocateResultSet);
}
}
break;
}
}
2 消费者组使用负载均衡组件
关系说明:在MQClientInstance“客户端实例工厂”里面维护了一个负载均衡后台服务线程RebalanceService。它里面还维护了一个消费者组–>消费者内部客户端实例这样的一个map。每个消费者内部客户端里面维护了一个负载均衡实现类。如下图所示

调用流程:rebalanceService服务线程的run()方法调用MQClientInstance的doRebalance(),在此方法或遍历消费者组–>消费者内部客户端这个map,并调用每个消费者内部客户端的doRebalance(),此方法会调用负载均衡组件的doRebalance(...)方法。

负载均衡的后台服务线程在MQ的客户端MQClientInstance启动的,它有一个成员变量
public class MQClientInstance {
...
private final RebalanceService rebalanceService;
...
}
赋初值是在它的构造函数中
public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) {
this.rebalanceService = new RebalanceService(this);
}
负载均衡的启动是在它的start()方法中,调用了RebalanceService的start()方法
public void start() throws MQClientException {
this.rebalanceService.start();
}
定时的负载均衡就在RebalanceService中实现了。
下面从几个问题出发来分析其中的机制
2.1 RebalanceService是什么?
它的类结构图如下,可以看到它继承自ServiceThread抽象类,属于RocketMQ中典型的创建后台服务线程运行方式,可参考RocketMQ中服务线程的分析。只要启动RebalanceService,它就会一直在后台运行,不会停,相当于一个定时任务。

2.2 RebalanceService的运行时,执行的代码体是什么?
下面是RebalanceService中run()方法体
public void run() {
log.info(this.getServiceName() + " service started");
// 典型的线程设计方式,用一个volatile变量来控制线程的运行
while (!this.isStopped()) {
// 在这儿进行了等待,等待一定的时间,相当于定时
this.waitForRunning(waitInterval);
// 被唤醒了执行真正的负载均衡【重要,下面这个调用即是执行负载均衡】
this.mqClientFactory.doRebalance();
}
log.info(this.getServiceName() + " service end");
}
2.3 负载均衡函数调用分析
首先调用的是MQClientInstance的doRebalance()方法,在该方法中,首先获取的是消费者组内部客户端map,然后遍历这个map中的元素,并调用内部客户端(MQConsumerInner)的doRebalance()进行负载均衡。在一个JVM中只存在一个MQClientInstance,而一个消费者组又只存在一个消费者内部客户端,所以进行负载均衡就是对每个消费者组内部客户端执行负载均衡,。
MQClientInstance#
public void doRebalance() {
// 遍历消费的存储表 <group,consumerImpl>
for (Map.Entry<String, MQConsumerInner> entry : this.consumerTable.entrySet()) {
// 获取对应的消费者内部客户端
MQConsumerInner impl = entry.getValue();
if (impl != null) {
try {
// 调用实现类做真正的负载均衡
impl.doRebalance();
} catch (Throwable e) {
log.error("doRebalance exception", e);
}
}
}
}
DefaultMQPushConsumerImpl#
public void doRebalance() {
if (!this.pause) {
// 调用里面的负载均衡实现类做负载均衡
this.rebalanceImpl.doRebalance(this.isConsumeOrderly());
}
}
2.4 更新主题消息队列

本文详细介绍了RocketMQ消费者负载均衡的实现,包括负载均衡的整体概述、一次负载均衡的流程,以及消费者组如何使用负载均衡组件。重点讨论了RebalanceService的作用、执行流程和负载均衡函数的调用分析。

1394

被折叠的 条评论
为什么被折叠?



