都知道Rocketmq中有ConsumerGroup的概念。在叢集模式下,多臺伺服器配置相同的ConsumerGroup,能夠使得每次只有一臺伺服器消費訊息(注意,但不保證只消費一次,存在網路抖動的情況)。那麼,筆者就很疑惑,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之間的訊息路由策略
訊息佇列的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的架構圖,是不是覺得畫的很透徹。
總結
- 任何的框架都有它衍生變化的歷史,瞭解架構變化的歷史,才能更好的理解一個框架
- 好好研讀使用手冊,包含了很多架構的細節
- 帶著問題去研讀原始碼