rocketmq-半入門級架構及核心流程概覽

一直在用rocketmq,對他的功能和大概流程略知一些,但是比較浮,經不起稍微的推敲。是時候進一步瞭解下這個NB的中間件了。

這裏不再贅述它的那些特性,網上一大堆,這裏主要按照自己想了解的一些方面作整理,貼出部分核心代碼,意圖通過表現各個角色間的交互,勾畫大致架構,方便以後對每個要點各個深入。如果想要引導來閱讀源代碼,推薦rmq源碼系列,寫的很有誠意,本筆記中也部分參考引用其文章。

部署結構(邏輯結構/物理結構)

消息中間件的整體看起來像我們相互郵寄明信片。李雷(Producer)通過郵局公告(Namesrv)找到郵局的地址,然後去郵局(Broker)把明信片(Message)發送給韓梅梅(Consumer)。

物理結構

  • Producer 消息生產者,負責產生消息,一般由業務系統負責產生消息。

  • Consumer 消息消費者,負責消費消息,一般是後臺系統負責異步消費。

  • Broker 消息中轉角色,負責存儲消息,轉發消息,一般也稱爲 Server。

  • Namesrv 註冊中心,管理協調角色,維護其他角色信息。

rmq物理部署.png

邏輯結構

邏輯結構裏面把所有角色都集羣化了,集羣化後就要牽扯到集羣消費。於是,基於以上的角色,又衍生出其他的概念。

  • Producer Group 一類 Producer 的集合名稱,這類 Producer 通常發送一類消息,且發送邏輯一致。

  • Consumer Group 一類 Consumer 的集合名稱,這類 Consumer 通常消費一類消息,且消費邏輯一致。

rmq邏輯部署.png

角色實現及其交互

按照邏輯/物理部署結構,各個角色間怎麼通信?怎麼保活?數據怎麼備份?

rmq自己對Netty進行了包裝(Facade),方便各個角色使用,在工程的remoting子工程中。

namesrv(Name Server)與broker

namesrv與broker是實現後續一切的基礎,producer和consumer的信息傳遞需要broker來中轉,那麼找到或者說確定一個broker便是第一步了。

namesrv(Name Server)

namesrv是很輕量的服務,簡單、可集羣橫向擴展、無狀態。他是整個mq的管家(像是Dubbo的服務發現),管理其他角色,如果沒有它,整個mq將難以開展工作。

簡單看下namesrv啓動時的行爲。它的核心代碼在工程的namesrv包下,代碼量很少。

namesrv的故事開始於NamesrvStartup的main方法,它主要來解析配置參數,根據配置裝填並初始化和啓動NamesrvControllerNamesrvController貫連整個Namesrv的業務功能,通過Netty向外暴露服務,主要負責:

  • 管理Topic和Broker的註冊信息

  • 管理KV的配置(持久化)

// NamesrvController
public boolean initialize() {
    // 管理KV的配置(並持久化)
    this.kvConfigManager.load();
	
    // 暴露服務
    this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);

    // netty執行線程池
    this.remotingExecutor =
        Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));

    // 註冊服務處理器
    this.registerProcessor();

    // kv信息打印及broker存活檢測任務
    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            NamesrvController.this.routeInfoManager.scanNotActiveBroker();
        }
    }, 5, 10, TimeUnit.SECONDS);

    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            NamesrvController.this.kvConfigManager.printAllPeriodically();
        }
    }, 1, 10, TimeUnit.MINUTES);

    // 省略TSL配置部分
}

RouteInfoManager相當於是管家的賬本,裏面保存着關於topic、broker的具體信息,這些信息全部保存在內存中。其維護的信息如下。

//RouteInfoManager

private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;

/**
* BrokerData {
* 	private String cluster;
* 	private String brokerName;
* 	private HashMap<Long/* brokerId */, String/* broker address */> brokerAddrs;
* }
**/
private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;

private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;

/**
* BrokerLiveInfo {
* 	private long lastUpdateTimestamp;
* 	private DataVersion dataVersion;
* 	private Channel channel;
* 	private String haServerAddr;
}
**/
private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;

private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;

那麼,這些信息從哪裏來呢?且慢,我們先看下broker。

broker

