您好,登錄后才能下訂單哦!
RocketMQ在底層存儲(chǔ)上借鑒了Kafka,但是也有它獨(dú)到的設(shè)計(jì),本文主要關(guān)注深刻影響著RocketMQ性能的底層文件存儲(chǔ)結(jié)構(gòu),中間會(huì)穿插一點(diǎn)點(diǎn)Kafka的東西以作為對(duì)比。
Commit Log,一個(gè)文件集合,每個(gè)文件1G大小,存儲(chǔ)滿后存下一個(gè),為了討論方便可以把它當(dāng)成一個(gè)文件,所有消息內(nèi)容全部持久化到這個(gè)文件中;Consume Queue:一個(gè)Topic可以有多個(gè),每一個(gè)文件代表一個(gè)邏輯隊(duì)列,這里存放消息在Commit Log的偏移值以及大小和Tag屬性。
為了簡(jiǎn)述方便,來個(gè)例子
假如集群有一個(gè)Broker,Topic為binlog的隊(duì)列(Consume Queue)數(shù)量為4,如下圖所示,按順序發(fā)送這5條內(nèi)容各不相同消息。
先簡(jiǎn)單關(guān)注下Commit Log和Consume Queue。
RMQ的消息整體是有序的,所以這5條消息按順序?qū)?nèi)容持久化在Commit Log中。Consume Queue則用于將消息均衡地排列在不同的邏輯隊(duì)列,集群模式下多個(gè)消費(fèi)者就可以并行消費(fèi)Consume Queue的消息。
了解了每個(gè)文件都在什么位置存放什么內(nèi)容,那接下來就正式開始討論這種存儲(chǔ)方案為什么在性能帶來的提升。
通常文件讀寫比較慢,如果對(duì)文件進(jìn)行順序讀寫,速度幾乎是接近于內(nèi)存的隨機(jī)讀寫,為什么會(huì)這么快,原因就是Page Cache。
先來個(gè)直觀的感受,整個(gè)OS有3.7G的物理內(nèi)存,用掉了2.7G,應(yīng)當(dāng)還剩下1G空閑的內(nèi)存,但OS給出的卻是175M。當(dāng)然這個(gè)數(shù)學(xué)題肯定不能這么算。
OS發(fā)現(xiàn)系統(tǒng)的物理內(nèi)存有大量剩余時(shí),為了提高IO的性能,就會(huì)使用多余的內(nèi)存當(dāng)做文件緩存,也就是圖上的buff / cache,廣義我們說的Page Cache就是這些內(nèi)存的子集。
OS在讀磁盤時(shí)會(huì)將當(dāng)前區(qū)域的內(nèi)容全部讀到Cache中,以便下次讀時(shí)能命中Cache,寫磁盤時(shí)直接寫到Cache中就寫返回,由OS的pdflush以某些策略將Cache的數(shù)據(jù)Flush回磁盤。
但是系統(tǒng)上文件非常多,即使是多余的Page Cache也是非常寶貴的資源,OS不可能將Page Cache隨機(jī)分配給任何文件,Linux底層就提供了mmap將一個(gè)程序指定的文件映射進(jìn)虛擬內(nèi)存(Virtual Memory),對(duì)文件的讀寫就變成了對(duì)內(nèi)存的讀寫,能充分利用Page Cache。不過,文件IO僅僅用到了Page Cache還是不夠的,如果對(duì)文件進(jìn)行隨機(jī)讀寫,會(huì)使虛擬內(nèi)存產(chǎn)生很多缺頁(yè)(Page Fault)中斷。
每個(gè)用戶空間的進(jìn)程都有自己的虛擬內(nèi)存,每個(gè)進(jìn)程都認(rèn)為自己所有的物理內(nèi)存,但虛擬內(nèi)存只是邏輯上的內(nèi)存,要想訪問內(nèi)存的數(shù)據(jù),還得通過內(nèi)存管理單元(MMU)查找頁(yè)表,將虛擬內(nèi)存映射成物理內(nèi)存。如果映射的文件非常大,程序訪問局部映射不到物理內(nèi)存的虛擬內(nèi)存時(shí),產(chǎn)生缺頁(yè)中斷,OS需要讀寫磁盤文件的真實(shí)數(shù)據(jù)再加載到內(nèi)存。如同我們的應(yīng)用程序沒有Cache住某塊數(shù)據(jù),直接訪問數(shù)據(jù)庫(kù)要數(shù)據(jù)再把結(jié)果寫到Cache一樣,這個(gè)過程相對(duì)而言是非常慢的。
但是順序IO時(shí),讀和寫的區(qū)域都是被OS智能Cache過的熱點(diǎn)區(qū)域,不會(huì)產(chǎn)生大量缺頁(yè)中斷,文件的IO幾乎等同于內(nèi)存的IO,性能當(dāng)然就上去了。
說了這么多Page Cache的優(yōu)點(diǎn),也得稍微提一下它的缺點(diǎn),內(nèi)核把可用的內(nèi)存分配給Page Cache后,free的內(nèi)存相對(duì)就會(huì)變少,如果程序有新的內(nèi)存分配需求或者缺頁(yè)中斷,恰好free的內(nèi)存不夠,內(nèi)核還需要花費(fèi)一點(diǎn)時(shí)間將熱度低的Page Cache的內(nèi)存回收掉,對(duì)性能非??量痰南到y(tǒng)會(huì)產(chǎn)生毛刺。
刷盤一般分成:同步刷盤和異步刷盤
同步刷盤
在消息真正落盤后,才返回成功給Producer,只要磁盤沒有損壞,消息就不會(huì)丟。
一般只用于金融場(chǎng)景,這種方式不是本文討論的重點(diǎn),因?yàn)闆]有利用Page Cache的特點(diǎn),RMQ采用GroupCommit的方式對(duì)同步刷盤進(jìn)行了優(yōu)化。
異步刷盤
讀寫文件充分利用了Page Cache,即寫入Page Cache就返回成功給Producer,RMQ中有兩種方式進(jìn)行異步刷盤,整體原理是一樣的。
刷盤由程序和OS共同控制
先談?wù)凮S,當(dāng)程序順序?qū)懳募r(shí),首先寫到Cache中,這部分被修改過,但卻沒有被刷進(jìn)磁盤,產(chǎn)生了不一致,這些不一致的內(nèi)存叫做臟頁(yè)(Dirty Page)。
臟頁(yè)設(shè)置太小,F(xiàn)lush磁盤的次數(shù)就會(huì)增加,性能會(huì)下降;臟頁(yè)設(shè)置太大,性能會(huì)提高,但萬一OS宕機(jī),臟頁(yè)來不及刷盤,消息就丟了。
一般不是高配玩家,用OS的默認(rèn)值就好,如上圖。
RMQ想要性能高,那發(fā)送消息時(shí),消息要寫進(jìn)Page Cache而不是直接寫磁盤,接收消息時(shí),消息要從Page Cache直接獲取而不是缺頁(yè)從磁盤讀取。
好了,原理回顧完,從消息發(fā)送和消息接收來看RMQ中被mmap后的Commit Log和Consume Queue的IO情況。
發(fā)送時(shí),Producer不直接與Consume Queue打交道。上文提到過,RMQ所有的消息都會(huì)存放在Commit Log中,為了使消息存儲(chǔ)不發(fā)生混亂,對(duì)Commit Log進(jìn)行寫之前就會(huì)上鎖。
消息持久被鎖串行化后,對(duì)Commit Log就是順序?qū)?,也就是常說的Append操作。配合上Page Cache,RMQ在寫Commit Log時(shí)效率會(huì)非常高。
Commit Log持久后,會(huì)將里面的數(shù)據(jù)Dispatch到對(duì)應(yīng)的Consume Queue上。
每一個(gè)Consume Queue代表一個(gè)邏輯隊(duì)列,是由ReputMessageService在單個(gè)Thread Loop中Append,顯然也是順序?qū)憽?/p>
消費(fèi)時(shí),Consumer不直接與Commit Log打交道,而是從Consume Queue中去拉取數(shù)據(jù)
拉取的順序從舊到新,在文件表示每一個(gè)Consume Queue都是順序讀,充分利用了Page Cache。
光拉取Consume Queue是沒有數(shù)據(jù)的,里面只有一個(gè)對(duì)Commit Log的引用,所以再次拉取Commit Log。
Commit Log會(huì)進(jìn)行隨機(jī)讀
但整個(gè)RMQ只有一個(gè)Commit Log,雖然是隨機(jī)讀,但整體還是有序地讀,只要那整塊區(qū)域還在Page Cache的范圍內(nèi),還是可以充分利用Page Cache。
在一臺(tái)真實(shí)的MQ上查看網(wǎng)絡(luò)和磁盤,即使消息端一直從MQ讀取消息,也幾乎看不到進(jìn)程從磁盤拉數(shù)據(jù),數(shù)據(jù)直接從Page Cache經(jīng)由Socket發(fā)送給了Consumer。
文章開頭就說到,RMQ是借鑒了Kafka的想法,同時(shí)也打破了Kafka在底層存儲(chǔ)的設(shè)計(jì)。
Kafka中關(guān)于消息的存儲(chǔ)只有一種文件,叫做Partition(不考慮細(xì)化的Segment),它履行了RMQ中Commit Log和Consume Queue公共的職責(zé),即它在邏輯上進(jìn)行拆分存,以提高消費(fèi)并行度,又在內(nèi)部存儲(chǔ)了真實(shí)的消息內(nèi)容。
這樣看上去非常完美,不管對(duì)于Producer還是Consumer,單個(gè)Partition文件在正常的發(fā)送和消費(fèi)邏輯中都是順序IO,充分利用Page Cache帶來的巨大性能提升,但是,萬一Topic很多,每個(gè)Topic又分了N個(gè)Partition,這時(shí)對(duì)于OS來說,這么多文件的順序讀寫在并發(fā)時(shí)變成了隨機(jī)讀寫。
這時(shí),不知道為什么,我突然想起了「打地鼠」這款游戲。對(duì)于每一個(gè)洞,我打的地鼠總是有順序的,但是,萬一有10000個(gè)洞,只有你一個(gè)人去打,無數(shù)只地鼠有先有后的出入于每個(gè)洞,這時(shí)還不是隨機(jī)去打,同學(xué)們腦補(bǔ)下這場(chǎng)景。
當(dāng)然,思路很好的同學(xué)馬上發(fā)現(xiàn)RMQ在隊(duì)列非常多的情況下Consume Queue不也是和Kafka類似,雖然每一個(gè)文件是順序IO,但整體是隨機(jī)IO。不要忘記了,RMQ的Consume Queue是不會(huì)存儲(chǔ)消息的內(nèi)容,任何一個(gè)消息也就占用20 Byte,所以文件可以控制得非常小,絕大部分的訪問還是Page Cache的訪問,而不是磁盤訪問。正式部署也可以將Commit Log和Consume Queue放在不同的物理SSD,避免多類文件進(jìn)行IO競(jìng)爭(zhēng)。
更多精彩的文章,請(qǐng)關(guān)注我的微信公眾號(hào): 艾瑞克的技術(shù)江湖
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。