溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

如何從RocketMQ消息持久化設(shè)計(jì)看磁盤(pán)性能瓶頸的突破

發(fā)布時(shí)間:2021-10-12 10:53:57 來(lái)源:億速云 閱讀:135 作者:柒染 欄目:云計(jì)算

今天就跟大家聊聊有關(guān)如何從RocketMQ消息持久化設(shè)計(jì)看磁盤(pán)性能瓶頸的突破,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。

從RocketMQ消息持久化設(shè)計(jì)看磁盤(pán)性能瓶頸的突破

分布式消息隊(duì)列通常有高可靠性的要求,所以消息數(shù)據(jù)是需要持久化存儲(chǔ)的。那么以什么方式來(lái)進(jìn)行持久化是一個(gè)值得商榷的問(wèn)題。

從存儲(chǔ)方式和效率來(lái)看,文件系統(tǒng) > KV存儲(chǔ) > 關(guān)系型數(shù)據(jù)庫(kù),直接操作文件系統(tǒng)自然是最快的一種存儲(chǔ)方式,但是僅僅如此就可以了嗎?

當(dāng)然不是,在無(wú)數(shù)的過(guò)往學(xué)習(xí)中,磁盤(pán)IO性能拖累系統(tǒng)性能是眾所周知的。那么RocketMQ是怎么解決呢?

各位看官且待我慢慢道來(lái)。

存儲(chǔ)架構(gòu)設(shè)計(jì)

首先我們回憶一下,假如現(xiàn)在有一個(gè)字你不認(rèn)識(shí),然后你手上正巧有一本漢語(yǔ)言辭典,請(qǐng)問(wèn)該怎么做才能以最快的速度查到這個(gè)字?

凡是上過(guò)小學(xué)的人,應(yīng)該都不會(huì)從漢語(yǔ)言辭典第一頁(yè)開(kāi)始一頁(yè)一頁(yè)的查找。

作為優(yōu)秀小學(xué)畢業(yè)生的我們,肯定是先通過(guò)偏旁檢索到這個(gè)字,然后根據(jù)檢索上這個(gè)字的頁(yè)碼,到漢語(yǔ)言辭典里對(duì)應(yīng)的頁(yè)碼中去找到這個(gè)字,于是你就知道它讀什么了。

大道相通。

RocketMQ在文件系統(tǒng)中,把所有的消息都存在了同一個(gè)文件中,這就像一本厚厚的漢語(yǔ)言辭典,作為消費(fèi)者,想要做到最大效率的實(shí)時(shí)消費(fèi),說(shuō)白了就是要快速定位到這個(gè)消息在文件中的位置,肯定不能從文件偏移量0開(kāi)始向下查找。

一張圖頂幾百字:

如何從RocketMQ消息持久化設(shè)計(jì)看磁盤(pán)性能瓶頸的突破

RocketMQ主要存儲(chǔ)文件有三個(gè),分別是:

CommitLog:消息存儲(chǔ)文件,所有的消息存在這里;

ConsumeQueue:消費(fèi)隊(duì)列文件,消息在存儲(chǔ)到CommitLog后,會(huì)將消息所在CommitLog偏移量、大小、tag的hashcode異步轉(zhuǎn)發(fā)到消費(fèi)隊(duì)列存儲(chǔ),供消費(fèi)者消費(fèi),其類(lèi)似于數(shù)據(jù)庫(kù)的索引文件,存儲(chǔ)的是指向物理存儲(chǔ)的地址,每個(gè)topic下的每個(gè)Message Queue都有一個(gè)對(duì)應(yīng)的ConsumeQueue文件;

Index:索引文件,消息在存儲(chǔ)到CommitLog后,會(huì)將消息key與消息所在CommitLog偏移量轉(zhuǎn)發(fā)到索引文件存儲(chǔ),供消息查詢(xún)。

從原理圖中,我們可以看出消息的生產(chǎn)與消費(fèi)進(jìn)行了分離,Producer端發(fā)送消息最終寫(xiě)入的是CommitLog,Consumer端先從ConsumeQueue讀取持久化消息的起始物理位置偏移量offset、大小size和消息Tag的HashCode值,再?gòu)腃ommitLog中進(jìn)行讀取待拉取消費(fèi)消息的真正實(shí)體內(nèi)容部分。