broker就是例子中郵局,郵局要具備什麼能力呢?

  1. 接收信件
  2. 保存信件,保證除不可抗因素外,信件不丟失
  3. 投遞信件
  4. 高效工作,信件快速接受及送達
  5. 信件投遞失敗的處理
  6. 信件投遞可回溯追蹤(當然,郵局不能看內容了)
  7. 向管家(namesrv)彙報自己的運營狀態

以上這些其實也就是broker要做的事情,只不過它不存在隱私一說罷了。當然他還需要有其他的特性。

  1. 支持消息的不同投遞模型:Publish/Subscribe,集羣分組消費等
  2. 高可用,無單點
  3. 消息有序
  4. 消息過濾
  5. 分佈式事務

broker的主要源碼放在broker目錄下,broker(其他模塊)的啓動模型與namesrv一致,BrokerStartup解析配置,並根據配置裝備BrokerController,然後初始化BrokerController並啓動(start)。進行初始化時的主要啓動一堆定時任務、存儲(MessageStore)和Netty配置(註冊Processor)。

// BrokerController
public boolean initialize() throws CloneNotSupportedException {
    boolean result = this.topicConfigManager.load();

    // 消費者消費信息
    result = result && this.consumerOffsetManager.load();
    // 訂閱組
    result = result && this.subscriptionGroupManager.load();
    // 過濾器
    result = result && this.consumerFilterManager.load();

    // 省略messageStore相關處理
    result = result && this.messageStore.load();

    if (result) {
        // 暴露服務,啓動兩個Server,小端口和VIPChannel有關
        this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.clientHousekeepingService);
        NettyServerConfig fastConfig = (NettyServerConfig) this.nettyServerConfig.clone();
        fastConfig.setListenPort(nettyServerConfig.getListenPort() - 2);
        this.fastRemotingServer = new NettyRemotingServer(fastConfig, this.clientHousekeepingService);
        
        // 省略創建各Processor的Executor
		// 註冊Processor
        this.registerProcessor();

        // 持久化消費信息
        this.scheduledExecutorService.scheduleAtFixedRate(() -> {
            BrokerController.this.consumerOffsetManager.persist();
        }, 1000 * 10, this.brokerConfig.getFlushConsumerOffsetInterval(), TimeUnit.MILLISECONDS);

        // 持久化過濾器信息
        this.scheduledExecutorService.scheduleAtFixedRate(() -> {
            BrokerController.this.consumerFilterManager.persist();
        }, 1000 * 10, 1000 * 10, TimeUnit.MILLISECONDS);

        // 設置namesrv地址,如果沒有設置,則從一個web服務fetch
        if (this.brokerConfig.getNamesrvAddr() != null) {
            this.brokerOuterAPI.updateNameServerAddressList(this.brokerConfig.getNamesrvAddr());
        } else if (this.brokerConfig.isFetchNamesrvAddrByAddressServer()) {
            this.scheduledExecutorService.scheduleAtFixedRate(() -> {
				BrokerController.this.brokerOuterAPI.fetchNameServerAddr();
            }, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS);
        }

        if (BrokerRole.SLAVE == this.messageStoreConfig.getBrokerRole()) {
            // 同步topic、consumerOffset、訂閱組等信息到Salve
            this.scheduledExecutorService.scheduleAtFixedRate(() -> {
                BrokerController.this.slaveSynchronize.syncAll();
            }, 1000 * 10, 1000 * 60, TimeUnit.MILLISECONDS);
        } else {
            // 如果是Master則定時打印與Salve的差異
        }

        // 省略TSL設置及事物初始化
    }
}

public void start() throws Exception {
    // 省略 啓動初始化的那些服務

    // 向所有namesrv註冊自己
    this.registerBrokerAll(true, false, true);

    // 定時向所有namesrv註冊自己
    this.scheduledExecutorService.scheduleAtFixedRate(() -> {
        BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
    }, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS);
}

public synchronized void registerBrokerAll(final boolean checkOrderConfig, boolean oneway, boolean forceRegister) {
	// 封裝broker上的topic信息
    TopicConfigSerializeWrapper topicConfigWrapper = this.getTopicConfigManager().buildTopicConfigSerializeWrapper();

    // 調用API向namesrv註冊
    doRegisterBrokerAll(checkOrderConfig, oneway, topicConfigWrapper);
}

