RocketMQ在存儲架構上的極致追求
Part one / 存儲結構選型對比
為了更方便的進行數據讀寫,消息在磁盤底層的文件目錄設計,都需要關注和解決什么問題呢:
- 首先,最基本的,消息原始記錄的寫入和存儲,且速率要快。
- 其次,要可以區(qū)分topic ,特別是允許消費者按topic進行接收。
- 再次,分布式集群下的多消費者負載均衡。
那么問題來了,消息文件該怎么設計呢?如果按topic來拆分文件進行存儲,是否可以?
- 缺點:生產者寫入時選擇對應的文件來寫入。當數據量逐漸增大之后,定位查詢文件地址,對磁盤的尋址所帶來的性能損耗,將不再可以忽略。
- 優(yōu)點:在消費時,可以直接加載相關文件進行讀取,不會產生隨機尋址。
如果用一整個文件來存消息呢?
- 優(yōu)點:所有的topic都被寫入一個文件中,這樣,寫入時,只要將消息按到達順序序追加到文件尾部即可,很容易實現順序寫入。
- 缺點:消費時,需要根據輔助信息來在文件中定位消息,會產生隨機讀,損耗性能。
因此,不管是按topic拆開多文件存儲,還是一整個文件存儲做有利有弊,需要按實際需要進行權衡。
Part two / RocketMQ的存儲方案選擇
RocketMQ存儲原始消息選擇的是寫同一個文件。
生產者將消息順序寫入commitLog文件究其原因,是由于RocketMQ一般都是普通業(yè)務場景使用居多,生產者和topic眾多,如果都獨立開各自存儲,每次消息生產的磁盤尋址對性能損耗是非常巨大的。
旁證側引:
kafka的文件存儲方式,是按topic拆分成partation來進行的。是什么樣的原因,讓kafka做出了和RocketMQ相反的選擇呢?
個人認為,主要還是使用場景的區(qū)別,kafka被優(yōu)先選擇用來進行大數據處理,相對于業(yè)務場景,數據維度的topic要少很多,并且kafka的生產者(spark flume binlog等)機器會更加集中,這使得kafka選擇按topic拆分文件的缺陷不那么突出,而大數據處理更重要的是消息讀取,順序讀的優(yōu)勢得以被充分利用。
"單partation,單cunsumer的kafka,性能異常的優(yōu)秀" 是經常被提及的一個觀點,其原因,相信有了上面的分析應該也差不多有結論了。
Part three / RocketMQ怎樣平衡讀性能
從第一部分的存儲方案對比可以知道,RocketMQ為了保證消息寫入效率,在存儲結構上選擇了順序寫,勢必會對消息的讀取和消費帶來不便。那么,它是怎么來平衡消費時的讀取速率的呢?關鍵問題是,找到一種途徑,可以快速的在commitLog中定位到所需消息的位置。從一堆數據中,快速定位想要的數據,這不是索引最擅長的事情么?所以,RocketMQ也為commitLog創(chuàng)建了索引文件,并且是區(qū)分topic的結構。
存儲架構和存儲構建鏈路示意圖
RocketMQ 的消息體構成
消息體元素構成
- topic 是業(yè)務場景的唯一標識,不可缺少;
- queueId 在申請topic的時候確定,關聯著消費索引consumerQueue中的隊列ID;
- tags 是消息特殊標簽,用于業(yè)務系統(tǒng)訂閱時提前過濾(這個功能真的是太重要了,吃過苦的同學都清楚);
- keys 是消息的關鍵字,構建index索引,用于關鍵字查詢用;
- msgBody 是真實消息體;
消息由發(fā)布者發(fā)布,并依次的、順序的寫到commitLog里,消息一旦被寫入,是不可以更改順序和內容的。commitLog規(guī)定最大1個G,達到規(guī)定大小則寫新的一個文件。
索引結構和構建過程
consumerQueue結構和創(chuàng)建過程
consumerQueue 是一種機制,可以讓消費端通過queue和commitLog之間的檢索關系,快速定位到commitLog里邊的具體消息內容,然后拉取進行消費。consumerQueue 按 topic的不同,被分為不同的queue,根據queueId來被消費者訂閱和消費;其中每個索引項是一個固定大小為20bytes的記錄,由消息在commitLog中的起始偏移量、消息體占用大小、type的hash碼三部分構成??梢酝ㄟ^這三個部分快速定位到所需消息位置和類型。而上述索引的構建過程,是在消息被寫入commitLog時,專門的后臺服務--putMessageService,將索引信息分發(fā)到 consumerQueue 和index文件里,來構建索引項。建索引的過程,實際上是一種分而治之思維的落地,除了索引,還有redis中的各種指標維護,核心是 分散壓力到每次請求,避免了大規(guī)模集中計算。
消息的消費
消費者對應consumerQueue不一定是一對一的,因此,怎么來讓每個新的消費者來了不會重復消費呢?
offset消費位點記錄在消息成功被拉取并消費時,后臺任務CommitOffsetManager 會將當前消費者,針對topic的消費位點進行記錄,目的是讓下一個或者重新啟動單餓消費者記住這個消費位點,不至于重復消費。因此,整個文件目錄就一目了然了:
Part four / 讀效率的追求
雖然通過上述文件存儲結構的分析,我們知道,消費者可以根據索引文件中的索引項來快速定位, 但事實上,消息的發(fā)布和消費,不可能直接針對磁盤進行讀寫操作的,這樣效率會非常非常低。
實際上,我們的操作基本是針對一塊內存進行操作的 。
利用NIO的內存映射機制,我們將commitLog的一部分文件交換到對外內存。然后利用操作系統(tǒng)的pageCache技術,在運行過程中把內存里的信息,與磁盤里的文件信息進行同步,或者交換:
- 消息發(fā)布者,在發(fā)布消息的時候,首先把消息添加到內存里,然后根據刷盤的配置可以來指定是同步刷盤還是異步刷盤,來將內存中的數據同步到磁盤上。
- 消息的消費者,在消費消息的時候,大多數情況下,會直接命中到內存上,不會進行磁盤讀,但極個別的情況下,需要消費的消息,在內存中沒法找到,這時候,就需要用換頁技術,將相關的信息,拉取到內存中。為什么是相關信息,而不是需要什么拉取什么?這是有一個機制,來保證潛在的即將被消費的信息直接換入內存,來提交效率。
摘自:Qcon大會 RocketMQ分享資料
Part five / 總結
整體一套處理流程看下來,其實我們可以看到很多熟悉的身影,比如Mysql的索引,redis的統(tǒng)計信息記錄等等,都非常相似。其實,我們可以這么認為:對于信息存儲和查詢的處理方案大都如出一轍,只要把握住最核心的部分,然后根據實際業(yè)務訴求進行適配優(yōu)化,基本都是可以達到期望的結果的。