消息中間件—RocketMQ消息存儲(一)一、MQ消息隊列的一般存儲方式二、RocketMQ消息存儲整體架構三、RocketMQ文件存儲模型層次結構四、總結

文章摘要:MQ分佈式消息隊列大致流程在於消息的一發一收一存,本篇將爲大家主要介紹下RocketMQ存儲部分的架構 消息存儲是MQ消息隊列中最爲複雜和最爲重要的一部分,所以小編也就放在RocketMQ系列篇幅中最後一部分來進行闡述和介紹。本文先從目前幾種比較常用的MQ消息隊列存儲方式出發,爲大家介紹RocketMQ選擇磁盤文件存儲的原因。然後,本文分別從RocketMQ的消息存儲整體架構和RocketMQ文件存儲模型層次結構兩方面進行深入分析介紹。使得大家讀完本文後對RocketMQ消息存儲部分有一個大致的瞭解和認識。 這裏先回顧往期RocketMQ技術分享的篇幅(如果有童鞋沒有讀過之前的文章,建議先好好讀下之前小編寫的篇幅或者其他網上相關的博客,把RocketMQ消息發送和消費部分的流程先大致搞明白): (1)消息中間件—RocketMQ的RPC通信(一) (2)消息中間件—RocketMQ的RPC通信(二) (3)消息中間件—RocketMQ消息發送 (4)消息中間件—RocketMQ消息消費(一) (5)消息中間件—RocketMQ消息消費(二)(push模式實現) (6)消息中間件—RocketMQ消息消費(三)(消息消費重試)

一、MQ消息隊列的一般存儲方式

當前業界幾款主流的MQ消息隊列採用的存儲方式主要有以下三種方式: (1)分佈式KV存儲:這類MQ一般會採用諸如levelDB、RocksDB和Redis來作爲消息持久化的方式,由於分佈式緩存的讀寫能力要優於DB,所以在對消息的讀寫能力要求都不是比較高的情況下,採用這種方式倒也不失爲一種可以替代的設計方案。消息存儲於分佈式KV需要解決的問題在於如何保證MQ整體的可靠性? (2)文件系統:目前業界較爲常用的幾款產品(RocketMQ/Kafka/RabbitMQ)均採用的是消息刷盤至所部署虛擬機/物理機的文件系統來做持久化(刷盤一般可以分爲異步刷盤和同步刷盤兩種模式)。小編認爲,消息刷盤爲消息存儲提供了一種高效率、高可靠性和高性能的數據持久化方式。除非部署MQ機器本身或是本地磁盤掛了,否則一般是不會出現無法持久化的故障問題。 (3)關係型數據庫DB:Apache下開源的另外一款MQ—ActiveMQ(默認採用的KahaDB做消息存儲)可選用JDBC的方式來做消息持久化,通過簡單的xml配置信息即可實現JDBC消息存儲。由於,普通關係型數據庫(如Mysql)在單表數據量達到千萬級別的情況下,其IO讀寫性能往往會出現瓶頸。因此,如果要選型或者自研一款性能強勁、吞吐量大、消息堆積能力突出的MQ消息隊列,那麼小編並不推薦採用關係型數據庫作爲消息持久化的方案。在可靠性方面,該種方案非常依賴DB,如果一旦DB出現故障,則MQ的消息就無法落盤存儲會導致線上故障; 因此,綜合上所述從存儲效率來說, 文件系統>分佈式KV存儲>關係型數據庫DB,直接操作文件系統肯定是最快和最高效的,而關係型數據庫TPS一般相比於分佈式KV系統會更低一些(簡略地說,關係型數據庫本身也是一個需要讀寫文件server,這時MQ作爲client與其建立連接併發送待持久化的消息數據,同時又需要依賴DB的事務等,這一系列操作都比較消耗性能),所以如果追求高效的IO讀寫,那麼選擇操作文件系統會更加合適一些。但是如果從易於實現和快速集成來看,關係型數據庫DB>分佈式KV存儲>文件系統,但是性能會下降很多。 另外,從消息中間件的本身定義來考慮,應該儘量減少對於外部第三方中間件的依賴。一般來說依賴的外部系統越多,也會使得本身的設計越複雜,所以小編個人的理解是採用文件系統作爲消息存儲的方式,更貼近消息中間件本身的定義。

二、RocketMQ消息存儲整體架構

RokcetMQ存儲設計架構.jpg