此時,我們可以看到的交互,每個broker啓動時會先對namesrv尋址,然後把自己的信息註冊到所有的namesrv上,並定時維護(心跳),namesrv維護的關於broker的信息來源於此,爲了應對不同的場景需要,namesrv把數據封裝成不同的結構,方面維護。namesrv關於請求的處理在DefaultRequestProcessor中,透過其processRequest方法我們可以明確其要承擔的全部責任,比如:broker註冊/註銷,增刪改查topic路由,KV信息維護等。broker註冊的信息包括但是不限於以下內容。

{
  "topicConfigSerializeWrapper": {
      "topicConfigTable":{
         "topic_?":{
           "defaultReadQueueNums":"16",
          "defaultWriteQueueNums":"16",
          "topicName":"",
          "readQueueNums":"",
          "writeQueueNums":"",
          "perm":"",
          "topicFilterType":"",
          "topicSysFlag":"",
          "order":""
         },
      },
      "dataVersion":{
         "timestamp":"xxxx",
         "counter":"xxxx"
      }
   },
  "filterServerList":[
     "",//filterServerAddr
  ],
  "clusterName":"",
  "brokerAddr":"",
  "brokerName":"",
  "brokerId":""
}

namesrv與producer

公告(namesrv)維護好了,李雷(producer)就可以通過它來獲取郵局(broker)的信息了。

先來看下producer的啓動時主要做了些什麼事情,producer的主要代碼放在工程的client子工程下的producer包中。拋開事務不談,我們的入口在DefaultMQProducer的start方法上。
下面列出啓動時我們關心的業務,啓動流程時序圖,網上也不少了。

// DefaultMQProducerImpl
public void start(final boolean startFactory) throws MQClientException {
    switch (this.serviceState) {
        case CREATE_JUST:
            this.serviceState = ServiceState.START_FAILED;
            // groupName合法性校驗
            this.checkConfig();
			
            // MixAll.CLIENT_INNER_PRODUCER_GROUP 用來發送消費者消費失敗的消息到重試隊列
            if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
                // 如果不是內部構建的
                this.defaultMQProducer.changeInstanceNameToPID();
            }

            // MQClientManager 以 clientId(ip@pid) 維度保證 MQClientInstance 的單例
            this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQProducer, rpcHook);
			
            // 向 MQClientInstance 註冊自己
            boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
            if (!registerOK) {
                this.serviceState = ServiceState.CREATE_JUST;
                throw new MQClientException("...");
            }
			
            // Just for testing or demo program
            this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());

            if (startFactory) {
                // 啓動MQClientInstance
                mQClientFactory.start();
            }

            this.serviceState = ServiceState.RUNNING;
            break;
        case RUNNING:
        case START_FAILED:
        case SHUTDOWN_ALREADY:
            throw new MQClientException("...");
        default:
            break;
    }
	
    // 向broker發送心跳&&上傳filter信息
    this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
}

這裏我們需要說下rmq中client(Producer、Consumer和Admin)的基本結構,不同角色的client通過Composite一個MQClientInstance的方式封裝各自不同的邏輯,所以他們的啓動流程大概一致,Consumer的複雜一些,但主流程無差別:

  1. 通過clientId獲取MQClientInstance實例
  2. 自己角色特有的邏輯
  3. MQClientInstance註冊自己
  4. 啓動MQClientInstance,如果MQClientInstance已啓動則返回
  5. 通過MQClientInstance方法同步更新自己角色需要的信息

按照以上總結我們着重看下MQClientInstance的start做了些什麼。

// MQClientInstance
public void start() throws MQClientException {

    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
            // 啓動remotingClient,開啓與外部交互的通道
            this.mQClientAPIImpl.start();

            // Start various schedule tasks
            this.startScheduledTask();

            // Start pull service
            // push模式下拉消息服務
            this.pullMessageService.start();

            // Start rebalance service
            // 開啓負載均衡消費隊列服務
            this.rebalanceService.start();

            // Start push service
            // 上個代碼片段提到的 CLIENT_INNER_PRODUCER_GROUP 對應的Producer
            this.defaultMQProducer.getDefaultMQProducerImpl().start(false);

            this.serviceState = ServiceState.RUNNING;
            break;
        case START_FAILED:
            throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
        case RUNNING:
        case SHUTDOWN_ALREADY:
        default:
            break;
    }
}

