您好,登錄后才能下訂單哦!
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的消息整體是有序的,所以這5條消息按順序將內容持久化在Commit Log中。Consume Queue則用于將消息均衡地排列在不同的邏輯隊列,集群模式下多個消費者就可以并行消費Consume Queue的消息。
了解了每個文件都在什么位置存放什么內容,那接下來就正式開始討論這種存儲方案為什么在性能帶來的提升。
通常文件讀寫比較慢,如果對文件進行順序讀寫,速度幾乎是接近于內存的隨機讀寫,為什么會這么快,原因就是Page Cache。
先來個直觀的感受,整個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,只要磁盤沒有損壞,消息就不會丟。
一般只用于金融場景,這種方式不是本文討論的重點,因為沒有利用Page Cache的特點,RMQ采用GroupCommit的方式對同步刷盤進行了優化。
異步刷盤
讀寫文件充分利用了Page Cache,即寫入Page Cache就返回成功給Producer,RMQ中有兩種方式進行異步刷盤,整體原理是一樣的。
刷盤由程序和OS共同控制
先談談OS,當程序順序寫文件時,首先寫到Cache中,這部分被修改過,但卻沒有被刷進磁盤,產生了不一致,這些不一致的內存叫做臟頁(Dirty Page)。
臟頁設置太小,Flush磁盤的次數就會增加,性能會下降;臟頁設置太大,性能會提高,但萬一OS宕機,臟頁來不及刷盤,消息就丟了。
一般不是高配玩家,用OS的默認值就好,如上圖。
RMQ想要性能高,那發送消息時,消息要寫進Page Cache而不是直接寫磁盤,接收消息時,消息要從Page Cache直接獲取而不是缺頁從磁盤讀取。
好了,原理回顧完,從消息發送和消息接收來看RMQ中被mmap后的Commit Log和Consume Queue的IO情況。
發送時,Producer不直接與Consume Queue打交道。上文提到過,RMQ所有的消息都會存放在Commit Log中,為了使消息存儲不發生混亂,對Commit Log進行寫之前就會上鎖。
消息持久被鎖串行化后,對Commit Log就是順序寫,也就是常說的Append操作。配合上Page Cache,RMQ在寫Commit Log時效率會非常高。
Commit Log持久后,會將里面的數據Dispatch到對應的Consume Queue上。
每一個Consume Queue代表一個邏輯隊列,是由ReputMessageService在單個Thread Loop中Append,顯然也是順序寫。
消費時,Consumer不直接與Commit Log打交道,而是從Consume Queue中去拉取數據
拉取的順序從舊到新,在文件表示每一個Consume Queue都是順序讀,充分利用了Page Cache。
光拉取Consume Queue是沒有數據的,里面只有一個對Commit Log的引用,所以再次拉取Commit Log。
Commit Log會進行隨機讀
但整個RMQ只有一個Commit Log,雖然是隨機讀,但整體還是有序地讀,只要那整塊區域還在Page Cache的范圍內,還是可以充分利用Page Cache。
在一臺真實的MQ上查看網絡和磁盤,即使消息端一直從MQ讀取消息,也幾乎看不到進程從磁盤拉數據,數據直接從Page Cache經由Socket發送給了Consumer。
文章開頭就說到,RMQ是借鑒了Kafka的想法,同時也打破了Kafka在底層存儲的設計。
Kafka中關于消息的存儲只有一種文件,叫做Partition(不考慮細化的Segment),它履行了RMQ中Commit Log和Consume Queue公共的職責,即它在邏輯上進行拆分存,以提高消費并行度,又在內部存儲了真實的消息內容。
這樣看上去非常完美,不管對于Producer還是Consumer,單個Partition文件在正常的發送和消費邏輯中都是順序IO,充分利用Page Cache帶來的巨大性能提升,但是,萬一Topic很多,每個Topic又分了N個Partition,這時對于OS來說,這么多文件的順序讀寫在并發時變成了隨機讀寫。
這時,不知道為什么,我突然想起了「打地鼠」這款游戲。對于每一個洞,我打的地鼠總是有順序的,但是,萬一有10000個洞,只有你一個人去打,無數只地鼠有先有后的出入于每個洞,這時還不是隨機去打,同學們腦補下這場景。
當然,思路很好的同學馬上發現RMQ在隊列非常多的情況下Consume Queue不也是和Kafka類似,雖然每一個文件是順序IO,但整體是隨機IO。不要忘記了,RMQ的Consume Queue是不會存儲消息的內容,任何一個消息也就占用20 Byte,所以文件可以控制得非常小,絕大部分的訪問還是Page Cache的訪問,而不是磁盤訪問。正式部署也可以將Commit Log和Consume Queue放在不同的物理SSD,避免多類文件進行IO競爭。
更多精彩的文章,請關注我的微信公眾號: 艾瑞克的技術江湖
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。