前言
從本章開始,將會以系列的方式,分章節講解Rocket Mq是如何實現消息的消費模型。
消息的廣播或單播
不管是JMS規范中的Topic/Queue,還是Kafka的Topic/Paratiton/ConsumerGroup,或者是RabbitMq里的Exchange等,從本質上來說,無非是廣播與單播的區別。廣播,指的是單點對多點;而單播,則是點對點。當然,對于互聯網中的大部分應用來說,組間廣播,組內單播是最常見的事情。例如,在kafka中,消息會廣播給所有的不同ConsumerGroup,而在相同的ConsumerGroup的消費者中,只能由其中一個消費者消費某條消息隊列。RocketMq 使用同樣的方式來實現組間廣播,組內單播。
對于RocketMq 來說,它使用了 消息模式來 劃分 廣播與單播的概念,BROADCASTING
對應廣播,CLUSTERING
對應單播。當Cosnumer客戶端指定消息模式為BROADCASTING
時,該Consumer可以消費所訂閱topic對應的所有隊列。當Cosnumer客戶端指定消息模式為CLUSTERING
時,只能消費所訂閱topic的指定隊列,而CLUSTERING
模式中,又有并發消費和順序消費之分。我們通過【圖1-1】來詳細講解上述幾種模式:
在【圖1-1】中,有一個名為[topic_x]的topic
,并且在broker1與broker2均創建了4條隊列,換言之,對于所有的客戶端來說,[topic_x] 的視圖共有8條隊列。因此,對于同一個nameser集群中,可以通過topicName、brokerName以及queueId 可以確定集群中一條消費隊里。
有一個ConsumerGroup 為 [ConsumerGroup1]的消費者客戶端[consumer_client3],其消息模式 為BROADCASTING
,并且訂閱了topic_x,因此,消費者客戶端[consumer_client3]可以消費topic_x 在[broker1]中的4條隊列【queue0,queue1,queue2,queue4】以及[broker2]中的4條隊列【queue0,queue1,queue2,queue4】。
而組內單播是 是針對 設置了相同的[ConsumerGroup] 以及 消息模式為CLUSTERING
的 消費者客戶端來說的。
如 [ConsumerGroup1] 中的消費者客戶端[consumer_client1] 以及 消費者客戶端[consumer_client2] ,他們均為CLUSTERING
模式,因此根據隊列的分配算法,[consumer_client1] 消費[broker1]中的4條隊,[consumer_client2] 消費[broker2]中的4條隊。當然對于消費組[ConsumerGroup2]里的消費客戶端[consumer_client1]一樣可以消費[broker1]中的4條隊列,從而到達了組內單播,組外廣播的目的。
對于CLUSTERING
模式的消費者來說,又分順序消費與并發消費。以[ConsumerGroup1]中的[consumer_client1]為例,圖中,它消費了隊列[queu-0]。當[consumer_client1]為順序消費時,它的消費模式為,通過一條線程逐條消息消費,這樣一來,就可以保證了這條隊列里,消息被消費的順序性了,換言之,當業務系統希望依賴rmq的順序消費時,即可使用該特性。當[consumer_client1]為并發消費時,它的消費模式為,例如,這次消費端一次拉取了三條消息,那么,消費端會開啟三條線程,并發處理這三條消息。
消息的拉取方式
消息的獲取本質就兩種,一種是推送push,另一種是拉取pull。兩種方式各有什么優劣勢、業界是如何選擇的、我們來詳細分析一下。
push方式,就是由消息的存儲方broker主動把消息推給消費者,傳統的實現JMS標準的mq,例如rabbit mq,就是使用這種方式。這種方式好處就是消息的實時性很強,正常情況下,只要broker一接受到生產者發送的新消息,即可立即把消息推給消費者;并且,實現方式相對來說會簡單很多。但push方式有一個比較嚴重的問題,就是慢消費。怎么說?就是在消息的生產速度遠大于消費速度,并且這些消息都是無法丟失時,勢必會造成消息在broker里堆積。更嚴重的是,broker會推送一大堆consumer無法處理的消息,而consuemr不是拒絕reject就是出現錯誤。滿消費的情況多了,就會嚴重影響broker的整性能及吞吐量。
對于pull方式,即消費者主動向broker拉取消息,換言之,consumer可以實現按需消費,更不用考慮應為自身消費慢而受到處理不了的消息影響。相對的,broker處理消息堆積的方式也會簡單很多,broker只需維護所有的消息隊列以及對應的消息偏移量即可,無需記錄每條消息的發送情況。所以,對于慢消費、消息生產速度不均勻以及需要強大的消息堆積能力等情況下,使用pull模式就很合適。
pull模式也有一個很大的短板,就是消費方很難準確地決定拉取消息的時機。如果消費者在一次pull過程中取到了新的消息,那么,消費者可以繼續發起pull請求。但沒有拉取到消息時,則需要重新等待一段時間后,才會繼續拉取,因為我們不可能不斷向broker發起拉取消息請求,如果這樣做,一來會導致許多無用功,二來很可能會導致broker的性能下降。換言之,我們只能采取等待一段時間的方案。
但等多久就比較難以判斷了。業界比較成熟的方案是,等待從比較短的時間開始,然后指數級增長等待的時間。比如第一次等待5s,然后10s,在然后是20s...直到有新的消息到來,在重設等待時間為5s。但是不管怎么樣,還是會存在長時間等待或者做無用功的情況。例如,在等待10s后,消息立馬過來,可消息下次拉取的時間是20s,換言之,消息會存在20s的延時。又或者消息確實一個小時后在過來,那么在一個消息內,一樣會存在多次無用功。是不是比較沮喪。
我們來看看RocketMq是如何解決這個問題的。rmq獨辟蹊徑,使用了一種長輪詢的做法,來平衡推拉模型各自的缺點。何為為長輪詢,正常情況下,發起拉取消息請求,只要broker有消息返回,那么,消費者還會不等待,繼續發起拉取消息請求。直到拉取消息失敗,即無最新消息。這時,對于消費者來說,不是直接return掉,而是通過broker把拉取的請求await在那里,直到broker接受到新的消息后,在把請求notify。這也是一種很不錯的思路。但海量的阻塞拉取請求對于broker來說,開銷還是不小的,rmq通過合理的時間評估,給await加上一個合適的時間。
基于長輪詢的做法,Rocket Mq封裝了Push模式和Pull模式的客戶端,但核心還是pull模式。
我們將在接下來的章節中,從源碼上分析Rocket Mq 是如何實現上述所提到的全部細節