RocketMq中MessageQueue的分配

飛奔的蛋蛋發表於2019-03-04

都知道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. 帶著問題去研讀原始碼

相關文章