[RocketMQ]消息中間件—RocketMQ消息發送

摘要:使用客戶端發送一條消息很Easy,在這背後RocketMQ完成了怎麼樣的操作呢?
大道至簡,消息隊列可以簡單概括爲:“一發一存一收”,在這三個過程中消息發送最爲簡單,也比較容易入手,適合初中階童鞋作爲MQ研究和學習的切入點。因此,本篇主要從一條消息發送爲切入點,詳細闡述在RocketMQ這款分佈式消息隊列中發送一條普通消息的大致流程和細節。在閱讀本篇之前希望讀者能夠先仔細讀下關於RocketMQ分佈式消息隊列Remoting通信模塊的兩篇文章:
(1)消息中間件—RocketMQ的RPC通信(一)
(2)消息中間件—RocketMQ的RPC通信(二)

一、RocketMQ網絡架構圖

RocketMQ分佈式消息隊列的網絡部署架構圖如下圖所示(其中,包含了生產者Producer發送普通消息至集羣的兩條主線)

RocketMQ部署架構.jpg


對於上圖中幾個角色的說明:
(1)NameServer:RocketMQ集羣的命名服務器(也可以說是註冊中心),它本身是無狀態的(實際情況下可能存在每個NameServer實例上的數據有短暫的不一致現象,但是通過定時更新,在大部分情況下都是一致的),用於管理集羣的元數據( 例如,KV配置、Topic、Broker的註冊信息)。
(2)Broker(Master):RocketMQ消息代理服務器主節點,起到串聯Producer的消息發送和Consumer的消息消費,和將消息的落盤存儲的作用;
(3)Broker(Slave):RocketMQ消息代理服務器備份節點,主要是通過同步/異步的方式將主節點的消息同步過來進行備份,爲RocketMQ集羣的高可用性提供保障;
(4)Producer(消息生產者):在這裏爲普通消息的生產者,主要基於RocketMQ-Client模塊將消息發送至RocketMQ的主節點。
對於上面圖中幾條通信鏈路的關係:
(1)Producer與NamerServer:每一個Producer會與NameServer集羣中的一個實例建立TCP連接,從這個NameServer實例上拉取Topic路由信息;
(2)Producer和Broker:Producer會和它要發送的topic相關聯的Master的Broker代理服務器建立TCP連接,用於發送消息以及定時的心跳信息;
(3)Broker和NamerServer:Broker(Master or Slave)均會和每一個NameServer實例來建立TCP連接。Broker在啓動的時候會註冊自己配置的Topic信息到NameServer集羣的每一臺機器中。即每一個NameServer均有該broker的Topic路由配置信息。其中,Master與Master之間無連接,Master與Slave之間有連接;

 

二、客戶端發送普通消息的demo方法

