RocketMQ高性能之底層存儲設計

說在前面

RocketMQ在底層存儲上借鑑了Kafka,但是也有它獨到的設計,本文主要關注深刻影響着RocketMQ性能的底層文件存儲結構,中間會穿插一點點Kafka的東西以作爲對比。

例子

Commit Log,一個文件集合,每個文件1G大小,存儲滿後存下一個,爲了討論方便可以把它當成一個文件,所有消息內容全部持久化到這個文件中;Consume Queue:一個Topic可以有多個,每一個文件代表一個邏輯隊列,這裏存放消息在Commit Log的偏移值以及大小和Tag屬性。

爲了簡述方便,來個例子

假如集羣有一個Broker,Topic爲binlog的隊列(Consume Queue)數量爲4,如下圖所示,按順序發送這5條內容各不相同消息。

發送消息

先簡單關注下Commit Log和Consume Queue。

RMQ文件全貌

RMQ的消息整體是有序的,所以這5條消息按順序將內容持久化在Commit Log中。Consume Queue則用於將消息均衡地排列在不同的邏輯隊列,集羣模式下多個消費者就可以並行消費Consume Queue的消息。

Page Cache

瞭解了每個文件都在什麼位置存放什麼內容,那接下來就正式開始討論這種存儲方案爲什麼在性能帶來的提升。

通常文件讀寫比較慢,如果對文件進行順序讀寫,速度幾乎是接近於內存的隨機讀寫,爲什麼會這麼快,原因就是Page Cache。

Free命令

先來個直觀的感受,整個OS有3.7G的物理內存,用掉了2.7G,應當還剩下1G空閒的內存,但OS給出的卻是175M。當然這個數學題肯定不能這麼算。

OS發現系統的物理內存有大量剩餘時,爲了提高IO的性能,就會使用多餘的內存當做文件緩存,也就是圖上的buff / cache,廣義我們說的Page Cache就是這些內存的子集。

OS在讀磁盤時會將當前區域的內容全部讀到Cache中,以便下次讀時能命中Cache,寫磁盤時直接寫到Cache中就寫返回,由OS的pdflush以某些策略將Cache的數據Flush回磁盤。

但是系統上文件非常多,即使是多餘的Page Cache也是非常寶貴的資源,OS不可能將Page Cache隨機分配給任何文件,Linux底層就提供了mmap將一個程序指定的文件映射進虛擬內存(Virtual Memory),對文件的讀寫就變成了對內存的讀寫,能充分利用Page Cache。不過,文件IO僅僅用到了Page Cache還是不夠的,如果對文件進行隨機讀寫,會使虛擬內存產生很多缺頁(Page Fault)中斷。

映射缺頁

每個用戶空間的進程都有自己的虛擬內存,每個進程都認爲自己所有的物理內存,但虛擬內存只是邏輯上的內存,要想訪問內存的數據,還得通過內存管理單元(MMU)查找頁表,將虛擬內存映射成物理內存。如果映射的文件非常大,程序訪問局部映射不到物理內存的虛擬內存時,產生缺頁中斷,OS需要讀寫磁盤文件的真實數據再加載到內存。如同我們的應用程序沒有Cache住某塊數據,直接訪問數據庫要數據再把結果寫到Cache一樣,這個過程相對而言是非常慢的。

但是順序IO時,讀和寫的區域都是被OS智能Cache過的熱點區域,不會產生大量缺頁中斷,文件的IO幾乎等同於內存的IO,性能當然就上去了。

說了這麼多Page Cache的優點,也得稍微提一下它的缺點,內核把可用的內存分配給Page Cache後,free的內存相對就會變少,如果程序有新的內存分配需求或者缺頁中斷,恰好free的內存不夠,內核還需要花費一點時間將熱度低的Page Cache的內存回收掉,對性能非常苛刻的系統會產生毛刺。

刷盤

刷盤一般分成:同步刷盤和異步刷盤

刷盤方式總覽

同步刷盤

在消息真正落盤後,才返回成功給Producer,只要磁盤沒有損壞,消息就不會丟。

同步刷盤——GroupCommit

一般只用於金融場景,這種方式不是本文討論的重點,因爲沒有利用Page Cache的特點,RMQ採用GroupCommit的方式對同步刷盤進行了優化。

異步刷盤

讀寫文件充分利用了Page Cache,即寫入Page Cache就返回成功給Producer,RMQ中有兩種方式進行異步刷盤,整體原理是一樣的。