上面說(shuō)了消費(fèi)者如何快速定位到消息位置,使消費(fèi)者可以高效的消費(fèi),那么下面我們說(shuō)說(shuō)RocketMQ中如何做到消息存儲(chǔ)的高效性。

我們先思考一個(gè)問(wèn)題,假如你是印刷廠的老板,你如何才能快速印刷出一本完整沒(méi)有錯(cuò)誤的漢語(yǔ)言辭典呢?

答案很簡(jiǎn)單,從第一頁(yè)開(kāi)始,按照順序一頁(yè)一頁(yè)的印刷,不要跳頁(yè)印刷,更不要隨機(jī)印刷。

正如我們的磁盤(pán)寫(xiě)入一樣,據(jù)某某調(diào)查研究表明,高性能磁盤(pán)在順序?qū)懭氲臅r(shí)候,速度基本可以堪比內(nèi)存的寫(xiě)入速度,但是磁盤(pán)隨機(jī)寫(xiě)入的時(shí)候,性能瓶頸非常明顯,速度會(huì)比較慢。

所以RocketMQ采用了全部消息都存入一個(gè)CommitLog文件中,并且對(duì)寫(xiě)操作加鎖(putMessageLock),保證串行順序?qū)懭胂ⅲ苊獯疟P(pán)竟?fàn)帉?dǎo)致IO WAIT增高,大大提高寫(xiě)入效率。

我們可以用一個(gè)更詳細(xì)的圖來(lái)說(shuō)明:

如何從RocketMQ消息持久化設(shè)計(jì)看磁盤(pán)性能瓶頸的突破

生產(chǎn)者按順序?qū)懭隒ommitLog,消費(fèi)者通過(guò)順序讀取ConsumeQueue進(jìn)行消費(fèi),這里有一個(gè)地方需要注意,雖然消費(fèi)者是按照順序讀取ConsumeQueue,但是并不代表它就是按照順序讀取消息,因?yàn)楦鶕?jù)ConsumeQueue中的起始物理位置偏移量offset讀取消息真實(shí)內(nèi)容,在并發(fā)量非常高的情況下,實(shí)際上是隨機(jī)讀取CommitLog,而隨機(jī)讀取文件帶來(lái)的性能開(kāi)銷(xiāo)影響還是比較大的,所以在這里,RocketMQ利用了操作系統(tǒng)的pagecache機(jī)制,批量從磁盤(pán)讀取,作為cache存在內(nèi)存中,加速后速的讀取速度。

存儲(chǔ)文件

我們打開(kāi)RocketMQ在磁盤(pán)上持久化的目錄(store目錄下),便可以很直觀的看到CommitLog,ConsumeQueue,Index三個(gè)文件夾。(其中config文件夾中是運(yùn)行期間一些配置信息,而abort,checkpoint我會(huì)在后續(xù)的文章中講述它們的作用,關(guān)注“IT一刻鐘”吧,不要在猶豫中錯(cuò)過(guò)了重要內(nèi)容!)

如何從RocketMQ消息持久化設(shè)計(jì)看磁盤(pán)性能瓶頸的突破

CommitLog文件夾中的內(nèi)容(${ROCKET_HOME}/store/commitlog)

如何從RocketMQ消息持久化設(shè)計(jì)看磁盤(pán)性能瓶頸的突破

可以看到每個(gè)文件1G大小,以該文件中第一個(gè)偏移量為文件名,偏移量小于20位用0補(bǔ)齊。如圖所示,第一個(gè)文件的初始偏移量為9663676416,第二個(gè)文件的初始偏移量為10737418240。

CommitLog文件內(nèi)部存儲(chǔ)邏輯是,每條消息的前4個(gè)字節(jié)存儲(chǔ)該條消息的總長(zhǎng)度(包含長(zhǎng)度信息本身),隨后便是消息內(nèi)容。如圖所示:

如何從RocketMQ消息持久化設(shè)計(jì)看磁盤(pán)性能瓶頸的突破

消息的長(zhǎng)度=消息長(zhǎng)度信息(4字節(jié))+ 消息內(nèi)容長(zhǎng)度。

實(shí)現(xiàn)消息查找的步驟:

