大道至簡,訊息佇列可以簡單概括為:“一發一存一收”,在這三個過程中訊息傳送最為簡單,也比較容易入手,適合初中階童鞋作為MQ研究和學習的切入點。因此,本篇主要從一條訊息傳送為切入點,詳細闡述在RocketMQ這款分散式訊息佇列中傳送一條普通訊息的大致流程和細節。
一、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例項均部署起來):
三、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次)進行訊息傳送核心流程的簡析。使用同步方式傳送訊息核心流程的入口如下:
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中該部分的核心方法原始碼如下(已經加了註釋):
/**
* 本地快取中不存在時從遠端的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_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的客戶端傳送普通訊息的流程大概到這裡就分析完成。關於順序訊息、分散式事務訊息等內容將在後續篇幅中陸續介紹,敬請期待。限於筆者的才疏學淺,對本文內容可能還有理解不到位的地方,如有闡述不合理之處還望留言一起探討。