(1)RocketMQ消息存儲結構類型及缺點

上圖即爲RocketMQ的消息存儲整體架構,RocketMQ採用的是混合型的存儲結構,即爲Broker單個實例下所有的隊列共用一個日誌數據文件(即爲CommitLog)來存儲。而Kafka採用的是獨立型的存儲結構,每個隊列一個文件。這裏小編認爲,RocketMQ採用混合型存儲結構的缺點在於,會存在較多的隨機讀操作,因此讀的效率偏低。同時消費消息需要依賴ConsumeQueue,構建該邏輯消費隊列需要一定開銷。

(2)RocketMQ消息存儲架構深入分析

從上面的整體架構圖中可見,RocketMQ的混合型存儲結構針對Producer和Consumer分別採用了數據和索引部分相分離的存儲結構,Producer發送消息至Broker端,然後Broker端使用同步或者異步的方式對消息刷盤持久化,保存至CommitLog中。只要消息被刷盤持久化至磁盤文件CommitLog中,那麼Producer發送的消息就不會丟失。正因爲如此,Consumer也就肯定有機會去消費這條消息,至於消費的時間可以稍微滯後一些也沒有太大的關係。退一步地講,即使Consumer端第一次沒法拉取到待消費的消息,Broker服務端也能夠通過長輪詢機制等待一定時間延遲後再次發起拉取消息的請求。 這裏,RocketMQ的具體做法是,使用Broker端的後臺服務線程—ReputMessageService不停地分發請求並異步構建ConsumeQueue(邏輯消費隊列)和IndexFile(索引文件)數據(ps:對於該服務線程在消息消費篇幅也有過介紹,不清楚的童鞋可以跳至消息消費篇幅再理解下)。然後,Consumer即可根據ConsumerQueue來查找待消費的消息了。其中,ConsumeQueue(邏輯消費隊列)作爲消費消息的索引,保存了指定Topic下的隊列消息在CommitLog中的起始物理偏移量offset,消息大小size和消息Tag的HashCode值。而IndexFile(索引文件)則只是爲了消息查詢提供了一種通過key或時間區間來查詢消息的方法(ps:這種通過IndexFile來查找消息的方法不影響發送與消費消息的主流程)。

(3)PageCache與Mmap內存映射

這裏有必要先稍微簡單地介紹下page cache的概念。系統的所有文件I/O請求,操作系統都是通過page cache機制實現的。對於操作系統來說,磁盤文件都是由一系列的數據塊順序組成,數據塊的大小由操作系統本身而決定,x86的linux中一個標準頁面大小是4KB。 操作系統內核在處理文件I/O請求時,首先到page cache中查找(page cache中的每一個數據塊都設置了文件以及偏移量地址信息),如果未命中,則啓動磁盤I/O,將磁盤文件中的數據塊加載到page cache中的一個空閒塊,然後再copy到用戶緩衝區中。 page cache本身也會對數據文件進行預讀取,對於每個文件的第一個讀請求操作,系統在讀入所請求頁面的同時會讀入緊隨其後的少數幾個頁面。因此,想要提高page cache的命中率(儘量讓訪問的頁在物理內存中),從硬件的角度來說肯定是物理內存越大越好。從操作系統層面來說,訪問page cache時,即使只訪問1k的消息,系統也會提前預讀取更多的數據,在下次讀取消息時, 就很可能可以命中內存。 在RocketMQ中,ConsumeQueue邏輯消費隊列存儲的數據較少,並且是順序讀取,在page cache機制的預讀取作用下,Consume Queue的讀性能會比較高近乎內存,即使在有消息堆積情況下也不會影響性能。而對於CommitLog消息存儲的日誌數據文件來說,讀取消息內容時候會產生較多的隨機訪問讀取,嚴重影響性能。如果選擇合適的系統IO調度算法,比如設置調度算法爲“Noop”(此時塊存儲採用SSD的話),隨機讀的性能也會有所提升。 另外,RocketMQ主要通過MappedByteBuffer對文件進行讀寫操作。其中,利用了NIO中的FileChannel模型直接將磁盤上的物理文件直接映射到用戶態的內存地址中(這種Mmap的方式減少了傳統IO將磁盤文件數據在操作系統內核地址空間的緩衝區和用戶應用程序地址空間的緩衝區之間來回進行拷貝的性能開銷),將對文件的操作轉化爲直接對內存地址進行操作,從而極大地提高了文件的讀寫效率(這裏需要注意的是,採用MappedByteBuffer這種內存映射的方式有幾個限制,其中之一是一次只能映射1.5~2G 的文件至用戶態的虛擬內存,這也是爲何RocketMQ默認設置單個CommitLog日誌數據文件爲1G的原因了)。