1.消費(fèi)者從消費(fèi)隊(duì)列中獲取到某個(gè)消息的偏移量offset與長(zhǎng)度size;

2.根據(jù)偏移量offset定位到消息所在的commitLog物理文件;

3.用偏移量與文件長(zhǎng)度取模,得到消息在這個(gè)commitLog文件內(nèi)部的偏移量;

4.從該偏移量取得size長(zhǎng)度的內(nèi)容返回即可。

注:如果只是根據(jù)消息偏移量查找消息,則首先找到文件內(nèi)偏移量,然后讀取前4個(gè)字節(jié)獲取消息的實(shí)際長(zhǎng)度,然后讀取指定的長(zhǎng)度。 這里有一個(gè)比較巧妙的設(shè)計(jì),CommitLog文件并不是每次生成一個(gè),然后寫(xiě)滿(mǎn)之后再創(chuàng)建下一個(gè),而是有一個(gè)預(yù)分配的機(jī)制。

即,CommitLog創(chuàng)建過(guò)程是把下一個(gè)文件的路徑、下下個(gè)文件的路徑以及文件大小作為參數(shù)封裝到AllocateRequest對(duì)象并添加到隊(duì)列中,后臺(tái)運(yùn)行的AllocateMappedFileService服務(wù)線程會(huì)不停地run,只要請(qǐng)求隊(duì)列里存在請(qǐng)求對(duì)象,就會(huì)去創(chuàng)建下個(gè)CommitLog,同時(shí)還會(huì)將下下個(gè)CommitLog預(yù)先創(chuàng)建并保存至請(qǐng)求隊(duì)列中等待下次獲取時(shí)直接返回,不用再次因?yàn)榈却鼵ommitLog創(chuàng)建分配而產(chǎn)生時(shí)間延遲。

ConsumeQueue文件夾中的內(nèi)容(${ROCKET_HOME}/store/consumequeue)

如何從RocketMQ消息持久化設(shè)計(jì)看磁盤(pán)性能瓶頸的突破

對(duì)于消費(fèi)者來(lái)說(shuō),最關(guān)心的莫過(guò)于某個(gè)主題下的所有消息,但是在RocketMQ中,不同主題下的消息都交錯(cuò)雜糅在同一個(gè)文件里,想要提高查詢(xún)速度,必須要構(gòu)建類(lèi)似于搜索索引的文件,于是就有了消費(fèi)隊(duì)列ConsumeQueue文件。

從實(shí)際物理存儲(chǔ)來(lái)說(shuō),ConsumeQueue對(duì)應(yīng)每個(gè)Topic和QueuId下面的文件,在上圖中,00000000000012000000就是在主題為sim-online-orders,QueueId為1下的ConsumeQueue文件。單個(gè)文件大小約5.72M,每個(gè)文件由30W條數(shù)據(jù)組成,每個(gè)文件默認(rèn)大小為600萬(wàn)個(gè)字節(jié),即每條數(shù)據(jù)20個(gè)字節(jié)。當(dāng)一個(gè)ConsumeQueue類(lèi)型的文件寫(xiě)滿(mǎn)了,則寫(xiě)入下一個(gè)文件。

ConsumeQueue文件內(nèi)部存儲(chǔ)邏輯如圖:

如何從RocketMQ消息持久化設(shè)計(jì)看磁盤(pán)性能瓶頸的突破

包含消息在commitLog文件的偏移量,消息長(zhǎng)度,消息tag的HashCode。 單個(gè)ConsumeQueue文件可以看作是ConsumeQueue條目數(shù)組,其下標(biāo)是ConsumeQueue的邏輯偏移量。

消息消費(fèi)隊(duì)列是RocketMQ為消息訂閱構(gòu)建的索引文件,目的在于提高主題與消息隊(duì)列檢索消息的速度。

Index文件夾中的內(nèi)容(${ROCKET_HOME}/store/index)

如何從RocketMQ消息持久化設(shè)計(jì)看磁盤(pán)性能瓶頸的突破

RocketMQ為了通過(guò)消息Key值查詢(xún)消息真正的實(shí)體內(nèi)容,引入了Hash索引機(jī)制。在實(shí)際的物理存儲(chǔ)上,文件名則是以創(chuàng)建時(shí)的時(shí)間戳命名的,固定的單個(gè)IndexFile文件大小約為400M,一個(gè)IndexFile可以保存2000W個(gè)索引。

