分布式開放消息系統(tǒng)(RocketMQ)的原理與實(shí)踐
來(lái)源:http://www.itdecent.cn/p/453c6e7ff81c
備注:
如果您此前未接觸過(guò)RocketMQ,請(qǐng)先閱讀附錄部分,以便了解RocketMQ的整體架構(gòu)和相關(guān)術(shù)語(yǔ)
文中的MQServer與Broker表示同一概念
分布式消息系統(tǒng)作為實(shí)現(xiàn)分布式系統(tǒng)可擴(kuò)展、可伸縮性的關(guān)鍵組件,需要具有高吞吐量、高可用等特點(diǎn)。而談到消息系統(tǒng)的設(shè)計(jì),就回避不了兩個(gè)問(wèn)題:
消息的順序問(wèn)題
消息的重復(fù)問(wèn)題
RocketMQ作為阿里開源的一款高性能、高吞吐量的消息中間件,它是怎樣來(lái)解決這兩個(gè)問(wèn)題的?RocketMQ 有哪些關(guān)鍵特性?其實(shí)現(xiàn)原理是怎樣的?
關(guān)鍵特性以及其實(shí)現(xiàn)原理
一、順序消息
消息有序指的是一類消息消費(fèi)時(shí),能按照發(fā)送的順序來(lái)消費(fèi)。例如:一個(gè)訂單產(chǎn)生了 3 條消息,分別是訂單創(chuàng)建、訂單付款、訂單完成。消費(fèi)時(shí),要按照這個(gè)順序消費(fèi)才有意義。但同時(shí)訂單之間又是可以并行消費(fèi)的。
假如生產(chǎn)者產(chǎn)生了2條消息:M1、M2,要保證這兩條消息的順序,應(yīng)該怎樣做?你腦中想到的可能是這樣:
你可能會(huì)采用這種方式保證消息順序
M1發(fā)送到S1后,M2發(fā)送到S2,如果要保證M1先于M2被消費(fèi),那么需要M1到達(dá)消費(fèi)端后,通知S2,然后S2再將M2發(fā)送到消費(fèi)端。
這個(gè)模型存在的問(wèn)題是,如果M1和M2分別發(fā)送到兩臺(tái)Server上,就不能保證M1先達(dá)到,也就不能保證M1被先消費(fèi),那么就需要在MQ Server集群維護(hù)消息的順序。那么如何解決?一種簡(jiǎn)單的方式就是將M1、M2發(fā)送到同一個(gè)Server上:
保證消息順序,你改進(jìn)后的方法
這樣可以保證M1先于M2到達(dá)MQServer(客戶端等待M1成功后再發(fā)送M2),根據(jù)先達(dá)到先被消費(fèi)的原則,M1會(huì)先于M2被消費(fèi),這樣就保證了消息的順序。
這個(gè)模型,理論上可以保證消息的順序,但在實(shí)際運(yùn)用中你應(yīng)該會(huì)遇到下面的問(wèn)題:
網(wǎng)絡(luò)延遲問(wèn)題
只要將消息從一臺(tái)服務(wù)器發(fā)往另一臺(tái)服務(wù)器,就會(huì)存在網(wǎng)絡(luò)延遲問(wèn)題。如上圖所示,如果發(fā)送M1耗時(shí)大于發(fā)送M2的耗時(shí),那么M2就先被消費(fèi),仍然不能保證消息的順序。即使M1和M2同時(shí)到達(dá)消費(fèi)端,由于不清楚消費(fèi)端1和消費(fèi)端2的負(fù)載情況,仍然有可能出現(xiàn)M2先于M1被消費(fèi)。如何解決這個(gè)問(wèn)題?將M1和M2發(fā)往同一個(gè)消費(fèi)者即可,且發(fā)送M1后,需要消費(fèi)端響應(yīng)成功后才能發(fā)送M2。
但又會(huì)引入另外一個(gè)問(wèn)題,如果發(fā)送M1后,消費(fèi)端1沒(méi)有響應(yīng),那是繼續(xù)發(fā)送M2呢,還是重新發(fā)送M1?一般為了保證消息一定被消費(fèi),肯定會(huì)選擇重發(fā)M1到另外一個(gè)消費(fèi)端2,就如下圖所示。
保證消息順序的正確姿勢(shì)
這樣的模型就嚴(yán)格保證消息的順序,細(xì)心的你仍然會(huì)發(fā)現(xiàn)問(wèn)題,消費(fèi)端1沒(méi)有響應(yīng)Server時(shí)有兩種情況,一種是M1確實(shí)沒(méi)有到達(dá),另外一種情況是消費(fèi)端1已經(jīng)響應(yīng),但是Server端沒(méi)有收到。如果是第二種情況,重發(fā)M1,就會(huì)造成M1被重復(fù)消費(fèi)。也就是我們后面要說(shuō)的第二個(gè)問(wèn)題,消息重復(fù)問(wèn)題。
回過(guò)頭來(lái)看消息順序問(wèn)題,嚴(yán)格的順序消息非常容易理解,而且處理問(wèn)題也比較容易,要實(shí)現(xiàn)嚴(yán)格的順序消息,簡(jiǎn)單且可行的辦法就是:
保證生產(chǎn)者 - MQServer - 消費(fèi)者是一對(duì)一對(duì)一的關(guān)系
但是這樣設(shè)計(jì),并行度就成為了消息系統(tǒng)的瓶頸(吞吐量不夠),也會(huì)導(dǎo)致更多的異常處理,比如:只要消費(fèi)端出現(xiàn)問(wèn)題,就會(huì)導(dǎo)致整個(gè)處理流程阻塞,我們不得不花費(fèi)更多的精力來(lái)解決阻塞的問(wèn)題。
但我們的最終目標(biāo)是要集群的高容錯(cuò)性和高吞吐量。這似乎是一對(duì)不可調(diào)和的矛盾,那么阿里是如何解決的?
世界上解決一個(gè)計(jì)算機(jī)問(wèn)題最簡(jiǎn)單的方法:“恰好”不需要解決它!—— 沈詢
有些問(wèn)題,看起來(lái)很重要,但實(shí)際上我們可以通過(guò)合理的設(shè)計(jì)或者將問(wèn)題分解來(lái)規(guī)避。如果硬要把時(shí)間花在解決它們身上,實(shí)際上是浪費(fèi)的,效率低下的。從這個(gè)角度來(lái)看消息的順序問(wèn)題,我們可以得出兩個(gè)結(jié)論:
1、不關(guān)注亂序的應(yīng)用實(shí)際大量存在
2、隊(duì)列無(wú)序并不意味著消息無(wú)序
最后我們從源碼角度分析RocketMQ怎么實(shí)現(xiàn)發(fā)送順序消息。
一般消息是通過(guò)輪詢所有隊(duì)列來(lái)發(fā)送的(負(fù)載均衡策略),順序消息可以根據(jù)業(yè)務(wù),比如說(shuō)訂單號(hào)相同的消息發(fā)送到同一個(gè)隊(duì)列。下面的示例中,OrderId相同的消息,會(huì)發(fā)送到同一個(gè)隊(duì)列:
// RocketMQ默認(rèn)提供了兩種MessageQueueSelector實(shí)現(xiàn):隨機(jī)/Hash
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List mqs, Message msg, Object arg) {
Integer id = (Integer) arg;
int index = id % mqs.size();
return mqs.get(index);
}
}, orderId);
在獲取到路由信息以后,會(huì)根據(jù)MessageQueueSelector實(shí)現(xiàn)的算法來(lái)選擇一個(gè)隊(duì)列,同一個(gè)OrderId獲取到的隊(duì)列是同一個(gè)隊(duì)列。
private SendResult send() {
// 獲取topic路由信息
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
if (topicPublishInfo != null && topicPublishInfo.ok()) {
MessageQueue mq = null;
// 根據(jù)我們的算法,選擇一個(gè)發(fā)送隊(duì)列
// 這里的arg = orderId
mq = selector.select(topicPublishInfo.getMessageQueueList(), msg, arg);
if (mq != null) {
return this.sendKernelImpl(msg, mq, communicationMode, sendCallback, timeout);
}
}
}
二、消息重復(fù)
上面在解決消息順序問(wèn)題時(shí),引入了一個(gè)新的問(wèn)題,就是消息重復(fù)。那么RocketMQ是怎樣解決消息重復(fù)的問(wèn)題呢?還是“恰好”不解決。
造成消息的重復(fù)的根本原因是:網(wǎng)絡(luò)不可達(dá)。只要通過(guò)網(wǎng)絡(luò)交換數(shù)據(jù),就無(wú)法避免這個(gè)問(wèn)題。所以解決這個(gè)問(wèn)題的辦法就是不解決,轉(zhuǎn)而繞過(guò)這個(gè)問(wèn)題。那么問(wèn)題就變成了:如果消費(fèi)端收到兩條一樣的消息,應(yīng)該怎樣處理?
1、消費(fèi)端處理消息的業(yè)務(wù)邏輯保持冪等性
2、保證每條消息都有唯一編號(hào)且保證消息處理成功與去重表的日志同時(shí)出現(xiàn)
第1條很好理解,只要保持冪等性,不管來(lái)多少條重復(fù)消息,最后處理的結(jié)果都一樣。第2條原理就是利用一張日志表來(lái)記錄已經(jīng)處理成功的消息的ID,如果新到的消息ID已經(jīng)在日志表中,那么就不再處理這條消息。
我們可以看到第1條的解決方式,很明顯應(yīng)該在消費(fèi)端實(shí)現(xiàn),不屬于消息系統(tǒng)要實(shí)現(xiàn)的功能。第2條可以消息系統(tǒng)實(shí)現(xiàn),也可以業(yè)務(wù)端實(shí)現(xiàn)。正常情況下出現(xiàn)重復(fù)消息的概率不一定大,且由消息系統(tǒng)實(shí)現(xiàn)的話,肯定會(huì)對(duì)消息系統(tǒng)的吞吐量和高可用有影響,所以最好還是由業(yè)務(wù)端自己處理消息重復(fù)的問(wèn)題,這也是RocketMQ不解決消息重復(fù)的問(wèn)題的原因。
RocketMQ不保證消息不重復(fù),如果你的業(yè)務(wù)需要保證嚴(yán)格的不重復(fù)消息,需要你自己在業(yè)務(wù)端去重。
三、事務(wù)消息
RocketMQ除了支持普通消息,順序消息,另外還支持事務(wù)消息。首先討論一下什么是事務(wù)消息以及支持事務(wù)消息的必要性。我們以一個(gè)轉(zhuǎn)帳的場(chǎng)景為例來(lái)說(shuō)明這個(gè)問(wèn)題:Bob向Smith轉(zhuǎn)賬100塊。
在單機(jī)環(huán)境下,執(zhí)行事務(wù)的情況,大概是下面這個(gè)樣子:
單機(jī)環(huán)境下轉(zhuǎn)賬事務(wù)示意圖
當(dāng)用戶增長(zhǎng)到一定程度,Bob和Smith的賬戶及余額信息已經(jīng)不在同一臺(tái)服務(wù)器上了,那么上面的流程就變成了這樣:
集群環(huán)境下轉(zhuǎn)賬事務(wù)示意圖
這時(shí)候你會(huì)發(fā)現(xiàn),同樣是一個(gè)轉(zhuǎn)賬的業(yè)務(wù),在集群環(huán)境下,耗時(shí)居然成倍的增長(zhǎng),這顯然是不能夠接受的。那我們?nèi)绾蝸?lái)規(guī)避這個(gè)問(wèn)題?
大事務(wù) = 小事務(wù) + 異步
將大事務(wù)拆分成多個(gè)小事務(wù)異步執(zhí)行。這樣基本上能夠?qū)⒖鐧C(jī)事務(wù)的執(zhí)行效率優(yōu)化到與單機(jī)一致。轉(zhuǎn)賬的事務(wù)就可以分解成如下兩個(gè)小事務(wù):
小事務(wù)+異步消息
圖中執(zhí)行本地事務(wù)(Bob賬戶扣款)和發(fā)送異步消息應(yīng)該保持同時(shí)成功或者失敗中,也就是扣款成功了,發(fā)送消息一定要成功,如果扣款失敗了,就不能再發(fā)送消息。那問(wèn)題是:我們是先扣款還是先發(fā)送消息呢?
首先我們看下,先發(fā)送消息,大致的示意圖如下:
事務(wù)消息:先發(fā)送消息
存在的問(wèn)題是:如果消息發(fā)送成功,但是扣款失敗,消費(fèi)端就會(huì)消費(fèi)此消息,進(jìn)而向Smith賬戶加錢。
先發(fā)消息不行,那我們就先扣款唄,大致的示意圖如下:
事務(wù)消息-先扣款
存在的問(wèn)題跟上面類似:如果扣款成功,發(fā)送消息失敗,就會(huì)出現(xiàn)Bob扣錢了,但是Smith賬戶未加錢。
可能大家會(huì)有很多的方法來(lái)解決這個(gè)問(wèn)題,比如:直接將發(fā)消息放到Bob扣款的事務(wù)中去,如果發(fā)送失敗,拋出異常,事務(wù)回滾。這樣的處理方式也符合“恰好”不需要解決的原則。RocketMQ支持事務(wù)消息,下面我們來(lái)看看RocketMQ是怎樣來(lái)實(shí)現(xiàn)的。
RocketMQ實(shí)現(xiàn)發(fā)送事務(wù)消息
RocketMQ第一階段發(fā)送Prepared消息時(shí),會(huì)拿到消息的地址,第二階段執(zhí)行本地事物,第三階段通過(guò)第一階段拿到的地址去訪問(wèn)消息,并修改狀態(tài)。細(xì)心的你可能又發(fā)現(xiàn)問(wèn)題了,如果確認(rèn)消息發(fā)送失敗了怎么辦?RocketMQ會(huì)定期掃描消息集群中的事物消息,這時(shí)候發(fā)現(xiàn)了Prepared消息,它會(huì)向消息發(fā)送者確認(rèn),Bob的錢到底是減了還是沒(méi)減呢?如果減了是回滾還是繼續(xù)發(fā)送確認(rèn)消息呢?RocketMQ會(huì)根據(jù)發(fā)送端設(shè)置的策略來(lái)決定是回滾還是繼續(xù)發(fā)送確認(rèn)消息。這樣就保證了消息發(fā)送與本地事務(wù)同時(shí)成功或同時(shí)失敗。
那我們來(lái)看下RocketMQ源碼,是不是這樣來(lái)處理事務(wù)消息的??蛻舳税l(fā)送事務(wù)消息的部分(完整代碼請(qǐng)查看:rocketmq-example工程下的com.alibaba.rocketmq.example.transaction.TransactionProducer):
// 未決事務(wù),MQ服務(wù)器回查客戶端
// 也就是上文所說(shuō)的,當(dāng)RocketMQ發(fā)現(xiàn)Prepared消息時(shí),會(huì)根據(jù)這個(gè)Listener實(shí)現(xiàn)的策略來(lái)決斷事務(wù)
TransactionCheckListener transactionCheckListener = new TransactionCheckListenerImpl();
// 構(gòu)造事務(wù)消息的生產(chǎn)者
TransactionMQProducer producer = new TransactionMQProducer("groupName");
// 設(shè)置事務(wù)決斷處理類
producer.setTransactionCheckListener(transactionCheckListener);
// 本地事務(wù)的處理邏輯,相當(dāng)于示例中檢查Bob賬戶并扣錢的邏輯
TransactionExecuterImpl tranExecuter = new TransactionExecuterImpl();
producer.start()
// 構(gòu)造MSG,省略構(gòu)造參數(shù)
Message msg = new Message(......);
// 發(fā)送消息
SendResult sendResult = producer.sendMessageInTransaction(msg, tranExecuter, null);
producer.shutdown();
接著查看sendMessageInTransaction方法的源碼,總共分為3個(gè)階段:發(fā)送Prepared消息、執(zhí)行本地事務(wù)、發(fā)送確認(rèn)消息。
public TransactionSendResult sendMessageInTransaction(.....) {
// 邏輯代碼,非實(shí)際代碼
// 1.發(fā)送消息
sendResult = this.send(msg);
// sendResult.getSendStatus() == SEND_OK
// 2.如果消息發(fā)送成功,處理與消息關(guān)聯(lián)的本地事務(wù)單元
LocalTransactionState localTransactionState = tranExecuter.executeLocalTransactionBranch(msg, arg);
// 3.結(jié)束事務(wù)
this.endTransaction(sendResult, localTransactionState, localException);
}
endTransaction方法會(huì)將請(qǐng)求發(fā)往broker(mq server)去更新事物消息的最終狀態(tài):
根據(jù)sendResult找到Prepared消息
根據(jù)localTransaction更新消息的最終狀態(tài)
如果endTransaction方法執(zhí)行失敗,導(dǎo)致數(shù)據(jù)沒(méi)有發(fā)送到broker,broker會(huì)有回查線程定時(shí)(默認(rèn)1分鐘)掃描每個(gè)存儲(chǔ)事務(wù)狀態(tài)的表格文件,如果是已經(jīng)提交或者回滾的消息直接跳過(guò),如果是prepared狀態(tài)則會(huì)向Producer發(fā)起CheckTransaction請(qǐng)求,Producer會(huì)調(diào)用DefaultMQProducerImpl.checkTransactionState()方法來(lái)處理broker的定時(shí)回調(diào)請(qǐng)求,而checkTransactionState會(huì)調(diào)用我們的事務(wù)設(shè)置的決斷方法,最后調(diào)用endTransactionOneway讓broker來(lái)更新消息的最終狀態(tài)。
再回到轉(zhuǎn)賬的例子,如果Bob的賬戶的余額已經(jīng)減少,且消息已經(jīng)發(fā)送成功,Smith端開始消費(fèi)這條消息,這個(gè)時(shí)候就會(huì)出現(xiàn)消費(fèi)失敗和消費(fèi)超時(shí)兩個(gè)問(wèn)題?解決超時(shí)問(wèn)題的思路就是一直重試,直到消費(fèi)端消費(fèi)消息成功,整個(gè)過(guò)程中有可能會(huì)出現(xiàn)消息重復(fù)的問(wèn)題,按照前面的思路解決即可。
消費(fèi)事務(wù)消息
這樣基本上可以解決超時(shí)問(wèn)題,但是如果消費(fèi)失敗怎么辦?阿里提供給我們的解決方法是:人工解決。大家可以考慮一下,按照事務(wù)的流程,因?yàn)槟撤N原因Smith加款失敗,需要回滾整個(gè)流程。如果消息系統(tǒng)要實(shí)現(xiàn)這個(gè)回滾流程的話,系統(tǒng)復(fù)雜度將大大提升,且很容易出現(xiàn)Bug,估計(jì)出現(xiàn)Bug的概率會(huì)比消費(fèi)失敗的概率大很多。我們需要衡量是否值得花這么大的代價(jià)來(lái)解決這樣一個(gè)出現(xiàn)概率非常小的問(wèn)題,這也是大家在解決疑難問(wèn)題時(shí)需要多多思考的地方。
20160321補(bǔ)充:在3.2.6版本中移除了事務(wù)消息的實(shí)現(xiàn),所以此版本不支持事務(wù)消息,具體情況請(qǐng)參考rocketmq的issues:
https://github.com/alibaba/RocketMQ/issues/65
https://github.com/alibaba/RocketMQ/issues/138
https://github.com/alibaba/RocketMQ/issues/156
四、Producer如何發(fā)送消息
Producer輪詢某topic下的所有隊(duì)列的方式來(lái)實(shí)現(xiàn)發(fā)送方的負(fù)載均衡,如下圖所示:
producer發(fā)送消息負(fù)載均衡
首先分析一下RocketMQ的客戶端發(fā)送消息的源碼:
// 構(gòu)造Producer
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
// 初始化Producer,整個(gè)應(yīng)用生命周期內(nèi),只需要初始化1次
producer.start();
// 構(gòu)造Message
Message msg = new Message("TopicTest1",// topic
"TagA",// tag:給消息打標(biāo)簽,用于區(qū)分一類消息,可為null
"OrderID188",// key:自定義Key,可以用于去重,可為null
("Hello MetaQ").getBytes());// body:消息內(nèi)容
// 發(fā)送消息并返回結(jié)果
SendResult sendResult = producer.send(msg);
// 清理資源,關(guān)閉網(wǎng)絡(luò)連接,注銷自己
producer.shutdown();
在整個(gè)應(yīng)用生命周期內(nèi),生產(chǎn)者需要調(diào)用一次start方法來(lái)初始化,初始化主要完成的任務(wù)有:
如果沒(méi)有指定namesrv地址,將會(huì)自動(dòng)尋址
啟動(dòng)定時(shí)任務(wù):更新namesrv地址、從namsrv更新topic路由信息、清理已經(jīng)掛掉的broker、向所有broker發(fā)送心跳...
啟動(dòng)負(fù)載均衡的服務(wù)
初始化完成后,開始發(fā)送消息,發(fā)送消息的主要代碼如下:
private SendResult sendDefaultImpl(Message msg,......) {
// 檢查Producer的狀態(tài)是否是RUNNING
this.makeSureStateOK();
// 檢查msg是否合法:是否為null、topic,body是否為空、body是否超長(zhǎng)
Validators.checkMessage(msg, this.defaultMQProducer);
// 獲取topic路由信息
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
// 從路由信息中選擇一個(gè)消息隊(duì)列
MessageQueue mq = topicPublishInfo.selectOneMessageQueue(lastBrokerName);
// 將消息發(fā)送到該隊(duì)列上去
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, timeout);
}
代碼中需要關(guān)注的兩個(gè)方法tryToFindTopicPublishInfo和selectOneMessageQueue。前面說(shuō)過(guò)在producer初始化時(shí),會(huì)啟動(dòng)定時(shí)任務(wù)獲取路由信息并更新到本地緩存,所以tryToFindTopicPublishInfo會(huì)首先從緩存中獲取topic路由信息,如果沒(méi)有獲取到,則會(huì)自己去namesrv獲取路由信息。selectOneMessageQueue方法通過(guò)輪詢的方式,返回一個(gè)隊(duì)列,以達(dá)到負(fù)載均衡的目的。
如果Producer發(fā)送消息失敗,會(huì)自動(dòng)重試,重試的策略:
重試次數(shù) < retryTimesWhenSendFailed(可配置)
總的耗時(shí)(包含重試n次的耗時(shí)) < sendMsgTimeout(發(fā)送消息時(shí)傳入的參數(shù))
同時(shí)滿足上面兩個(gè)條件后,Producer會(huì)選擇另外一個(gè)隊(duì)列發(fā)送消息
五、消息存儲(chǔ)
RocketMQ的消息存儲(chǔ)是由consume queue和commit log配合完成的。
1、Consume Queue
consume queue是消息的邏輯隊(duì)列,相當(dāng)于字典的目錄,用來(lái)指定消息在物理文件commit log上的位置。
我們可以在配置中指定consumequeue與commitlog存儲(chǔ)的目錄
每個(gè)topic下的每個(gè)queue都有一個(gè)對(duì)應(yīng)的consumequeue文件,比如:
{topicName}/
{fileName}
Consume Queue文件組織,如圖所示:
Consume Queue文件組織示意圖
根據(jù)topic和queueId來(lái)組織文件,圖中TopicA有兩個(gè)隊(duì)列0,1,那么TopicA和QueueId=0組成一個(gè)ConsumeQueue,TopicA和QueueId=1組成另一個(gè)ConsumeQueue。
按照消費(fèi)端的GroupName來(lái)分組重試隊(duì)列,如果消費(fèi)端消費(fèi)失敗,消息將被發(fā)往重試隊(duì)列中,比如圖中的%RETRY%ConsumerGroupA。
按照消費(fèi)端的GroupName來(lái)分組死信隊(duì)列,如果消費(fèi)端消費(fèi)失敗,并重試指定次數(shù)后,仍然失敗,則發(fā)往死信隊(duì)列,比如圖中的%DLQ%ConsumerGroupA。
死信隊(duì)列(Dead Letter Queue)一般用于存放由于某種原因無(wú)法傳遞的消息,比如處理失敗或者已經(jīng)過(guò)期的消息。
Consume Queue中存儲(chǔ)單元是一個(gè)20字節(jié)定長(zhǎng)的二進(jìn)制數(shù)據(jù),順序?qū)戫樞蜃x,如下圖所示:
consumequeue文件存儲(chǔ)單元格式
CommitLog Offset是指這條消息在Commit Log文件中的實(shí)際偏移量
Size存儲(chǔ)中消息的大小
Message Tag HashCode存儲(chǔ)消息的Tag的哈希值:主要用于訂閱時(shí)消息過(guò)濾(訂閱時(shí)如果指定了Tag,會(huì)根據(jù)HashCode來(lái)快速查找到訂閱的消息)
2、Commit Log
CommitLog:消息存放的物理文件,每臺(tái)broker上的commitlog被本機(jī)所有的queue共享,不做任何區(qū)分。
文件的默認(rèn)位置如下,仍然可通過(guò)配置文件修改:
{commitlog}${fileName}
CommitLog的消息存儲(chǔ)單元長(zhǎng)度不固定,文件順序?qū)?,隨機(jī)讀。消息的存儲(chǔ)結(jié)構(gòu)如下表所示,按照編號(hào)順序以及編號(hào)對(duì)應(yīng)的內(nèi)容依次存儲(chǔ)。
Commit Log存儲(chǔ)單元結(jié)構(gòu)圖
3、消息存儲(chǔ)實(shí)現(xiàn)
消息存儲(chǔ)實(shí)現(xiàn),比較復(fù)雜,也值得大家深入了解,后面會(huì)單獨(dú)成文來(lái)分析,這小節(jié)只以代碼說(shuō)明一下具體的流程。
// Set the storage time
msg.setStoreTimestamp(System.currentTimeMillis());
// Set the message body BODY CRC (consider the most appropriate setting
msg.setBodyCRC(UtilAll.crc32(msg.getBody()));
StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService();
synchronized (this) {
long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now();
// Here settings are stored timestamp, in order to ensure an orderly global
msg.setStoreTimestamp(beginLockTimestamp);
// MapedFile:操作物理文件在內(nèi)存中的映射以及將內(nèi)存數(shù)據(jù)持久化到物理文件中
MapedFile mapedFile = this.mapedFileQueue.getLastMapedFile();
// 將Message追加到文件commitlog
result = mapedFile.appendMessage(msg, this.appendMessageCallback);
switch (result.getStatus()) {
case PUT_OK:break;
case END_OF_FILE:
// Create a new file, re-write the message
mapedFile = this.mapedFileQueue.getLastMapedFile();
result = mapedFile.appendMessage(msg, this.appendMessageCallback);
break;
DispatchRequest dispatchRequest = new DispatchRequest(
topic,// 1
queueId,// 2
result.getWroteOffset(),// 3
result.getWroteBytes(),// 4
tagsCode,// 5
msg.getStoreTimestamp(),// 6
result.getLogicsOffset(),// 7
msg.getKeys(),// 8
/**
- Transaction
*/
msg.getSysFlag(),// 9
msg.getPreparedTransactionOffset());// 10
// 1.分發(fā)消息位置到ConsumeQueue
// 2.分發(fā)到IndexService建立索引
this.defaultMessageStore.putDispatchRequest(dispatchRequest);
}
4、消息的索引文件
如果一個(gè)消息包含key值的話,會(huì)使用IndexFile存儲(chǔ)消息索引,文件的內(nèi)容結(jié)構(gòu)如圖:
消息索引
索引文件主要用于根據(jù)key來(lái)查詢消息的,流程主要是:
根據(jù)查詢的 key 的 hashcode%slotNum 得到具體的槽的位置(slotNum 是一個(gè)索引文件里面包含的最大槽的數(shù)目,例如圖中所示 slotNum=5000000)
根據(jù) slotValue(slot 位置對(duì)應(yīng)的值)查找到索引項(xiàng)列表的最后一項(xiàng)(倒序排列,slotValue 總是指向最新的一個(gè)索引項(xiàng))
遍歷索引項(xiàng)列表返回查詢時(shí)間范圍內(nèi)的結(jié)果集(默認(rèn)一次最大返回的 32 條記錄)
六、消息訂閱
RocketMQ消息訂閱有兩種模式,一種是Push模式,即MQServer主動(dòng)向消費(fèi)端推送;另外一種是Pull模式,即消費(fèi)端在需要時(shí),主動(dòng)到MQServer拉取。但在具體實(shí)現(xiàn)時(shí),Push和Pull模式都是采用消費(fèi)端主動(dòng)拉取的方式。
首先看下消費(fèi)端的負(fù)載均衡:
消費(fèi)端負(fù)載均衡
消費(fèi)端會(huì)通過(guò)RebalanceService線程,10秒鐘做一次基于topic下的所有隊(duì)列負(fù)載:
遍歷Consumer下的所有topic,然后根據(jù)topic訂閱所有的消息
獲取同一topic和Consumer Group下的所有Consumer
然后根據(jù)具體的分配策略來(lái)分配消費(fèi)隊(duì)列,分配的策略包含:平均分配、消費(fèi)端配置等
如同上圖所示:如果有 5 個(gè)隊(duì)列,2 個(gè) consumer,那么第一個(gè) Consumer 消費(fèi) 3 個(gè)隊(duì)列,第二 consumer 消費(fèi) 2 個(gè)隊(duì)列。這里采用的就是平均分配策略,它類似于我們的分頁(yè),TOPIC下面的所有queue就是記錄,Consumer的個(gè)數(shù)就相當(dāng)于總的頁(yè)數(shù),那么每頁(yè)有多少條記錄,就類似于某個(gè)Consumer會(huì)消費(fèi)哪些隊(duì)列。
通過(guò)這樣的策略來(lái)達(dá)到大體上的平均消費(fèi),這樣的設(shè)計(jì)也可以很方面的水平擴(kuò)展Consumer來(lái)提高消費(fèi)能力。
消費(fèi)端的Push模式是通過(guò)長(zhǎng)輪詢的模式來(lái)實(shí)現(xiàn)的,就如同下圖:
Push模式示意圖
Consumer端每隔一段時(shí)間主動(dòng)向broker發(fā)送拉消息請(qǐng)求,broker在收到Pull請(qǐng)求后,如果有消息就立即返回?cái)?shù)據(jù),Consumer端收到返回的消息后,再回調(diào)消費(fèi)者設(shè)置的Listener方法。如果broker在收到Pull請(qǐng)求時(shí),消息隊(duì)列里沒(méi)有數(shù)據(jù),broker端會(huì)阻塞請(qǐng)求直到有數(shù)據(jù)傳遞或超時(shí)才返回。
當(dāng)然,Consumer端是通過(guò)一個(gè)線程將阻塞隊(duì)列LinkedBlockingQueue中的PullRequest發(fā)送到broker拉取消息,以防止Consumer一致被阻塞。而Broker端,在接收到Consumer的PullRequest時(shí),如果發(fā)現(xiàn)沒(méi)有消息,就會(huì)把PullRequest扔到ConcurrentHashMap中緩存起來(lái)。broker在啟動(dòng)時(shí),會(huì)啟動(dòng)一個(gè)線程不停的從ConcurrentHashMap取出PullRequest檢查,直到有數(shù)據(jù)返回。
七、RocketMQ的其他特性
前面的6個(gè)特性都是基本上都是點(diǎn)到為止,想要深入了解,還需要大家多多查看源碼,多多在實(shí)際中運(yùn)用。當(dāng)然除了已經(jīng)提到的特性外,RocketMQ還支持:
定時(shí)消息
消息的刷盤策略
主動(dòng)同步策略:同步雙寫、異步復(fù)制
海量消息堆積能力
高效通信
.......
其中涉及到的很多設(shè)計(jì)思路和解決方法都值得我們深入研究:
消息的存儲(chǔ)設(shè)計(jì):既要滿足海量消息的堆積能力,又要滿足極快的查詢效率,還要保證寫入的效率。
高效的通信組件設(shè)計(jì):高吞吐量,毫秒級(jí)的消息投遞能力都離不開高效的通信。
.......
RocketMQ最佳實(shí)踐
一、Producer最佳實(shí)踐
1、一個(gè)應(yīng)用盡可能用一個(gè) Topic,消息子類型用 tags 來(lái)標(biāo)識(shí),tags 可以由應(yīng)用自由設(shè)置。只有發(fā)送消息設(shè)置了tags,消費(fèi)方在訂閱消息時(shí),才可以利用 tags 在 broker 做消息過(guò)濾。
2、每個(gè)消息在業(yè)務(wù)層面的唯一標(biāo)識(shí)碼,要設(shè)置到 keys 字段,方便將來(lái)定位消息丟失問(wèn)題。由于是哈希索引,請(qǐng)務(wù)必保證 key 盡可能唯一,這樣可以避免潛在的哈希沖突。
3、消息發(fā)送成功或者失敗,要打印消息日志,務(wù)必要打印 sendresult 和 key 字段。
4、對(duì)于消息不可丟失應(yīng)用,務(wù)必要有消息重發(fā)機(jī)制。例如:消息發(fā)送失敗,存儲(chǔ)到數(shù)據(jù)庫(kù),能有定時(shí)程序嘗試重發(fā)或者人工觸發(fā)重發(fā)。
5、某些應(yīng)用如果不關(guān)注消息是否發(fā)送成功,請(qǐng)直接使用sendOneWay方法發(fā)送消息。
二、Consumer最佳實(shí)踐
1、消費(fèi)過(guò)程要做到冪等(即消費(fèi)端去重)
2、盡量使用批量方式消費(fèi)方式,可以很大程度上提高消費(fèi)吞吐量。
3、優(yōu)化每條消息消費(fèi)過(guò)程
三、其他配置
線上應(yīng)該關(guān)閉autoCreateTopicEnable,即在配置文件中將其設(shè)置為false。
RocketMQ在發(fā)送消息時(shí),會(huì)首先獲取路由信息。如果是新的消息,由于MQServer上面還沒(méi)有創(chuàng)建對(duì)應(yīng)的Topic,這個(gè)時(shí)候,如果上面的配置打開的話,會(huì)返回默認(rèn)TOPIC的(RocketMQ會(huì)在每臺(tái)broker上面創(chuàng)建名為TBW102的TOPIC)路由信息,然后Producer會(huì)選擇一臺(tái)Broker發(fā)送消息,選中的broker在存儲(chǔ)消息時(shí),發(fā)現(xiàn)消息的topic還沒(méi)有創(chuàng)建,就會(huì)自動(dòng)創(chuàng)建topic。后果就是:以后所有該TOPIC的消息,都將發(fā)送到這臺(tái)broker上,達(dá)不到負(fù)載均衡的目的。
所以基于目前RocketMQ的設(shè)計(jì),建議關(guān)閉自動(dòng)創(chuàng)建TOPIC的功能,然后根據(jù)消息量的大小,手動(dòng)創(chuàng)建TOPIC。
RocketMQ設(shè)計(jì)相關(guān)
RocketMQ的設(shè)計(jì)假定:
每臺(tái)PC機(jī)器都可能宕機(jī)不可服務(wù)
任意集群都有可能處理能力不足
最壞的情況一定會(huì)發(fā)生
內(nèi)網(wǎng)環(huán)境需要低延遲來(lái)提供最佳用戶體驗(yàn)
RocketMQ的關(guān)鍵設(shè)計(jì):
分布式集群化
強(qiáng)數(shù)據(jù)安全
海量數(shù)據(jù)堆積
毫秒級(jí)投遞延遲(推拉模式)
這是RocketMQ在設(shè)計(jì)時(shí)的假定前提以及需要到達(dá)的效果。我想這些假定適用于所有的系統(tǒng)設(shè)計(jì)。隨著我們系統(tǒng)的服務(wù)的增多,每位開發(fā)者都要注意自己的程序是否存在單點(diǎn)故障,如果掛了應(yīng)該怎么恢復(fù)、能不能很好的水平擴(kuò)展、對(duì)外的接口是否足夠高效、自己管理的數(shù)據(jù)是否足夠安全...... 多多規(guī)范自己的設(shè)計(jì),才能開發(fā)出高效健壯的程序。
附錄:RocketMQ涉及到的幾個(gè)專業(yè)術(shù)語(yǔ)和整體架構(gòu)介紹
一、RocketMQ中的專業(yè)術(shù)語(yǔ)
Topic
topic表示消息的第一級(jí)類型,比如一個(gè)電商系統(tǒng)的消息可以分為:交易消息、物流消息...... 一條消息必須有一個(gè)Topic。
Tag
Tag表示消息的第二級(jí)類型,比如交易消息又可以分為:交易創(chuàng)建消息,交易完成消息..... 一條消息可以沒(méi)有Tag。RocketMQ提供2級(jí)消息分類,方便大家靈活控制。
Queue
一個(gè)topic下,我們可以設(shè)置多個(gè)queue(消息隊(duì)列)。當(dāng)我們發(fā)送消息時(shí),需要要指定該消息的topic。RocketMQ會(huì)輪詢?cè)搕opic下的所有隊(duì)列,將消息發(fā)送出去。
Producer 與 Producer Group
Producer表示消息隊(duì)列的生產(chǎn)者。消息隊(duì)列的本質(zhì)就是實(shí)現(xiàn)了publish-subscribe模式,生產(chǎn)者生產(chǎn)消息,消費(fèi)者消費(fèi)消息。所以這里的Producer就是用來(lái)生產(chǎn)和發(fā)送消息的,一般指業(yè)務(wù)系統(tǒng)。
Producer Group是一類Producer的集合名稱,這類Producer通常發(fā)送一類消息,且發(fā)送邏輯一致。
Consumer 與 Consumer Group
消息消費(fèi)者,一般由后臺(tái)系統(tǒng)異步消費(fèi)消息。
Push Consumer
Consumer 的一種,應(yīng)用通常向 Consumer 對(duì)象注冊(cè)一個(gè) Listener 接口,一旦收到消息,Consumer 對(duì)象立刻回調(diào) Listener 接口方法。
Pull Consumer
Consumer 的一種,應(yīng)用通常主動(dòng)調(diào)用 Consumer 的拉消息方法從 Broker 拉消息,主動(dòng)權(quán)由應(yīng)用控制。
Consumer Group是一類Consumer的集合名稱,這類Consumer通常消費(fèi)一類消息,且消費(fèi)邏輯一致。
Broker
消息的中轉(zhuǎn)者,負(fù)責(zé)存儲(chǔ)和轉(zhuǎn)發(fā)消息??梢岳斫鉃橄㈥?duì)列服務(wù)器,提供了消息的接收、存儲(chǔ)、拉取和轉(zhuǎn)發(fā)服務(wù)。broker是RocketMQ的核心,它不不能掛的,所以需要保證broker的高可用。
廣播消費(fèi)
一條消息被多個(gè)Consumer消費(fèi),即使這些Consumer屬于同一個(gè)Consumer Group,消息也會(huì)被Consumer Group中的每個(gè)Consumer都消費(fèi)一次。在廣播消費(fèi)中的Consumer Group概念可以認(rèn)為在消息劃分方面無(wú)意義。
集群消費(fèi)
一個(gè)Consumer Group中的Consumer實(shí)例平均分?jǐn)傁M(fèi)消息。例如某個(gè)Topic有 9 條消息,其中一個(gè)Consumer Group有 3 個(gè)實(shí)例(可能是 3 個(gè)進(jìn)程,或者 3 臺(tái)機(jī)器),那么每個(gè)實(shí)例只消費(fèi)其中的 3 條消息。
NameServer
NameServer即名稱服務(wù),兩個(gè)功能:
接收broker的請(qǐng)求,注冊(cè)broker的路由信息
接口client的請(qǐng)求,根據(jù)某個(gè)topic獲取其到broker的路由信息
NameServer沒(méi)有狀態(tài),可以橫向擴(kuò)展。每個(gè)broker在啟動(dòng)的時(shí)候會(huì)到NameServer注冊(cè);Producer在發(fā)送消息前會(huì)根據(jù)topic到NameServer獲取路由(到broker)信息;Consumer也會(huì)定時(shí)獲取topic路由信息。
二、RocketMQ Overview
rocketmq overview
Producer向一些隊(duì)列輪流發(fā)送消息,隊(duì)列集合稱為Topic,Consumer如果做廣播消費(fèi),則一個(gè)consumer實(shí)例消費(fèi)這個(gè)Topic對(duì)應(yīng)的所有隊(duì)列;如果做集群消費(fèi),則多個(gè)Consumer實(shí)例平均消費(fèi)這個(gè)Topic對(duì)應(yīng)的隊(duì)列集合。
再看下RocketMQ物理部署結(jié)構(gòu)圖:
RocketMQ網(wǎng)絡(luò)部署圖
RocketMQ網(wǎng)絡(luò)部署特點(diǎn):
Name Server 是一個(gè)幾乎無(wú)狀態(tài)節(jié)點(diǎn),可集群部署,節(jié)點(diǎn)之間無(wú)任何信息同步。
Broker部署相對(duì)復(fù)雜,Broker分為Master與Slave,一個(gè)Master可以對(duì)應(yīng)多個(gè)Slave,但是一個(gè)Slave只能對(duì)應(yīng)一個(gè)Master,Master與Slave的對(duì)應(yīng)關(guān)系通過(guò)指定相同的BrokerName,不同的BrokerId來(lái)定義,BrokerId=0表示Master,非0表示Slave。Master也可以部署多個(gè)。每個(gè)Broker與Name Server集群中的所有節(jié)點(diǎn)建立長(zhǎng)連接,定時(shí)注冊(cè)Topic信息到所有Name Server。
Producer與Name Server集群中的其中一個(gè)節(jié)點(diǎn)(隨機(jī)選擇)建立長(zhǎng)連接,定期從Name Server取Topic路由信息,并向提供Topic 服務(wù)的Master建立長(zhǎng)連接,且定時(shí)向Master發(fā)送心跳。Producer 完全無(wú)狀態(tài),可集群部署。
Consumer與Name Server集群中的其中一個(gè)節(jié)點(diǎn)(隨機(jī)選擇)建立長(zhǎng)連接,定期從Name Server取Topic 路由信息,并向提供Topic服務(wù)的Master、Slave建立長(zhǎng)連接,且定時(shí)向Master、Slave發(fā)送心跳。Consumer既可以從Master訂閱消息,也可以從Slave訂閱消息,訂閱規(guī)則由Broker配置決定。
三、其他參考資料
(如需查看請(qǐng)向本號(hào)發(fā)送“RocketMQ”)
RocketMQ用戶指南
RocketMQ原理簡(jiǎn)介
RocketMQ最佳實(shí)踐
阿里分布式開放消息服務(wù)(ONS)原理與實(shí)踐2
阿里分布式開放消息服務(wù)(ONS)原理與實(shí)踐3
RocketMQ原理解析
推薦關(guān)鍵詞「分布式系統(tǒng)」閱讀:
騰訊文學(xué)內(nèi)容中心分布式文件系統(tǒng)的設(shè)計(jì)和實(shí)現(xiàn)
億級(jí)Web系統(tǒng)搭建:?jiǎn)螜C(jī)到分布式集群
大型SOA架構(gòu)體系里的數(shù)據(jù)一致性問(wèn)題
騰訊計(jì)費(fèi)平臺(tái)部:分布式MySQL數(shù)據(jù)庫(kù)TDSQL架構(gòu)分析
可擴(kuò)展Web架構(gòu)與分布式系統(tǒng)
途牛網(wǎng)站無(wú)線架構(gòu)變遷實(shí)踐
注:直接點(diǎn)擊上述文章的標(biāo)題,即可查看相關(guān)文章。
版權(quán)申明:內(nèi)容來(lái)源網(wǎng)絡(luò),版權(quán)歸原創(chuàng)者所有。除非無(wú)法確認(rèn),我們都會(huì)標(biāo)明作者及出處,如有侵權(quán)煩請(qǐng)告知,我們會(huì)立即刪除并表示歉意。謝謝。
-END-
作者:meng_philip123
鏈接:http://www.itdecent.cn/p/468176c6bc1b