private void startScheduledTask() {
	
    // 如果沒有配置namesrv的地址,默認從web服務定時抓取
    if (null == this.clientConfig.getNamesrvAddr()) {
        this.scheduledExecutorService.scheduleAtFixedRate(() -> {
            MQClientInstance.this.mQClientAPIImpl.fetchNameServerAddr();
        }, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS);
    }

    // 從namesrv(NamesrvController)獲取topic的路由信息
    this.scheduledExecutorService.scheduleAtFixedRate(() -> {
		MQClientInstance.this.updateTopicRouteInfoFromNameServer();
    }, 10, this.clientConfig.getPollNameServerInterval(), TimeUnit.MILLISECONDS);

    // 和broker的交互
    this.scheduledExecutorService.scheduleAtFixedRate(() -> {
        // 清理已離線的Broker
    	MQClientInstance.this.cleanOfflineBroker();
        // 向所有Broker發送心跳,心跳中帶有consumer相關信息:clientId,Subscription等
        MQClientInstance.this.sendHeartbeatToAllBrokerWithLock();
    }, 1000, this.clientConfig.getHeartbeatBrokerInterval(), TimeUnit.MILLISECONDS);

    // 持久化消費偏移量(持久化到遠程或者本地)
    this.scheduledExecutorService.scheduleAtFixedRate(() -> {
    	MQClientInstance.this.persistAllConsumerOffset();
    }, 1000 * 10, this.clientConfig.getPersistConsumerOffsetInterval(), TimeUnit.MILLISECONDS);
	
    // 調整push方式下的拉取線程數
    this.scheduledExecutorService.scheduleAtFixedRate(() -> {
    	MQClientInstance.this.adjustThreadPool();
    }, 1, 1, TimeUnit.MINUTES);
}

MQClientInstance啓動時會啓動一個從namesrv定時蒐集本地所有訂閱(consumer)的和發送(producer)的topic,然後依次通過mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 1000 * 3)來進行更新topic的路由信息。

問題:負載均衡怎麼做的?

namesrv與consumer

正如上文說的那樣,Producer和Consumer都組合了MQClientInstance,而MQClientInstance來完成其和namesrv的交互,所以它的過程和producer是一致的,他們需要維護的信息也是一致的。

producer與broker

producer與broker的交互有以下幾個關注點:producer支持發送消息的方式?發送失敗producer怎麼處理?broker怎麼處理producer的消息?

消息發送

producer發送方式有SYNC、ASYNC、ONEWAY,這些方式定義在CommunicationMode中,但是這塊從DefaultMQProducer定義的send方法命名中沒有明確區分同步和異步,在我當前的版本中ASYNC的實現存在問題(It will be removed at 4.4.0 cause for exception handling and the wrong Semantics of timeout.)。

