7張圖揭曉RocketMQ存儲(chǔ)設(shè)計(jì)的精髓
點(diǎn)擊上方“蘇三說(shuō)技術(shù)”,選擇“設(shè)為星標(biāo)”
RocketMQ作為一款基于磁盤(pán)存儲(chǔ)的中間件,具有無(wú)限積壓能力,并提供高吞吐、低延遲的服務(wù)能力,其最核心的部分必然是它優(yōu)雅的存儲(chǔ)設(shè)計(jì)。
溫馨提示:本文節(jié)選自新上市《RocketMQ技術(shù)內(nèi)幕》第二版本,一個(gè)最大的改變就是在進(jìn)入源碼分析之前,首先通過(guò)圖文的方式,提煉出RocketMQ的核心工作機(jī)制,降低源碼閱讀的難度,引發(fā)思考。
1、存儲(chǔ)概述
RocketMQ存儲(chǔ)的文件主要包括Commitlog文件、ConsumeQueue文件、Index文件。
RocketMQ將所有主題的消息存儲(chǔ)在同一個(gè)文件中,確保消息發(fā)送時(shí)按順序?qū)懳募M最大能力確保消息發(fā)送的高可用性與高吞吐量。
但消息中間件一般都是基于主題的訂閱與發(fā)布模式,消息消費(fèi)時(shí)必須按照主題進(jìn)行帥選消息,顯然從Commitlog文件中按照topic去篩選消息會(huì)變得及其低效,為了提高根據(jù)主題檢索消息的效率,RocketMQ引入了ConsumeQueue文件,俗成消費(fèi)隊(duì)列文件。
關(guān)系型數(shù)據(jù)庫(kù)可以按照字段屬性進(jìn)行記錄檢索,作為一款主要面向業(yè)務(wù)開(kāi)發(fā)的消息中間件,RocketMQ也提供了基于消息屬性的檢索能力,底層的核心設(shè)計(jì)理念是為Commitlog文件建立哈希索引,并存儲(chǔ)在Index文件中。
在RocketMQ中順序?qū)懭氲紺ommitlog文件后,ConsumeQueue與Index文件都是異步構(gòu)建的,其數(shù)據(jù)流向圖如下:
2、存儲(chǔ)文件組織方式
RocketMQ在消息寫(xiě)入過(guò)程中追求極致的磁盤(pán)順序?qū)憽K兄黝}的消息全部寫(xiě)入一個(gè)文件,即Commitlog文件。所有消息按抵達(dá)順序依次追加到文件中,消息一旦寫(xiě)入,不支持修改。Commitlog文件的具體布局如下圖所示:
基于文件編程與基于內(nèi)存編程有一個(gè)很大的不同是在基于內(nèi)存的編程模式中我們有現(xiàn)成的數(shù)據(jù)結(jié)構(gòu),例如 List、HashMap,對(duì)數(shù)據(jù)的讀寫(xiě)非常方便,那么一條一條消息存入文件Commitlog后,該如何查找呢?
正如關(guān)系型數(shù)據(jù)會(huì)為每一條數(shù)據(jù)引入一個(gè)ID字段,在基于文件編程的模型中,也會(huì)為一條消息引入一個(gè)身份標(biāo)志:消息物理偏移量,即消息存儲(chǔ)在文件的起始位置。
正是有了物理偏移量的概念,Commitlog的文件名命名也是極具技巧性,使用了存儲(chǔ)在該文件的第一條消息在整個(gè)Commitlog文件組中的偏移量來(lái)命名,例如第一個(gè) Commitlog文件為 0000000000000000000,第二個(gè)文件為00000000001073741824,然后依次類(lèi)推。
這樣做的好處是給出任意一個(gè)消息的物理偏移量,例如消息偏移量為 73741824,可以通過(guò)二分法進(jìn)行查找,快速定位這個(gè)文件在第一個(gè)文件中,然后用消息的物理偏移量減去該文件的名稱所得到的差值,就是在該文件中的絕對(duì)地址。
Commitlog文件的設(shè)計(jì)理念是追求極致的消息寫(xiě),但我們知道消息消費(fèi)模型是基于主題的訂閱機(jī)制,即一個(gè)消費(fèi)組是消費(fèi)特定主題的消息。如果根據(jù)主題從commitlog文件中檢索消息,我們會(huì)發(fā)現(xiàn)這絕不是一個(gè)好主意,只能從文件的第一條消息逐條檢索,其性能可想而知,故為了解決基于topic的消息檢索問(wèn)題,RocketMQ引入了consumequeue文件,consumequeue的結(jié)構(gòu)如下圖所示。
ConsumeQueue文件是消息消費(fèi)隊(duì)列文件,是Commitlog文件基于Topic的索引文件,主要用于消費(fèi)者根據(jù)Topic消費(fèi)消息,其組織方式為/topic/queue,同一個(gè)隊(duì)列中存在多個(gè)文件。
Consumequeue的設(shè)計(jì)極具技巧,每個(gè)條目長(zhǎng)度固定(8字節(jié)commitlog物理偏移量、4字節(jié)消息長(zhǎng)度、8字節(jié)tag hashcode)。
這里不是存儲(chǔ)tag的原始字符串,而選擇存儲(chǔ)hashcode,目的就是確保每個(gè)條目的長(zhǎng)度固定,可以使用訪問(wèn)類(lèi)似數(shù)組下標(biāo)的方式快速定位條目,極大地提高了ConsumeQueue文件的讀取性能。
試想一下,消息消費(fèi)者根據(jù)topic、消息消費(fèi)進(jìn)度(consumeuqe邏輯偏移量),即第幾個(gè)Consumeque條目,這樣的消費(fèi)進(jìn)度去訪問(wèn)消息的方法為使用邏輯偏移量logicOffset * 20即可找到該條目的起始偏移量(consumequeue文件中的偏移量),然后讀取該偏移量后20個(gè)字節(jié)即得到一個(gè)條目,無(wú)須遍歷consumequeue文件。
RocketMQ與Kafka相比具有一個(gè)強(qiáng)大的優(yōu)勢(shì),就是支持按消息屬性檢索消息,引入consumequeue文件解決了基于topic查找的問(wèn)題,但如果想基于消息的某一個(gè)屬性查找消息,consumequeue文件就無(wú)能為力了。
RocketMQ引入了Index索引文件,實(shí)現(xiàn)基于文件的哈希索引。IndexFile的文件存儲(chǔ)結(jié)構(gòu)如下圖所示:
IndexFile文件基于物理磁盤(pán)文件實(shí)現(xiàn)Hash索引。其文件由40字節(jié)的文件頭、500萬(wàn)個(gè)哈希槽,每個(gè)哈希槽4個(gè)字節(jié),最后由2000萬(wàn)個(gè)Index條目,每個(gè)條目由20個(gè)字節(jié)構(gòu)成,分別為4字節(jié)索引key的hashcode、8字節(jié)消息物理偏移量、4字節(jié)時(shí)間戳、4字節(jié)的前一個(gè)Index條目(哈希沖突的鏈表結(jié)構(gòu))。
即建立了索引Key的hashcode與物理偏移量的映射關(guān)系,根據(jù)key先快速定義到commitlog文件,關(guān)于Hash索引具體到工作機(jī)制,可以參考筆直《RocketMQ技術(shù)內(nèi)幕》第二版4.5.3節(jié)的詳細(xì)介紹。
3、順序?qū)?/span>
基于磁盤(pán)的讀寫(xiě),提高其寫(xiě)入性能的另外一個(gè)設(shè)計(jì)原理是磁盤(pán)順序?qū)?/strong>。
磁盤(pán)順序?qū)憦V泛用在基于文件的存儲(chǔ)模型中,大家不妨思考一下 MySQL Redo 日志的引入目的,我們知道在 MySQL InnoDB 的存儲(chǔ)引擎中,會(huì)有一個(gè)內(nèi)存 Pool,用來(lái)緩存磁盤(pán)的文件塊,當(dāng)更新語(yǔ)句將數(shù)據(jù)修改后,會(huì)首先在內(nèi)存中進(jìn)行修改,然后將變更寫(xiě)入到 redo 文件(刷寫(xiě)到磁盤(pán)),然后定時(shí)將InnoDB內(nèi)存池中的數(shù)據(jù)刷寫(xiě)到磁盤(pán)。
為什么不一有數(shù)據(jù)變更,就直接更新到指定的數(shù)據(jù)文件中呢?以MySQL InnoDB中一個(gè)庫(kù)存在上千張,每一個(gè)張的數(shù)據(jù)會(huì)使用單獨(dú)的文件存儲(chǔ),如果每一個(gè)表的數(shù)據(jù)發(fā)生變更,就刷寫(xiě)到磁盤(pán),就會(huì)存在大量的隨機(jī)寫(xiě)入,性能無(wú)法得到提升,故引入一個(gè)redo文件,順序?qū)憆edo文件,從表面上多了一步刷盤(pán)操作,但由于是順序?qū)懀啾入S機(jī)寫(xiě),帶來(lái)的性能提升是非常顯著的。
4、內(nèi)存映射機(jī)制
雖然基于磁盤(pán)的順序?qū)懣梢詷O大提高IO的寫(xiě)效率,但如果基于文件的存儲(chǔ)采用常規(guī)的JAVA文件操作API,例如 FileOutputStream等,其性能提升會(huì)很有限,RocketMQ引入了內(nèi)存映射,將磁盤(pán)文件映射到內(nèi)存中,以操作內(nèi)存的方式操作磁盤(pán),性能又提升了一個(gè)檔次。
在JAVA中可通過(guò)FileChannel的map方法創(chuàng)建內(nèi)存映射文件。
在Linux服務(wù)器中由該方法創(chuàng)建的文件使用的就是操作系統(tǒng)的pagecache,即頁(yè)緩存。
Linux操作系統(tǒng)中的內(nèi)存使用策略時(shí)會(huì)盡可能地利用機(jī)器的物理內(nèi)存,并常駐內(nèi)存中,就是所謂的頁(yè)緩存。在操作系統(tǒng)的內(nèi)存不夠的情況下,采用緩存置換算法,例如LRU將不常用的頁(yè)緩存回收,即操作系統(tǒng)會(huì)自動(dòng)管理這部分內(nèi)存。
如果RocketMQ Broker進(jìn)程異常退出,存儲(chǔ)在頁(yè)緩存中的數(shù)據(jù)并不會(huì)丟失,操作系統(tǒng)會(huì)定時(shí)將頁(yè)緩存中的數(shù)據(jù)持久化到磁盤(pán),做到數(shù)據(jù)安全可靠。不過(guò)如果是機(jī)器斷電等異常情況,存儲(chǔ)在頁(yè)緩存中的數(shù)據(jù)就有可能丟失。
5、靈活多變的刷盤(pán)策略
有了順序?qū)懞蛢?nèi)存映射的加持,RocketMQ的寫(xiě)入性能得到了極大的保證,但凡事都有利弊,引入了內(nèi)存映射和頁(yè)緩存機(jī)制,消息會(huì)先寫(xiě)入到頁(yè)緩存,此時(shí)消息并沒(méi)有真正持久化到磁盤(pán)。那么broker收到客戶端的消息發(fā)送后,是存儲(chǔ)到頁(yè)緩存中就直接返回成功,還是要持久化到磁盤(pán)中才返回成功呢?
這是一個(gè)“艱難”的抉擇,是在性能與消息可靠性方面進(jìn)行權(quán)衡。為此,RocketMQ提供了多種策略:同步刷盤(pán)、異步刷盤(pán)。
5.1 同步刷盤(pán)
同步刷盤(pán)在RocketMQ的實(shí)現(xiàn)中成為組提交,并不是每一條消息都必須刷盤(pán)。其設(shè)計(jì)理念如圖所示:
采用同步刷盤(pán),每一個(gè)線程將數(shù)據(jù)追到到內(nèi)存后,并向刷盤(pán)線程提交刷盤(pán)請(qǐng)求,然后會(huì)阻塞;刷盤(pán)線程從任務(wù)隊(duì)列中獲取一個(gè)任務(wù),然后觸發(fā)一次刷盤(pán),但并不只刷與請(qǐng)求相關(guān)的消息,而是會(huì)直接將內(nèi)存中待刷盤(pán)的所有消息一次批量刷盤(pán),然后就可以喚醒一組請(qǐng)求線程,實(shí)現(xiàn)組刷盤(pán)。
5.2 異步刷盤(pán)
同步刷盤(pán)的優(yōu)點(diǎn)是能保證消息不丟失,即向客戶端返回成功就代表這條消息已被持久化到磁盤(pán),即消息非常可靠,但這是以犧牲寫(xiě)入響應(yīng)延遲性能為代價(jià)的,由于RocketMQ的消息是先寫(xiě)入 pagecache,故消息丟失的可能性較小,如果能容忍一定幾率的消息丟失,可以考慮使用異步刷盤(pán)。
異步刷盤(pán)指的是broker將消息存儲(chǔ)到pagecache后就立即返回成功,然后開(kāi)啟一個(gè)異步線程定時(shí)執(zhí)行FileChannel的forece方法,將內(nèi)存中的數(shù)據(jù)定時(shí)刷寫(xiě)到磁盤(pán),默認(rèn)間隔為500ms。
6、內(nèi)存級(jí)讀寫(xiě)分離
RocketMQ為了降低pagecache的使用壓力引入了transientStorePoolEnable機(jī)制,即內(nèi)存級(jí)別的讀寫(xiě)分離機(jī)制。
默認(rèn)情況下RocketMQ將消息寫(xiě)入pagecache,消息消費(fèi)時(shí)從pagecache中讀取,這樣在高并發(fā)時(shí)pagecache的壓力會(huì)比較大,容易出現(xiàn)瞬時(shí)broker busy,故RocketMQ還引入了transientStorePoolEnable,將消息先寫(xiě)入堆外內(nèi)存并立即返回,然后異步將堆外內(nèi)存中的數(shù)據(jù)提交到pagecache,再異步刷盤(pán)到磁盤(pán)中。其工作機(jī)制如下圖所示:
消息在消費(fèi)讀取時(shí)不會(huì)嘗試從堆外內(nèi)存中讀,而是從pagecache中讀取,這樣就形成了內(nèi)存級(jí)別的讀寫(xiě)分離,即消息寫(xiě)入時(shí)主要面對(duì)堆外內(nèi)存,而讀消息時(shí)主要面對(duì)pagecache。
該方案的優(yōu)點(diǎn)是消息是直接寫(xiě)入堆外內(nèi)存,然后異步寫(xiě)入pagecache。相比每條消息追加直接寫(xiě)入pagechae,其最大的優(yōu)勢(shì)是將消息寫(xiě)入pagecache操作批量化。
該方案的缺點(diǎn)是如果由于某些意外操作導(dǎo)致Broker進(jìn)程異常退出,那么存儲(chǔ)在堆外內(nèi)存的數(shù)據(jù)會(huì)丟失,但如果是放入pagecache,broker異常退出并不會(huì)丟失消息。
最后說(shuō)一句(求關(guān)注,別白嫖我)
如果這篇文章對(duì)您有所幫助,或者有所啟發(fā)的話,幫忙掃描下發(fā)二維碼關(guān)注一下,您的支持是我堅(jiān)持寫(xiě)作最大的動(dòng)力。
求一鍵三連:點(diǎn)贊、轉(zhuǎn)發(fā)、在看。
關(guān)注公眾號(hào):【中間件興趣圈】,在公眾號(hào)中回復(fù):「PDF」可獲取大量學(xué)習(xí)資料,回復(fù)「專(zhuān)欄」可獲取15個(gè)主流Java中間件源碼分析專(zhuān)欄,另外回復(fù):加群,可以跟很多BAT大廠的前輩交流和學(xué)習(xí)。

