Consumer Rebance机制介绍
Rebalance机制,即再均衡机制,是RocketMQ中一个重要的功能。当一个Topic下的多个队列(或称之为分区)需要在同一个消费者组下的多个消费者实例之间进行重新分配时,就会触发Rebalance机制。该机制的本意是为了提升消息的并行处理能力。
举例来说,如果一个Topic下有5个队列,在只有1个消费者的情况下,那么这个消费者将负责处理这5个队列的消息。如果此时我们增加一个消费者,那么可以通过Rebalance机制,给其中一个消费者分配2个队列,给另一个分配3个队列,从而提升消息的并行处理能力。
Consumer Reblance代码分析
消费者启动时的代码逻辑
public void start() throws MQClientException {
synchronized (this) {
switch (this.serviceState) {
case CREATE_JUST:
this.serviceState = ServiceState.START_FAILED;
// If not specified,looking address from name server
if (null == this.clientConfig.getNamesrvAddr()) {
this.mQClientAPIImpl.fetchNameServerAddr();
}
// Start request-response channel
//TODO 开启消息发送服务 NRC
this.mQClientAPIImpl.start();
// Start various schedule tasks
//TODO 12.1 定时任务
this.startScheduledTask();
//TODO 12.2 开启拉消息服务(消费者:线程)
this.pullMessageService.start(); // -> run中运行
//TODO 12.3 负载均衡服务(消费者:线程)
this.rebalanceService.start();
// Start push service
this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
log.info("the client factory [{}] start OK", this.clientId);
this.serviceState = ServiceState.RUNNING;
break;
case START_FAILED:
throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
default:
break;
}
}
}
我们重点关注负载均衡 this.rebalanceService.start();
public void start() {
log.info("Try to start service thread:{} started:{} lastThread:{}", getServiceName(), started.get(), thread);
//判断消费者是否已经启动
if (!started.compareAndSet(false, true)) {
return;
}
stopped = false;
this.thread = new Thread(this, getServiceName());
this.thread.setDaemon(isDaemon);
this.thread.start();
}
public class RebalanceService extends ServiceThread {
private static long waitInterval =
Long.parseLong(System.getProperty(
"rocketmq.client.rebalance.waitInterval", "20000"));
private final InternalLogger log = ClientLogger.getLog();
private final MQClientInstance mqClientFactory;
public RebalanceService(MQClientInstance mqClientFactory) {
this.mqClientFactory = mqClientFactory;
}
@Override
public void run() {
log.info(this.getServiceName() + " service started");
//当前类状态若正常,则已知执行while循环
while (!this.isStopped()) {
this.waitForRunning(waitInterval);
this.mqClientFactory.doRebalance();
}
log.info(this.getServiceName() + " service end");
}
@Override
public String getServiceName() {
return RebalanceService.class.getSimpleName();
}
}
遍历消费者实例,将每个消费者组都进行负载均衡
public void doRebalance() {
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);
}
}
}
}
public void doRebalance(final boolean isOrder) {
Map<String, SubscriptionData> subTable = this.getSubscriptionInner();
if (subTable != null) {
for (final Map.Entry<String, SubscriptionData> entry : subTable.entrySet()) {
final String topic = entry.getKey();
try {
//对每个topic都进行负载均衡
this.rebalanceByTopic(topic, isOrder);
} catch (Throwable e) {
if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
log.warn("rebalanceByTopic Exception", e);
}
}
}
}
private void rebalanceByTopic(final String topic, final boolean isOrder) {
switch (messageModel) {
//广播模型
case BROADCASTING: {
Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
if (mqSet != null) {
boolean changed = this.updateProcessQueueTableInRebalance(topic, mqSet, isOrder);
if (changed) {
this.messageQueueChanged(topic, mqSet, mqSet);
}
}
break;
}
//集群模型
case CLUSTERING: {
//查topic下的消息队列
Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
//查询topic下的所有消费者
List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);
if (null == mqSet) {
if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
log.warn("doRebalance, {}, but the topic[{}] not exist.", consumerGroup, topic);
}
}
if (mqSet != null && cidAll != null) {
List<MessageQueue> mqAll = new ArrayList<MessageQueue>();
mqAll.addAll(mqSet);
Collections.sort(mqAll);
Collections.sort(cidAll);
//负载均衡组件
AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;
//负载均衡结果
List<MessageQueue> allocateResult = strategy.allocate(
this.consumerGroup,
this.mQClientFactory.getClientId(),
mqAll,
cidAll);
Set<MessageQueue> allocateResultSet = new HashSet<MessageQueue>();
if (allocateResult != null) {
allocateResultSet.addAll(allocateResult);
}
//负载均衡执行结束后,判断是否有新的消费策略变化,更新拉取策略
boolean changed = this.updateProcessQueueTableInRebalance(topic, allocateResultSet, isOrder);
if (changed) {
//发送更新通知
this.messageQueueChanged(topic, mqSet, allocateResultSet);
}
}
break;
}
default:
break;
}
}
负载均衡操作的总结:
1.找到每个Topic下面的所有的队列。
2.找到每个topic下面的所有的消费者。
3.根据负载均衡策略给消费者分配队列。
4.消息队列更新。
负载均衡策略
主要是根据AllocateMessageQueueStrategy这个类去实现
public interface AllocateMessageQueueStrategy {
/**
* Allocating by consumer id
*
* @param consumerGroup current consumer group
* @param currentCID current consumer id
* @param mqAll message queue set in current topic
* @param cidAll consumer set in current consumer group
* @return The allocate result of given strategy
*/
List<MessageQueue> allocate(
//消费者组
final String consumerGroup,
//当前的消费者Id
final String currentCID,
//所有的队列
final List<MessageQueue> mqAll,
//所有的消费者
final List<String> cidAll
);
/**
* Algorithm name
*
* @return The strategy name
*/
String getName();
}
分配策略常用的有三个:
1.平均分配(AverageAllocateStrategy):将所有的队列平均分配给所有的消费者。
2.根据消费者数量分配(DefaultAllocateMessageQueueStrategy):
根据消费者的数量和队列的数量来分配队列,尽量保证每个消费者分配到的队列数量相同。
3.根据消费者权重分配(WeightedAllocateStrategy):
根据消费者设置的权重来分配队列,权重高的消费者可以分配到更多的队列。
Consumer Reblance的影响
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结束之后瞬间可能需要消费很多消息。