我們先來(lái)看看Index索引文件的內(nèi)部存儲(chǔ)邏輯:

如何從RocketMQ消息持久化設(shè)計(jì)看磁盤(pán)性能瓶頸的突破

IndexFile包含三個(gè)部分:IndexHead,Hash槽,Index條目。

1.IndexHead,包含40個(gè)字節(jié),記錄一些統(tǒng)計(jì)信息:

    beginTimestamp:該索引文件中包含消息的最小存儲(chǔ)時(shí)間。

    endTimestamp:該索引文件中包含消息的最大存儲(chǔ)時(shí)間。

    beginPhyoffset:該索引文件中包含消息的最小物理偏移量(commitlog文件偏移量)。

    endPhyoffset:該索引文件中包含消息的最大物理偏移量(commitlog文件偏移量)。

    hashslotCount: hashslot個(gè)數(shù),并不是hash槽使用的個(gè)數(shù),在這里意義不大。

    indexCount: Index條目列表當(dāng)前已使用的個(gè)數(shù),Index條目在Index條目列表中按順序存儲(chǔ)。

2.Hash槽,默認(rèn)500萬(wàn)個(gè)槽,每個(gè)槽位存儲(chǔ)著該消息key的HashCode所對(duì)應(yīng)的最新Index條目的下標(biāo)數(shù)。

3.Index條目列表,默認(rèn)一個(gè)索引文件包含2000萬(wàn)個(gè)條目:

    hashcode:key的HashCode。

    phyoffset:消息對(duì)應(yīng)的物理偏移量。

    timedif:該消息存儲(chǔ)時(shí)間與第一條消息的時(shí)間戳的差值,小于0該消息無(wú)效。

    preIndexNo:該HashCode上一個(gè)條目的Index索引,當(dāng)出現(xiàn)hash沖突時(shí),構(gòu)建的鏈表結(jié)構(gòu)。

    大家看懂了這個(gè)數(shù)據(jù)結(jié)構(gòu)沒(méi)有?設(shè)計(jì)的真是精妙。

如果沒(méi)有理解,我給大家畫(huà)個(gè)圖,來(lái)體會(huì)一下這個(gè)數(shù)據(jù)結(jié)構(gòu)的精妙:

首先根據(jù)key的HashCode對(duì)槽數(shù)取模,得到槽位,然后將相應(yīng)的數(shù)據(jù)按順序存入到Index條目中,同時(shí)將條目數(shù)存回對(duì)應(yīng)的槽內(nèi)。

如何從RocketMQ消息持久化設(shè)計(jì)看磁盤(pán)性能瓶頸的突破

如果遇到Hash沖突,Index條目會(huì)通過(guò)pre index no構(gòu)建鏈表結(jié)構(gòu):

如何從RocketMQ消息持久化設(shè)計(jì)看磁盤(pán)性能瓶頸的突破

如圖第二個(gè)槽位沖突,第5條index條目的pre index no存儲(chǔ)原來(lái)的第二條序號(hào)。 其實(shí)就是HashMap的變形結(jié)構(gòu)。

通過(guò)以上結(jié)構(gòu)便可以用消息的key快速定位到消息內(nèi)容。

內(nèi)存映射

如果說(shuō)以上內(nèi)容是RocketMQ通過(guò)優(yōu)化數(shù)據(jù)結(jié)構(gòu)的方式來(lái)提高分布式消息隊(duì)列的性能,那么這里便是通過(guò)操作系統(tǒng)底層來(lái)優(yōu)化性能。

在Linux中,操作系統(tǒng)分為“用戶(hù)態(tài)”和“內(nèi)核態(tài)”,普通的標(biāo)準(zhǔn)IO操作文件時(shí),首先從磁盤(pán)將數(shù)據(jù)復(fù)制到內(nèi)核態(tài)內(nèi)存,接著從內(nèi)核態(tài)內(nèi)存復(fù)制到用戶(hù)態(tài)內(nèi)存,完成讀取操作,然后從用戶(hù)態(tài)內(nèi)存復(fù)制到網(wǎng)絡(luò)驅(qū)動(dòng)的內(nèi)核態(tài)內(nèi)存,最后從網(wǎng)絡(luò)驅(qū)動(dòng)的內(nèi)核態(tài)內(nèi)存復(fù)制到網(wǎng)卡中進(jìn)行傳輸,完成寫(xiě)出操作。