在RocketMQ源碼工程的example包下就有最爲簡單的發送普通消息的樣例代碼(ps:對於剛剛接觸RocketMQ的童鞋使用這個包下面的樣例代碼進行系統性的學習和調試)。
我們可以直接run下“org.apache.rocketmq.example.simple”包下Producer類的main方法即可完成一次普通消息的發送(主要代碼如下,在這裏需本地將NameServer和Broker實例均部署起來):

        //step1.啓動DefaultMQProducer,啓動時的具體流程一會兒會詳細說明
        DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.setInstanceName("producer");
        producer.start();

        try {
            {
                //step2.封裝將要發送消息的內容
                Message msg = new Message("TopicTest",
                        "TagA",
                        "OrderID188",
                        "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
                //step3.發送消息流程,具體流程待會兒說
                SendResult sendResult = producer.send(msg);
            }
        } catch (Exception e) {
            //Exception code
        }
        producer.shutdown();

三、RocketMQ發送普通消息的全流程解讀

從上面一節中可以看出,消息生產者發送消息的demo代碼還是較爲簡單的,核心就幾行代碼,但在深入研讀RocketMQ的Client模塊後,發現其發送消息的核心流程還是有一些複雜的。下面將主要從DefaultMQProducer的啓動流程、send發送方法和Broker代理服務器的消息處理三方面分別進行分析和闡述。

3.1 DefaultMQProducer的啓動流程

在客戶端發送普通消息的demo代碼部分,我們先是將DefaultMQProducer實例啓動起來,裏面調用了默認生成消息的實現類—DefaultMQProducerImpl的start()方法。

@Override
    public void start() throws MQClientException {
        this.defaultMQProducerImpl.start();
    }

默認生成消息的實現類—DefaultMQProducerImpl的啓動主要流程如下:
(1)初始化得到MQClientInstance實例對象,並註冊至本地緩存變量—producerTable中;
(2)將默認Topic(“TBW102”)保存至本地緩存變量—topicPublishInfoTable中;
(3)MQClientInstance實例對象調用自己的start()方法,啓動一些客戶端本地的服務線程,如拉取消息服務、客戶端網絡通信服務、重新負載均衡服務以及其他若干個定時任務(包括,更新路由/清理下線Broker/發送心跳/持久化consumerOffset/調整線程池),並重新做一次啓動(這次參數爲false);
(4)最後向所有的Broker代理服務器節點發送心跳包;
總結起來,DefaultMQProducer的主要啓動流程如下:

DefaultMQProducer的start方法啓動過程.jpg


這裏有以下幾點需要說明:
(1)在一個客戶端中,一個producerGroup只能有一個實例;
(2)根據不同的clientId,MQClientManager將給出不同的MQClientInstance;
(3)根據不同的producerGroup,MQClientInstance將給出不同的MQProducer和MQConsumer(保存在本地緩存變量——producerTable和consumerTable中);

 

3.2 send發送方法的核心流程

通過Rocketmq的客戶端模塊發送消息主要有以下三種方法:
(1)同步方式
(2)異步方式
(3)Oneway方式
其中,使用(1)、(2)種方式來發送消息比較常見,具體使用哪一種方式需要根據業務情況來判斷。本節內容將結合同步發送方式(同步發送模式下,如果有發送失敗的最多會有3次重試(也可以自己設置),其他模式均1次)進行消息發送核心流程的簡析。使用同步方式發送消息核心流程的入口如下:

     /**
     * 同步方式發送消息核心流程的入口,默認超時時間爲3s
     *
     * @param msg     發送消息的具體Message內容
     * @param timeout 其中發送消息的超時時間可以參數設置
     * @return
     * @throws MQClientException
     * @throws RemotingException
     * @throws MQBrokerException
     * @throws InterruptedException
     */
    public SendResult send(Message msg,
        long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
        return this.sendDefaultImpl(msg, CommunicationMode.SYNC, null, timeout);
    }

3.2.1 嘗試獲取TopicPublishInfo的路由信息

我們一步步debug進去後會發現在sendDefaultImpl()方法中先對待發送的消息進行前置的驗證。如果消息的Topic和Body均沒有問題的話,那麼會調用—tryToFindTopicPublishInfo()方法,根據待發送消息的中包含的Topic嘗試從Client端的本地緩存變量—topicPublishInfoTable中查找,如果沒有則會從NameServer上更新Topic的路由信息(其中,調用了MQClientInstance實例的updateTopicRouteInfoFromNameServer方法,最終執行的是MQClientAPIImpl實例的getTopicRouteInfoFromNameServer方法),這裏分別會存在以下兩種場景:
(1)生產者第一次發送消息(此時,Topic在NameServer中並不存在):因爲第一次獲取時候並不能從遠端的NameServer上拉取下來並更新本地緩存變量—topicPublishInfoTable成功。因此,第二次需要通過默認Topic—TBW102的TopicRouteData變量來構造TopicPublishInfo對象,並更新DefaultMQProducerImpl實例的本地緩存變量——topicPublishInfoTable。
另外,在該種類型的場景下,當消息發送至Broker代理服務器時,在SendMessageProcessor業務處理器的sendBatchMessage/sendMessage方法裏面的super.msgCheck(ctx, requestHeader, response)消息前置校驗中,會調用TopicConfigManager的createTopicInSendMessageMethod方法,在Broker端完成新Topic的創建並持久化至配置文件中(配置文件路徑:{rocketmq.home.dir}/store/config/topics.json)。(ps:該部分內容其實屬於Broker有點超本篇的範圍,不過由於涉及新Topic的創建因此在略微提了下)
(2)生產者發送Topic已存在的消息:由於在NameServer中已經存在了該Topic,因此在第一次獲取時候就能夠取到並且更新至本地緩存變量中topicPublishInfoTable,隨後tryToFindTopicPublishInfo方法直接可以return。
在RocketMQ中該部分的核心方法源碼如下(已經加了註釋):

    /**
     * 根據msg的topic從topicPublishInfoTable獲取對應的topicPublishInfo
     * 如果沒有則更新路由信息,從nameserver端拉取最新路由信息
     *
     * topicPublishInfo
     * 
     * @param topic
     * @return
     */
    private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
        //step1.先從本地緩存變量topicPublishInfoTable中先get一次
        TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
        if (null == topicPublishInfo || !topicPublishInfo.ok()) {
            this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
            //step1.2 然後從nameServer上更新topic路由信息
            this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
            //step2 然後再從本地緩存變量topicPublishInfoTable中再get一次
            topicPublishInfo = this.topicPublishInfoTable.get(topic);
        }

        if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
            return topicPublishInfo;
        } else {
            /**
             *  第一次的時候isDefault爲false,第二次的時候default爲true,即爲用默認的topic的參數進行更新
             */
            this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
            topicPublishInfo = this.topicPublishInfoTable.get(topic);
            return topicPublishInfo;
        }
    }