RMQ異步刷盤方式

刷盤由程序和OS共同控制

先談談OS,當程序順序寫文件時,首先寫到Cache中,這部分被修改過,但卻沒有被刷進磁盤,產生了不一致,這些不一致的內存叫做髒頁(Dirty Page)。

髒頁原理

髒頁設置太小,Flush磁盤的次數就會增加,性能會下降;髒頁設置太大,性能會提高,但萬一OS宕機,髒頁來不及刷盤,消息就丟了。

Linux髒頁配置

一般不是高配玩家,用OS的默認值就好,如上圖。

RMQ消費場景對性能的影響

RMQ想要性能高,那發送消息時,消息要寫進Page Cache而不是直接寫磁盤,接收消息時,消息要從Page Cache直接獲取而不是缺頁從磁盤讀取。

好了,原理回顧完,從消息發送和消息接收來看RMQ中被mmap後的Commit Log和Consume Queue的IO情況。

RMQ發送邏輯

發送時,Producer不直接與Consume Queue打交道。上文提到過,RMQ所有的消息都會存放在Commit Log中,爲了使消息存儲不發生混亂,對Commit Log進行寫之前就會上鎖。

Commit Log順序寫

消息持久被鎖串行化後,對Commit Log就是順序寫,也就是常說的Append操作。配合上Page Cache,RMQ在寫Commit Log時效率會非常高。

Commit Log持久後,會將裏面的數據Dispatch到對應的Consume Queue上。

Consume Queue順序寫

每一個Consume Queue代表一個邏輯隊列,是由ReputMessageService在單個Thread Loop中Append,顯然也是順序寫。

消費邏輯底層

消費時,Consumer不直接與Commit Log打交道,而是從Consume Queue中去拉取數據

Consume Queue順序讀

拉取的順序從舊到新,在文件表示每一個Consume Queue都是順序讀,充分利用了Page Cache。

光拉取Consume Queue是沒有數據的,裏面只有一個對Commit Log的引用,所以再次拉取Commit Log。

Commit Log隨機讀

Commit Log會進行隨機讀

Commit Log整體有序的隨機讀

但整個RMQ只有一個Commit Log,雖然是隨機讀,但整體還是有序地讀,只要那整塊區域還在Page Cache的範圍內,還是可以充分利用Page Cache。

運行中的RMQ磁盤與網絡情況

在一臺真實的MQ上查看網絡和磁盤,即使消息端一直從MQ讀取消息,也幾乎看不到進程從磁盤拉數據,數據直接從Page Cache經由Socket發送給了Consumer。

對比Kafka

文章開頭就說到,RMQ是借鑑了Kafka的想法,同時也打破了Kafka在底層存儲的設計。

Kafka分區模型

Kafka中關於消息的存儲只有一種文件,叫做Partition(不考慮細化的Segment),它履行了RMQ中Commit Log和Consume Queue公共的職責,即它在邏輯上進行拆分存,以提高消費並行度,又在內部存儲了真實的消息內容。

Partition順序讀寫

這樣看上去非常完美,不管對於Producer還是Consumer,單個Partition文件在正常的發送和消費邏輯中都是順序IO,充分利用Page Cache帶來的巨大性能提升,但是,萬一Topic很多,每個Topic又分了N個Partition,這時對於OS來說,這麼多文件的順序讀寫在併發時變成了隨機讀寫。

Kafka Partition隨機讀寫情況

這時,不知道爲什麼,我突然想起了「打地鼠」這款遊戲。對於每一個洞,我打的地鼠總是有順序的,但是,萬一有10000個洞,只有你一個人去打,無數只地鼠有先有後的出入於每個洞,這時還不是隨機去打,同學們腦補下這場景。

當然,思路很好的同學馬上發現RMQ在隊列非常多的情況下Consume Queue不也是和Kafka類似,雖然每一個文件是順序IO,但整體是隨機IO。不要忘記了,RMQ的Consume Queue是不會存儲消息的內容,任何一個消息也就佔用20 Byte,所以文件可以控制得非常小,絕大部分的訪問還是Page Cache的訪問,而不是磁盤訪問。正式部署也可以將Commit Log和Consume Queue放在不同的物理SSD,避免多類文件進行IO競爭。

說在後面

更多精彩的文章,請關注我的微信公衆號: 艾瑞克的技術江湖
RocketMQ高性能之底層存儲設計

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