三、RocketMQ文件存儲模型層次結構

RocketMQ文件存儲模型結構.jpg

RocketMQ文件存儲模型層次結構如上圖所示,根據類別和作用從概念模型上大致可以劃分爲5層,下面將從各個層次分別進行分析和闡述: (1)RocketMQ業務處理器層:Broker端對消息進行讀取和寫入的業務邏輯入口,這一層主要包含了業務邏輯相關處理操作(根據解析RemotingCommand中的RequestCode來區分具體的業務操作類型,進而執行不同的業務處理流程),比如前置的檢查和校驗步驟、構造MessageExtBrokerInner對象、decode反序列化、構造Response返回對象等; (2)RocketMQ數據存儲組件層;該層主要是RocketMQ的存儲核心類—DefaultMessageStore,其爲RocketMQ消息數據文件的訪問入口,通過該類的“putMessage()”和“getMessage()”方法完成對CommitLog消息存儲的日誌數據文件進行讀寫操作(具體的讀寫訪問操作還是依賴下一層中CommitLog對象模型提供的方法);另外,在該組件初始化時候,還會啓動很多存儲相關的後臺服務線程,包括AllocateMappedFileService(MappedFile預分配服務線程)、ReputMessageService(回放存儲消息服務線程)、HAService(Broker主從同步高可用服務線程)、StoreStatsService(消息存儲統計服務線程)、IndexService(索引文件服務線程)等; (3)RocketMQ存儲邏輯對象層:該層主要包含了RocketMQ數據文件存儲直接相關的三個模型類IndexFile、ConsumerQueue和CommitLog。IndexFile爲索引數據文件提供訪問服務,ConsumerQueue爲邏輯消息隊列提供訪問服務,CommitLog則爲消息存儲的日誌數據文件提供訪問服務。這三個模型類也是構成了RocketMQ存儲層的整體結構(對於這三個模型類的深入分析將放在後續篇幅中); (4)封裝的文件內存映射層:RocketMQ主要採用JDK NIO中的MappedByteBuffer和FileChannel兩種方式完成數據文件的讀寫。其中,採用MappedByteBuffer這種內存映射磁盤文件的方式完成對大文件的讀寫,在RocketMQ中將該類封裝成MappedFile類。這裏限制的問題在上面已經講過;對於每類大文件(IndexFile/ConsumerQueue/CommitLog),在存儲時分隔成多個固定大小的文件(單個IndexFile文件大小約爲400M、單個ConsumerQueue文件大小約5.72M、單個CommitLog文件大小爲1G),其中每個分隔文件的文件名爲前面所有文件的字節大小數+1,即爲文件的起始偏移量,從而實現了整個大文件的串聯。這裏,每一種類的單個文件均由MappedFile類提供讀寫操作服務(其中,MappedFile類提供了順序寫/隨機讀、內存數據刷盤、內存清理等和文件相關的服務); (5)磁盤存儲層:主要指的是部署RocketMQ服務器所用的磁盤。這裏,需要考慮不同磁盤類型(如SSD或者普通的HDD)特性以及磁盤的性能參數(如IOPS、吞吐量和訪問時延等指標)對順序寫/隨機讀操作帶來的影響(ps:小編建議在正式業務上線之前做好多輪的性能壓測,具體用壓測的結果來評測);

四、總結

RocketMQ的RocketMQ消息存儲(一)篇幅就先分析到這兒了。RocketMQ消息存儲部分的內容與其他所有篇幅(RocketMQ的Remoting通信、普通消息發送和消息消費部分)相比是最爲複雜的,需要讀者反覆多看源碼並多次對消息讀和寫進行Debug(可以通過在Broker端的SendMessageProcessor/PullMessageProcesssor/QueryMessaageProcessor幾個業務處理器入口,在其重要方法中打印相關重要屬性值的方式或者一步步地Debug代碼,來仔細研究下其中的存儲過程),反覆幾次後纔可以對消息存儲這部分有一個較爲深刻的理解,同時也有助於提高對RocketMQ的整體理解。限於筆者的才疏學淺,對本文內容可能還有理解不到位的地方,如有闡述不合理之處還望留言一起探討。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章