我們以最簡單的入口(DefaultMQProducer#send(Message))大致看下producer的發送流程。

// DefaultMQProducerImpl
private SendResult sendDefaultImpl(
        Message msg,
        final CommunicationMode communicationMode,
        final SendCallback sendCallback,
        final long timeout
    ) {
    
    // 省略合法性檢查

    final long invokeID = random.nextLong();
    long beginTimestampFirst = System.currentTimeMillis();
    long beginTimestampPrev = beginTimestampFirst;
    // 選擇發送topic信息,此步驟數據由·MQClientInstance·中啓動的定時任務維護
    TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
    if (topicPublishInfo != null && topicPublishInfo.ok()) {
        boolean callTimeout = false;
        MessageQueue mq = null;
        Exception exception = null;
        SendResult sendResult = null;
        // 同步模式下,總髮送次數確定,默認失敗重試2次,也就是共嘗試發送3次
        int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
        int times = 0;
        for (; times < timesTotal; times++) {
            String lastBrokerName = null == mq ? null : mq.getBrokerName();
            // 選擇本次發送的隊列
            MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
            if (mqSelected != null) {
                mq = mqSelected;
                try {
                    beginTimestampPrev = System.currentTimeMillis();
                    long costTime = beginTimestampPrev - beginTimestampFirst;
                    if (timeout < costTime) {
                        callTimeout = true;
                        // 超時中斷
                        break;
                    }
                    // 封裝信息,並調用ClientAPI發送出去
                    sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
                    endTimestamp = System.currentTimeMillis();
                    // 更新本次borker可用性,如果sendLatencyFaultEnable開啓
                    this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
                    // 如果SYNC模式下發送失敗進行重試,ASYNC和ONEWAY模式下直接返回null
                } catch (Exception e) {
                    // 根據不同的異常類型確認處理方式:重試、中斷、返回
                    // 更新本次borker可用性,如果sendLatencyFaultEnable開啓
                	this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
                }
            } else {
                // 沒有可用隊列不進行發送
                break;
            }
        }
        // 省略異常處理
    }
    // 省略異常處理
}

那麼發送消息的怎麼確認發送隊列呢?sendLatencyFaultEnable是什麼?

先通過topic找到對應的topicPublishInfo,顧名思義topicPublishInfo裏面維護了該topic相關的broker和隊列信息,在沒有開啓sendLatencyFaultEnable情況下(默認),按照遞增取模的方式選擇即將要發送的隊列,如果開啓了sendLatencyFaultEnable,再取模後還需要判斷當前隊列的broker是否可用。上面代碼中我們可以看到發送消息後,不管成功還是失敗都會更新一個叫做Item的狀態,這個Item指的就是broker,rmq根據此次消耗的時間來更新該broker不可用的時間,以此達到對不穩定broker的規避。具體代碼可看·MQFaultStrategy·。

消息接收

上面說到BrokerController時,我們未貼出關於broker的NettyServer關於Processor的註冊部分。在這部分中,註冊了不同的Processor:

  1. SendMessageProcessor 處理client發送的消息
  2. PullMessageProcessor 處理client來拉取消息
  3. QueryMessageProcessor 處理對消息的查詢
  4. EndTransactionProcessor 處理事務消息

這些Processor分別處理不同的RequestCode。我們這次關心的是SendMessageProcessor

處理的流程比較簡單,這裏只列出基本流程,可從SendMessageProcessor#processRequest看起。

  1. 檢查broker是否可以對外提供服務(是否到了配置的對外服務時間)
  2. 消息合法性校驗、broker是否可寫、消息寫入隊列是否存在等檢查
  3. 如果是消費失敗重試消息則處理其是否要進入死信隊列
  4. 轉換請求信息封裝爲MessageExtBrokerInner
  5. MessageExtBrokerInner發給MessageStore進行持久化
  6. 處理MessageStore返回的結果,並根據結果用BrokerStatsManager做統計更新
  7. 封裝相應信息並返回。

consumer與broker

韓梅梅終於要收到信了。和現實一樣,要麼郵遞員送給你,要麼你自己上郵局去取信。這兩種方式對應了rmq中的兩種去消息方式:push(DefaultMQPushConsumer)和pull(DefaultMQPullConsumer)。這兩種方式在定義上是有區別的,但實現上實際都是pull,只是對pull的處理不同。說到這裏就不得不提長輪詢了,pull方式我們客戶端需要自己起一個線程定時去broker拉取信息,當broker沒有消息可消費時立刻返回;push方式與pull流程一致,但是broker對其請求處理不同,當broker此刻無消息可消費時,broker會hold住當前請求,直到有消息返回或者到了超時時間返回。戳這裏(–>長輪詢–<–>長輪詢進階–<)更深入瞭解長輪詢,感謝偉大的互聯網讓信息容易共享。

此處以push模式下的MessageListenerConcurrently消費策略來梳理下獲取消息的流程。
DefaultMQPushConsumer相比DefaultMQProducerImpl多啓動了幾個服務:負載均衡服務(consumer平均分配隊列)、維護offset服務、消息消費服務(consumeMessageService)。

// DefaultMQPushConsumer
public synchronized void start() throws MQClientException {
    // 省略配置檢查及服務的啓動

    // 更新訂閱topic信息
    this.updateTopicSubscribeInfoWhenSubscriptionChanged();

    // 每個topic隨機選取一個broker檢查subscription語法是否正確,請求code:RequestCode.CHECK_CLIENT_CONFIG
    this.mQClientFactory.checkClientInBroker();
	
    // 向broker發送心跳,心跳攜帶client相關信息
    this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();

    // 進行負載均衡,默認策略爲`AllocateMessageQueueAveragely`,調整完後發起第一個pullRequest
    this.mQClientFactory.rebalanceImmediately();
}

consumer定時拉取服務是在MQClientInstance中啓動的一個叫做pullMessageService(PullMessageService)的定時服務,這個服務監聽在一個pullRequestQueue隊列上,當有拉取請求時,根據PullRequest選取對應分組的DefaultMQPushConsumerImpl進行拉取。

public class PullRequest {
    private String consumerGroup;
    private MessageQueue messageQueue;
    private ProcessQueue processQueue;
    private long nextOffset;
    private boolean lockedFirst = false;

    // ...
}

PullRequest中有兩個Queue,messageQueue是指要從broker哪個隊列拉數據,ProcessQueue是拉數據成功後存放數據的隊列。有了processQueue便可以做一些簡單的流控,目前可以根據processQueue的消息數量或者消息的大小來決定是否停止本次拉取,並設置下次拉取的延遲而不是立即開始下次拉取。如果流控通過,consumer開始通過pullAPIWrapper向broker拉去信息,拉取成功後提交一個消費任務(默認非CONSUME_MESSAGE_DIRECTLY,如果此時線程池等待隊列已滿,任務延遲提交),消費任務回掉我們註冊的listener,消息至此投遞完成。

投遞完成後,消費的結果怎麼處理?我們剛提到ProcessQueue可以進行流控,那麼合適出隊以消費的消息?消費失敗又需要做什麼處理?這裏不在展開,可見ConsumeMessageConcurrentlyService類中。

除了怎麼去消費消息外,對consumer而言還有另外一個需要解決的問題:怎麼給採用CLUSTERING方式的消費羣組成員平均分配隊列?答案在AllocateMessageQueueAveragely中,該策略保證消息隊列被平均分配給同消費組內的消費者(圖片來源)。

rmq消費者結構

消息存儲

數據的落盤、存儲、數據結構、目錄結構這些請戳–>broker的數據持久化<–

源代碼入口 CommitLog#putMessage(final MessageExtBrokerInner msg)
重要的一些基礎 MappedFile-JavaJava NIOBtree

此處貼上一張關於broker數據結構及流轉的圖,更直觀來看他的數據結構流向(圖片來源)。
rmq數據結構轉換.png

broker與broker(HA)

broker有這幾種角色ASYNC_MASTER、SYNC_MASTER、SLAVE,消息肯定都是寫到MASTER上的,然後同步給SLAVE。

同步的方式分爲SYNC和ASYNC兩種:

  • ASYNC,消息寫完後就直接返回,後臺線程去做同步的操作;
  • SYNC,等到同步完成後返回。

這兩種方式也稱作異步複製和同步雙寫。HA的整個過程只在store模塊做的,是基於JDK的nio來寫的,沒有依賴remoting模塊。

除此之外,broker啓動時,如果當前broker是SLAVE,則會啓動一個定時任務來同步topic等信息。

// SlaveSynchronize
public void syncAll() {
    this.syncTopicConfig();
    this.syncConsumerOffset();
    this.syncDelayOffset();
    this.syncSubscriptionGroupConfig();
}

結語

以上只是對整體架構和主流成的簡單認知,關於其中的細節問題,就需要我們帶着疑問去看源碼了。

問題:
怎麼處理定時消息?
消費失敗的消息怎麼處理?
鏈接是否像dubbo那樣複用?
broker的數據結構是怎樣的?
broker怎麼保證存儲高吞吐?
broker怎麼過濾消息?

“古人學問無遺力,少壯工夫老始成。紙上得來終覺淺,絕知此事要躬行。” – 陸游《冬夜讀書示子聿》

以上貼出源代碼基於incubator-rocketmq release-4.3.2版本,爲了方便均有刪減改,但不影響實際流程。

參考資料

RocketMQ 原理簡介

rocketmq 用戶指南

RocketMQ 最佳實踐

譯-apache-rocketmq用戶指南

誓嘉(rmq作者)文檔

RocketMQ 運維指令

rmq源碼系列

RocketMQ源碼學習(三)-Broker(與Producer交互部分)

Java NIO-閱讀筆記及總結

深入淺出MappedByteBuffer

rmq數據結構轉換圖片來源

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章