/**
     * 本地緩存中不存在時從遠端的NameServer註冊中心中拉取Topic路由信息
     *
     * @param topic
     * @param timeoutMillis
     * @param allowTopicNotExist
     * @return
     * @throws MQClientException
     * @throws InterruptedException
     * @throws RemotingTimeoutException
     * @throws RemotingSendRequestException
     * @throws RemotingConnectException
     */
    public TopicRouteData getTopicRouteInfoFromNameServer(final String topic, final long timeoutMillis,
        boolean allowTopicNotExist) throws MQClientException, InterruptedException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException {
        GetRouteInfoRequestHeader requestHeader = new GetRouteInfoRequestHeader();
        requestHeader.setTopic(topic);
        //設置請求頭中的Topic參數後,發送獲取Topic路由信息的request請求給NameServer
        RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ROUTEINTO_BY_TOPIC, requestHeader);
       //這裏由於是同步方式發送,所以直接return response的響應
        RemotingCommand response = this.remotingClient.invokeSync(null, request, timeoutMillis);
        assert response != null;
        switch (response.getCode()) {
            //如果NameServer中不存在待發送消息的Topic
            case ResponseCode.TOPIC_NOT_EXIST: {
                if (allowTopicNotExist && !topic.equals(MixAll.DEFAULT_TOPIC)) {
                    log.warn("get Topic [{}] RouteInfoFromNameServer is not exist value", topic);
                }

                break;
            }
            //如果獲取Topic存在,則成功返回,利用TopicRouteData進行解碼,且直接返回TopicRouteData
            case ResponseCode.SUCCESS: {
                byte[] body = response.getBody();
                if (body != null) {
                    return TopicRouteData.decode(body, TopicRouteData.class);
                }
            }
            default:
                break;
        }

        throw new MQClientException(response.getCode(), response.getRemark());
    }

將TopicRouteData轉換至TopicPublishInfo路由信息的映射圖如下:

 

Client中TopicRouteData到TopicPublishInfo的映射.jpg

 

其中,上面的TopicRouteData和TopicPublishInfo路由信息變量大致如下:

 

TopicRouteData變量內容.jpg

TopicPublishInfo變量內容.jpg

3.2.2 選擇消息發送的隊列

在獲取了TopicPublishInfo路由信息後,RocketMQ的客戶端在默認方式下,selectOneMessageQueuef()方法會從TopicPublishInfo中的messageQueueList中選擇一個隊列(MessageQueue)進行發送消息。具體的容錯策略均在MQFaultStrategy這個類中定義:

