訊息中介軟體—RocketMQ訊息傳送

Java後端開發發表於2019-02-18

大道至簡,訊息佇列可以簡單概括為:“一發一存一收”,在這三個過程中訊息傳送最為簡單,也比較容易入手,適合初中階童鞋作為MQ研究和學習的切入點。因此,本篇主要從一條訊息傳送為切入點,詳細闡述在RocketMQ這款分散式訊息佇列中傳送一條普通訊息的大致流程和細節。

一、RocketMQ網路架構圖

RocketMQ分散式訊息佇列的網路部署架構圖如下圖所示(其中,包含了生產者Producer傳送普通訊息至叢集的兩條主線)

訊息中介軟體—RocketMQ訊息傳送


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例項均部署起來):

訊息中介軟體—RocketMQ訊息傳送

三、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的主要啟動流程如下:

訊息中介軟體—RocketMQ訊息傳送


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次)進行訊息傳送核心流程的簡析。使用同步方式傳送訊息核心流程的入口如下:

訊息中介軟體—RocketMQ訊息傳送

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中該部分的核心方法原始碼如下(已經加了註釋):

訊息中介軟體—RocketMQ訊息傳送

/**

* 本地快取中不存在時從遠端的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路由資訊的對映圖如下:

訊息中介軟體—RocketMQ訊息傳送

Client中TopicRouteData到TopicPublishInfo的對映.jpg

其中,上面的TopicRouteData和TopicPublishInfo路由資訊變數大致如下:

訊息中介軟體—RocketMQ訊息傳送


TopicRouteData變數內容.jpg

訊息中介軟體—RocketMQ訊息傳送


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_KEY020003670EC418B4AAC208AD

46930000WAITtrueTAGSTagA, 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_KEY020003670EC418B4AAC20

8AD46930000WAITtrueTAGSTagA, 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的客戶端傳送普通訊息的流程大概到這裡就分析完成。關於順序訊息、分散式事務訊息等內容將在後續篇幅中陸續介紹,敬請期待。限於筆者的才疏學淺,對本文內容可能還有理解不到位的地方,如有闡述不合理之處還望留言一起探討。


相關文章