這個(gè)全過(guò)程中涉及到四次復(fù)制,可以說(shuō)效率是可見(jiàn)的低。

于是,在RocketMQ中,通過(guò)Java中的MappedByteBuffer(mmap方式)實(shí)現(xiàn)“零拷貝”,省去了向用戶(hù)態(tài)的內(nèi)存復(fù)制,提高了消息存儲(chǔ)和網(wǎng)絡(luò)發(fā)送的速度。

這里我們說(shuō)一說(shuō)什么是mmap內(nèi)存映射技術(shù)。

mmap技術(shù)可以直接將用戶(hù)進(jìn)程私有地址空間中的一塊區(qū)域與文件對(duì)象建立映射關(guān)系,這樣程序就好像可以直接從內(nèi)存中完成對(duì)文件讀/寫(xiě)操作一樣。當(dāng)發(fā)生缺頁(yè)中斷時(shí),直接將文件從磁盤(pán)拷貝至用戶(hù)態(tài)的進(jìn)程空間內(nèi),只進(jìn)行了一次數(shù)據(jù)拷貝。對(duì)于容量較大的文件來(lái)說(shuō)(文件大小一般需要限制在1.5~2G以下),采用mmap的方式讀/寫(xiě)效率和性能都非常高。如圖:

如何從RocketMQ消息持久化設(shè)計(jì)看磁盤(pán)性能瓶頸的突破

使用Mmap的限制:

a.Mmap映射的內(nèi)存空間釋放的問(wèn)題:由于映射的內(nèi)存空間本身就不屬于JVM的堆內(nèi)存區(qū)(Java Heap),因此其不受JVM GC的控制,卸載這部分內(nèi)存空間需要通過(guò)系統(tǒng)調(diào)用 unmap()方法來(lái)實(shí)現(xiàn)。然而unmap()方法是FileChannelImpl類(lèi)里實(shí)現(xiàn)的私有方法,無(wú)法直接顯示調(diào)用。RocketMQ中的做法是,通過(guò)Java反射的方式調(diào)用“sun.misc”包下的Cleaner類(lèi)的clean()方法來(lái)釋放映射占用的內(nèi)存空間;

b.MappedByteBuffer內(nèi)存映射大小限制:因?yàn)槠湔加玫氖翘摂M內(nèi)存(非JVM的堆內(nèi)存),大小不受JVM的-Xmx參數(shù)限制,但其大小也受到OS虛擬內(nèi)存大小的限制。一般來(lái)說(shuō),一次只能映射1.5~2G 的文件至用戶(hù)態(tài)的虛擬內(nèi)存空間,這也是為何RocketMQ默認(rèn)設(shè)置單個(gè)CommitLog日志數(shù)據(jù)文件為1G的原因了;
c.使用MappedByteBuffe的其他問(wèn)題:會(huì)存在內(nèi)存占用率較高和文件關(guān)閉不確定性的問(wèn)題;

突破性能瓶頸的處理方法有哪些?

1.簡(jiǎn)單高效的數(shù)據(jù)結(jié)構(gòu),提高檢索速度;

2.磁盤(pán)的順序?qū)懭?,避免無(wú)序io競(jìng)爭(zhēng),提高消息存儲(chǔ)速度;

3.預(yù)分配機(jī)制,降低文件處理等待時(shí)間;

4.依賴(lài)pagecache機(jī)制,批量從磁盤(pán)讀取消息并加載到緩存,提高讀取速度;

5.內(nèi)存映射機(jī)制,較少用戶(hù)態(tài)內(nèi)核態(tài)之間的復(fù)制次數(shù),提高處理效率。

看完上述內(nèi)容,你們對(duì)如何從RocketMQ消息持久化設(shè)計(jì)看磁盤(pán)性能瓶頸的突破有進(jìn)一步的了解嗎?如果還想了解更多知識(shí)或者相關(guān)內(nèi)容,請(qǐng)關(guān)注億速云行業(yè)資訊頻道,感謝大家的支持。

向AI問(wèn)一下細(xì)節(jié)

免責(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)容。

AI