public class MQFaultStrategy {
    //維護每個Broker發送消息的延遲
    private final LatencyFaultTolerance<String> latencyFaultTolerance = new LatencyFaultToleranceImpl();
    //發送消息延遲容錯開關
    private boolean sendLatencyFaultEnable = false;
    //延遲級別數組
    private long[] latencyMax = {50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L};
    //不可用時長數組
    private long[] notAvailableDuration = {0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L};
  ......
}

這裏通過一個sendLatencyFaultEnable開關來進行選擇採用下面哪種方式:
(1)sendLatencyFaultEnable開關打開:在隨機遞增取模的基礎上,再過濾掉not available的Broker代理。所謂的"latencyFaultTolerance",是指對之前失敗的,按一定的時間做退避。例如,如果上次請求的latency超過550Lms,就退避3000Lms;超過1000L,就退避60000L。
(2)sendLatencyFaultEnable開關關閉(默認關閉):採用隨機遞增取模的方式選擇一個隊列(MessageQueue)來發送消息。

    /**
     * 根據sendLatencyFaultEnable開關是否打開來分兩種情況選擇隊列發送消息
     * @param tpInfo
     * @param lastBrokerName
     * @return
     */
    public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
        if (this.sendLatencyFaultEnable) {
            try {

                //1.在隨機遞增取模的基礎上,再過濾掉not available的Broker代理;對之前失敗的,按一定的時間做退避
                int index = tpInfo.getSendWhichQueue().getAndIncrement();
                for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
                    int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
                    if (pos < 0)
                        pos = 0;
                    MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
                    if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
                        if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
                            return mq;
                    }
                }

                final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
                int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
                if (writeQueueNums > 0) {
                    final MessageQueue mq = tpInfo.selectOneMessageQueue();
                    if (notBestBroker != null) {
                        mq.setBrokerName(notBestBroker);
                        mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);
                    }
                    return mq;
                } else {
                    latencyFaultTolerance.remove(notBestBroker);
                }
            } catch (Exception e) {
                log.error("Error occurred when selecting message queue", e);
            }

            return tpInfo.selectOneMessageQueue();
        }

        //2.採用隨機遞增取模的方式選擇一個隊列(MessageQueue)來發送消息
        return tpInfo.selectOneMessageQueue(lastBrokerName);
    }

3.2.3 發送封裝後的RemotingCommand數據包

在選擇完發送消息的隊列後,RocketMQ就會調用sendKernelImpl()方法發送消息(該方法爲,通過RocketMQ的Remoting通信模塊真正發送消息的核心)。在該方法內總共完成以下幾個步流程:
(1)根據前面獲取到的MessageQueue中的brokerName,調用MQClientInstance實例的findBrokerAddressInPublish()方法,得到待發送消息中存放的Broker代理服務器地址,如果沒有找到則跟新路由信息;
(2)如果沒有禁用,則發送消息前後會有鉤子函數的執行(executeSendMessageHookBefore()/executeSendMessageHookAfter()方法);
(3)將與該消息相關信息封裝成RemotingCommand數據包,其中請求碼RequestCode爲以下幾種之一:
a.SEND_MESSAGE(普通發送消息)
b.SEND_MESSAGE_V2(優化網絡數據包發送)c.SEND_BATCH_MESSAGE(消息批量發送)
(4)根據獲取到的Broke代理服務器地址,將封裝好的RemotingCommand數據包發送對應的Broker上,默認發送超時間爲3s;
(5)這裏,真正調用RocketMQ的Remoting通信模塊完成消息發送是在MQClientAPIImpl實例sendMessageSync()方法中,代碼具體如下:

    private SendResult sendMessageSync(
        final String addr,
        final String brokerName,
        final Message msg,
        final long timeoutMillis,
        final RemotingCommand request
    ) throws RemotingException, MQBrokerException, InterruptedException {
        RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis);
        assert response != null;
        return this.processSendResponse(brokerName, msg, response);
    }

(6)processSendResponse方法對發送正常和異常情況分別進行不同的處理並返回sendResult對象;
(7)發送返回後,調用updateFaultItem更新Broker代理服務器的可用時間;
(8)對於異常情況,且標誌位—retryAnotherBrokerWhenNotStoreOK,設置爲true時,在發送失敗的時候,會選擇換一個Broker;
在生產者發送完成消息後,客戶端日誌打印如下:

