RocketMQ高性能之底層存儲設計

說在前面

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

例子

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

爲了簡述方便,來個例子緩存

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

發送消息併發

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

RMQ文件全貌oop

RMQ的消息總體是有序的,因此這5條消息按順序將內容持久化在Commit Log中。Consume Queue則用於將消息均衡地排列在不一樣的邏輯隊列,集羣模式下多個消費者就能夠並行消費Consume Queue的消息。性能

Page Cache

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

一般文件讀寫比較慢,若是對文件進行順序讀寫,速度幾乎是接近於內存的隨機讀寫,爲何會這麼快,緣由就是Page Cache。spa

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髒頁配置

上圖爲centos系統的默認配置,dirty_ratio爲阻塞式flush的閾值,而dirty_background_ratio是非阻塞式的flush。

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競爭。

相關文章
相關標籤/搜索