RocketMq中MessageQueue的分配

12,335 阅读5分钟

都知道Rocketmq中有ConsumerGroup的概念。在集群模式下,多台服务器配置相同的ConsumerGroup,能够使得每次只有一台服务器消费消息(注意,但不保证只消费一次,存在网络抖动的情况)。那么,笔者就很疑惑,Rocketmq是如何实现这个模式的?如何保证只有一台服务器消费?

虽然答案很简单,但却是一个很好的带着问题看源码的机会。

RocketMq结构

RocketMQ架构示意图

从图中可以看到,MQ主要投递消息和拉取消息两个环节。

众多的架构都是顺应时代潮流而来,Rocketmq的结构体系当然也不是阿里所独创的,而是依据AMQP协议而来。Rocketmq中的Producer,Broker,以及Consumer都是依据AMQP中的概念衍生出来的。所以这里不妨讲讲AMQP(Advanced Message Queuing Protocal,高级消息队列协议),便于大家更好的理解技术的发展过程。

paper下载 http://www.amqp.org/specification/0-9-1/amqp-org-download

  • Broker: 接收和分发的应用
  • Virtual host:出于多租户和安全因素,把AMQP的基本组件划分到一个虚拟分组中。各个租户之间是网络隔离的,类似Linux中的namespace概念(可自行Google)
  • Connection:publisher/consumer 和broker之间的TCP连接
  • Channel:是相较于Connection更加轻量的连接,是Connection上的逻辑连接
  • Exchange: 负责将message分发到不同的Queue中
  • Queue: 消息最终会落到Queue中,消息由Broker push给Consumer或者由Consumer来pull消息
  • Binding:exchange和queue之间的消息路由策略
AMQP架构示意图

消息队列的3大类型

当然基于这样一个协议,不单单是RocketMq一个闪耀在消息队列选型中,还有不同的消息队列。

https://mp.weixin.qq.com/s/B1D-J_1wpaqj0sxcmaArbQ

主要分为了3大阵营:

  • 有Broker 重Topic流:kafka,JMS
  • 有Broker 轻Topic流: RocketMQ
  • 无Broker: ZeroMQ

当然,如果熟悉了AMQP协议,你也可以选择自研一个消息队列

https://zhuanlan.zhihu.com/p/28967866

了解了一些背景,来看下RocketMQ中消息的投递过程。还是那个具体的问题,RocketMQ是如何选择一个队列来投递的呢?

Producer如何投递消息到不同队列

这里提一下,RocketMq中所有关于生产者和消费者的代码都在client包下。打开源码,可以看到Procuder下有个selector包,看到这个包是不是感到就是它的感觉。

可以看到selector下的三个类都是实现了MessageQueueSelector,来看下MessageQueueSelector的代码。

public interface MessageQueueSelector {
    MessageQueue select(final List<MessageQueue> mqs, final Message msg, final Object arg);
}

public class MessageQueue {
	private String topic;
	private String brokerName;
	private int queueId;
}

看一下哪里调用了MessageQueueSelector.select(),发现是DefaultMQProducerImpl,那么可以确认就是由MessageQueueSelector提供了选择哪个队列。

RocketMq提供了3种不同的选择队列方式:

  • SelectMessageQueueByHash
  • SelectMessageQueueByMachineRoom
  • SelectMessageQueueByRandom

默认队列数量

细心的同学肯定会问那么队列数量是无限大的吗?这个可以查阅RocketMq的使用手册,默认的队列数量是4 (defaultTopicQueueNums: 4),当然你也可以选择自己配置。

同时不知道有没有同学找错地儿,笔者刚开始是找错地儿了,在TopicPublishInfo中也找到了个selectOneMessageQueue,代码如下。

public class TopicPublishInfo{
    // 不同版本,代码有些不同,逻辑类似
    public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
        if (lastBrokerName != null) {
            int index = this.sendWhichQueue.getAndIncrement();
            for (int i = 0; i < this.messageQueueList.size(); i++) {
                int pos = Math.abs(index++) % this.messageQueueList.size();
                MessageQueue mq = this.messageQueueList.get(pos);
                if (!mq.getBrokerName().equals(lastBrokerName)) {
                    return mq;
                }
            }

            return null;
        }
        else {
            int index = this.sendWhichQueue.getAndIncrement();
            int pos = Math.abs(index) % this.messageQueueList.size();
            return this.messageQueueList.get(pos);
        }
    }
}

查了下调用方发现是MQFaultStrategy,看来是Rocketmq消费失败时候,会将消息重新投递到不同的队列,这样在集群模式下能够保证分布到不同机器消费。(是不是还有疑惑,为什么能保证到不同机器,请往下看)

Consumer如何从消息队列获取消息

这里是比较难理解的一步,首先查阅RocketMQ手册可以看到:

RocketMQ 的 Consumer 都是从 Broker 拉消息来消费,但是为了能做到实时收消息,RocketMQ 使用长轮询方式,可以保证消息实时性同 Push 方式一致。返种长轮询方式类似亍 Web QQ 收収消息机制。请参考以下信息了解更多。http://www.ibm.com/developerworks/cn/web/wa-lo-comet/

虽然解释的很详细,但是对新手还是不是很友好。简单的来说,就是使用长轮询,客户端发起请求和服务端先连接上,但是如果服务端没有数据,这是连接还是hold住,当有数据push给客户端的时候才关闭连接。这样不但保证了消费者不会被上游的消息打垮,也保证了消息的实时性。

那么还有个问题,Consumer如何从MessageQueue上拉取消息呢?是随机拉吗?

不妨来看下MQPullConsumer,DefaultMQPullConsumer就是继承于它。

public class MQPullConsumer {

    // 拉消息,非阻塞
    // 
    // @param mq from which message queue
    // @param subExpression 订阅的tag,只支持"tag1 || tag2 || tag3"
    // @param offset 标志位
    // @param maxNums 消费最大数量
    PullResult pull(final MessageQueue mq, final String subExpression, final long offset,
	    final int maxNums) throws MQClientException, RemotingException, MQBrokerException,
	    InterruptedException;
}

可以看到MessageQueue是传进来的,这就比较尴尬了,实在无法理解是什么时候决定好从哪个队列拉取消息的。幸亏有万能的搜索引擎,

https://zhuanlan.zhihu.com/p/25140744

RocketMq有专门的类AllocateMessageQueueStrategy.class,就藏在Client.Consumer.rebalance包下。

  • AllocateMessageQueueAveragely
  • AllocateMessageQueueAveragelyByCircle
  • AllocateMessageQueueByConfig
  • AllocateMessageQueueByMachineRoom
  • AllocateMessageQueueConsistentHash

每一次Consumer数量的变更,都会触发AllocateMessageQueueStrategy。也就是每一次Consumer拉取的队列都是固定好的。

现在,在回过头来看看第一张RocketMQ的架构图,是不是觉得画的很透彻。

总结

  1. 任何的框架都有它衍生变化的历史,了解架构变化的历史,才能更好的理解一个框架
  2. 好好研读使用手册,包含了很多架构的细节
  3. 带着问题去研读源码