SendResult [sendStatus=SEND_OK, msgId=020003670EC418B4AAC208AD46930000, offsetMsgId=AC1415A200002A9F000000000000017A, messageQueue=MessageQueue [topic=TopicTest, brokerName=HQSKCJJIDRRD6KC, queueId=2], queueOffset=1]

3.3 Broker代理服務器的消息處理簡析

Broker代理服務器中存在很多Processor業務處理器,用於處理不同類型的請求,其中一個或者多個Processor會共用一個業務處理器線程池。對於接收到消息,Broker會使用SendMessageProcessor這個業務處理器來處理。SendMessageProcessor會依次做以下處理:
(1)消息前置校驗,包括broker是否可寫、校驗queueId是否超過指定大小、消息中的Topic路由信息是否存在,如果不存在就新建一個。這裏與上文中“嘗試獲取TopicPublishInfo的路由信息”一節中介紹的內容對應。如果Topic路由信息不存在,則Broker端日誌輸出如下:

2018-06-14 17:17:24 INFO SendMessageThread_1 - receive SendMessage request command, RemotingCommand [code=310, language=JAVA, version=252, opaque=6, flag(B)=0, remark=null, extFields={a=ProducerGroupName, b=TopicTest, c=TBW102, d=4, e=2, f=0, g=1528967815569, h=0, i=KEYSOrderID188UNIQ_KEY020003670EC418B4AAC208AD46930000WAITtrueTAGSTagA, j=0, k=false, m=false}, serializeTypeCurrentRPC=JSON]
2018-06-14 17:17:24 WARN SendMessageThread_1 - the topic TopicTest not exist, producer: /172.20.21.162:62661
2018-06-14 17:17:24 INFO SendMessageThread_1 - Create new topic by default topic:[TBW102] config:[TopicConfig [topicName=TopicTest, readQueueNums=4, writeQueueNums=4, perm=RW-, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false]] producer:[172.20.21.162:62661]

Topic路由信息新建後,第二次消息發送後,Broker端日誌輸出如下:

2018-08-02 16:26:13 INFO SendMessageThread_1 - receive SendMessage request command, RemotingCommand [code=310, language=JAVA, version=253, opaque=6, flag(B)=0, remark=null, extFields={a=ProducerGroupName, b=TopicTest, c=TBW102, d=4, e=2, f=0, g=1533198373524, h=0, i=KEYSOrderID188UNIQ_KEY020003670EC418B4AAC208AD46930000WAITtrueTAGSTagA, j=0, k=false, m=false}, serializeTypeCurrentRPC=JSON]
2018-08-02 16:26:13 INFO SendMessageThread_1 - the msgInner's content is:MessageExt [queueId=2, storeSize=0, queueOffset=0, sysFlag=0, bornTimestamp=1533198373524, bornHost=/172.20.21.162:53914, storeTimestamp=0, storeHost=/172.20.21.162:10911, msgId=null, commitLogOffset=0, bodyCRC=0, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message [topic=TopicTest, flag=0, properties={KEYS=OrderID188, UNIQ_KEY=020003670EC418B4AAC208AD46930000, WAIT=true, TAGS=TagA}, body=11body's content is:Hello world]]

(2)構建MessageExtBrokerInner;
(3)調用“brokerController.getMessageStore().putMessage”將MessageExtBrokerInner做落盤持久化處理;
(4)根據消息落盤結果(正常/異常情況),BrokerStatsManager做一些統計數據的更新,最後設置Response並返回;

四、總結

使用RocketMQ的客戶端發送普通消息的流程大概到這裏就分析完成。建議讀者可以將作者之前寫的兩篇關於RocketMQ的RPC通信(一)和(二)結合起來讀,可能整體會更加連貫,收穫更大。關於順序消息、分佈式事務消息等內容將在後續篇幅中陸續介紹,敬請期待。限於筆者的才疏學淺,對本文內容可能還有理解不到位的地方,如有闡述不合理之處還望留言一起探討。



作者:癲狂俠
鏈接:https://www.jianshu.com/p/1932365b681e
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯繫作者獲得授權並註明出處。

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