Kafka和RocketMQ底層存儲(chǔ)之那些你不知道的事
大家好,我是yes。
我們都知道 RocketMQ 和 Kafka 消息都是存在磁盤中的,那為什么消息存磁盤讀寫還可以這么快?有沒有做了什么優(yōu)化?都是存磁盤它們兩者的實(shí)現(xiàn)之間有什么區(qū)別么?各自有什么優(yōu)缺點(diǎn)?
今天我們就來一探究竟。
存儲(chǔ)介質(zhì)-磁盤
一般而言消息中間件的消息都存儲(chǔ)在本地文件中,因?yàn)閺男蕘砜粗苯臃疟镜匚募亲羁斓?,并且穩(wěn)定性最高。畢竟要是放類似數(shù)據(jù)庫(kù)等第三方存儲(chǔ)中的話,就多一個(gè)依賴少一份安全,并且還有網(wǎng)絡(luò)的開銷。
那對(duì)于將消息存入磁盤文件來說一個(gè)流程的瓶頸就是磁盤的寫入和讀取。我們知道磁盤相對(duì)而言讀寫速度較慢,那通過磁盤作為存儲(chǔ)介質(zhì)如何實(shí)現(xiàn)高吞吐呢?
順序讀寫
答案就是順序讀寫。
首先了解一下頁(yè)緩存,頁(yè)緩存是操作系統(tǒng)用來作為磁盤的一種緩存,減少磁盤的I/O操作。
在寫入磁盤的時(shí)候其實(shí)是寫入頁(yè)緩存中,使得對(duì)磁盤的寫入變成對(duì)內(nèi)存的寫入。寫入的頁(yè)變成臟頁(yè),然后操作系統(tǒng)會(huì)在合適的時(shí)候?qū)⑴K頁(yè)寫入磁盤中。
在讀取的時(shí)候如果頁(yè)緩存命中則直接返回,如果頁(yè)緩存 miss 則產(chǎn)生缺頁(yè)中斷,從磁盤加載數(shù)據(jù)至頁(yè)緩存中,然后返回?cái)?shù)據(jù)。
并且在讀的時(shí)候會(huì)預(yù)讀,根據(jù)局部性原理當(dāng)讀取的時(shí)候會(huì)把相鄰的磁盤塊讀入頁(yè)緩存中。在寫入的時(shí)候會(huì)后寫,寫入的也是頁(yè)緩存,這樣存著可以將一些小的寫入操作合并成大的寫入,然后再刷盤。
而且根據(jù)磁盤的構(gòu)造,順序 I/O 的時(shí)候,磁頭幾乎不用換道,或者換道的時(shí)間很短。
根據(jù)網(wǎng)上的一些測(cè)試結(jié)果,順序?qū)懕P的速度比隨機(jī)寫內(nèi)存還要快。
當(dāng)然這樣的寫入存在數(shù)據(jù)丟失的風(fēng)險(xiǎn),例如機(jī)器突然斷電,那些還未刷盤的臟頁(yè)就丟失了。不過可以調(diào)用 fsync強(qiáng)制刷盤,但是這樣對(duì)于性能的損耗較大。
因此一般建議通過多副本機(jī)制來保證消息的可靠,而不是同步刷盤。
可以看到順序 I/O 適應(yīng)磁盤的構(gòu)造,并且還有預(yù)讀和后寫。RocketMQ 和 Kafka 都是順序?qū)懭牒徒祈樞蜃x取。它們都采用文件追加的方式來寫入消息,只能在日志文件尾部寫入新的消息,老的消息無法更改。
mmap-文件內(nèi)存映射
從上面可知訪問磁盤文件會(huì)將數(shù)據(jù)加載到頁(yè)緩存中,但是頁(yè)緩存屬于內(nèi)核空間,用戶空間訪問不了,因此數(shù)據(jù)還需要拷貝到用戶空間緩沖區(qū)。
可以看到數(shù)據(jù)需要從頁(yè)緩存再經(jīng)過一次拷貝程序才能訪問的到,因此還可以通過mmap來做一波優(yōu)化,利用內(nèi)存映射文件來避免拷貝。
簡(jiǎn)單的說文件映射就是將程序虛擬頁(yè)面直接映射到頁(yè)緩存上,這樣就無需有內(nèi)核態(tài)再往用戶態(tài)的拷貝,而且也避免了重復(fù)數(shù)據(jù)的產(chǎn)生。并且也不必再通過調(diào)用read或write方法對(duì)文件進(jìn)行讀寫,可以通過映射地址加偏移量的方式直接操作。
sendfile-零拷貝
既然消息是存在磁盤中的,那消費(fèi)者來拉消息的時(shí)候就得從磁盤拿。我們先來看看一般發(fā)送文件的流程是如何的。
簡(jiǎn)單說下DMA是什么,全稱 Direct Memory Access ,它可以獨(dú)立地直接讀寫系統(tǒng)內(nèi)存,不需要 CPU 介入,像顯卡、網(wǎng)卡之類都會(huì)用DMA。
可以看到數(shù)據(jù)其實(shí)是冗余的,那我們來看看mmap之后的發(fā)送文件流程是怎樣的。
可以看到上下文切換的次數(shù)沒有變化,但是數(shù)據(jù)少拷貝一份,這和我們上文提到的mmap能達(dá)到的效果是一樣的。
但是數(shù)據(jù)還是冗余了一份,這不是可以直接把數(shù)據(jù)從頁(yè)緩存拷貝到網(wǎng)卡不就好了嘛?sendfile就有這個(gè)功效。我們先來看看Linux2.1版本中的sendfile。
因?yàn)榫鸵粋€(gè)系統(tǒng)調(diào)用就滿足了發(fā)送的需求,相比 read + write或者 mmap + write上下文切換肯定是少了的,但是好像數(shù)據(jù)還是有冗余啊。是的,因此 Linux2.4 版本的 sendfile + 帶 「分散-收集(Scatter-gather)」的DMA。實(shí)現(xiàn)了真正的無冗余。
這就是我們常說的零拷貝,在 Java 中FileChannal.transferTo()底層用的就是sendfile。
接下來我們看看以上說的幾點(diǎn)在 RocketMQ 和 Kafka中是如何應(yīng)用的。
RocketMQ 和 Kafka 的應(yīng)用
RocketMQ
采用Topic混合追加方式,即一個(gè) CommitLog 文件中會(huì)包含分給此 Broker 的所有消息,不論消息屬于哪個(gè) Topic 的哪個(gè) Queue 。
所以所有的消息過來都是順序追加寫入到 CommitLog 中,并且建立消息對(duì)應(yīng)的 CosumerQueue ,然后消費(fèi)者是通過 CosumerQueue 得到消息的真實(shí)物理地址再去 CommitLog 獲取消息的。可以將 CosumerQueue 理解為消息的索引。
在 RocketMQ 中不論是 CommitLog 還是 CosumerQueue 都采用了 mmap。
在發(fā)消息的時(shí)候默認(rèn)用的是將數(shù)據(jù)拷貝到堆內(nèi)存中,然后再發(fā)送。我們來看下代碼。
可以看到這個(gè)配置 transferMsgByHeap默認(rèn)是 true ,那我們?cè)倏聪M(fèi)者拉消息時(shí)候的代碼。
可以看到 RocketMQ 默認(rèn)把消息拷貝到堆內(nèi) Buffer 中,再塞到響應(yīng)體里面發(fā)送。但是可以通過參數(shù)配置不經(jīng)過堆,不過也并沒有用到真正的零拷貝,而是通過mapedBuffer 發(fā)送到 SocketBuffer 。
所以 RocketMQ 用了順序?qū)懕P、mmap。并沒有用到 sendfile ,還有一步頁(yè)緩存到 SocketBuffer 的拷貝。
然后拉消息的時(shí)候嚴(yán)格的說對(duì)于 CommitLog 來說讀取是隨機(jī)的,因?yàn)?CommitLog 的消息是混合的存儲(chǔ)的,但是從整體上看,消息還是從 CommitLog 順序讀的,都是從舊數(shù)據(jù)到新數(shù)據(jù)有序的讀取。并且一般而言消息存進(jìn)去馬上就會(huì)被消費(fèi),因此消息這時(shí)候應(yīng)該還在頁(yè)緩存中,所以不需要讀盤。
而且我們?cè)谏厦嫣岬剑?strong>頁(yè)緩存會(huì)定時(shí)刷盤,這刷盤不可控,并且內(nèi)存是有限的,會(huì)有swap等情況。
而且mmap其實(shí)只是做了映射,當(dāng)真正讀取頁(yè)面的時(shí)候產(chǎn)生缺頁(yè)中斷,才會(huì)將數(shù)據(jù)真正加載到內(nèi)存中,這對(duì)于消息隊(duì)列來說可能會(huì)產(chǎn)生監(jiān)控上的毛刺。
因此 RocketMQ 做了一些優(yōu)化,有:文件預(yù)分配和文件預(yù)熱。
文件預(yù)分配
CommitLog 的大小默認(rèn)是1G,當(dāng)超過大小限制的時(shí)候需要準(zhǔn)備新的文件,而 RocketMQ 就起了一個(gè)后臺(tái)線程 AllocateMappedFileService,不斷的處理 AllocateRequest,AllocateRequest其實(shí)就是預(yù)分配的請(qǐng)求,會(huì)提前準(zhǔn)備好下一個(gè)文件的分配,防止在消息寫入的過程中分配文件,產(chǎn)生抖動(dòng)。
文件預(yù)熱
有一個(gè)warmMappedFile方法,它會(huì)把當(dāng)前映射的文件,每一頁(yè)遍歷多去,寫入一個(gè)0字節(jié),然后再調(diào)用mlock和 madvise(MADV_WILLNEED)。
我們?cè)賮砜聪?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: " operator="" mono",="" consolas,="" monaco,="" menlo,="" monospace;word-break:="" break-all;color:="" rgb(60,="" 112,="" 198);"="">this.mlock,內(nèi)部其實(shí)就是調(diào)用了mlock和 madvise(MADV_WILLNEED)。
mlock:可以將進(jìn)程使用的部分或者全部的地址空間鎖定在物理內(nèi)存中,防止其被交換到swap空間。
madvise:給操作系統(tǒng)建議,說這文件在不久的將來要訪問的,因此,提前讀幾頁(yè)可能是個(gè)好主意。
RocketMQ 小結(jié)
順序?qū)懕P,整體來看是順序讀盤,并且使用了 mmap,不是真正的零拷貝。又因?yàn)轫?yè)緩存的不確定性和 mmap 惰性加載(訪問時(shí)缺頁(yè)中斷才會(huì)真正加載數(shù)據(jù)),用了文件預(yù)先分配和文件預(yù)熱即每頁(yè)寫入一個(gè)0字節(jié),然后再調(diào)用mlock和 madvise(MADV_WILLNEED)。
Kafka
Kafka 的日志存儲(chǔ)和 RocketMQ 不一樣,它是一個(gè)分區(qū)一個(gè)文件。
Kafka 的消息寫入對(duì)于單分區(qū)來說也是順序?qū)懀绻謪^(qū)不多的話從整體上看也算順序?qū)?,它的日志文件并沒有用到 mmap,而索引文件用了 mmap。但發(fā)消息 Kafka 用到了零拷貝。
對(duì)于消息的寫入來說 mmap 其實(shí)沒什么用,因?yàn)橄⑹菑木W(wǎng)絡(luò)中來。而對(duì)于發(fā)消息來說 sendfile 對(duì)比 mmap+write 我覺得效率更高,因?yàn)樯倭艘淮雾?yè)緩存到 SocketBuffer 中的拷貝。
來看下Kafka發(fā)消息的源碼,最終調(diào)用的是 FileChannel.transferTo,底層就是 sendfile。
從 Kafka 源碼中我沒看到有類似于 RocketMQ的 mlock 等操作,我覺得原因是首先日志也沒用到 mmap,然后 swap 其實(shí)可以通過 Linux 系統(tǒng)參數(shù) vm.swappiness來調(diào)節(jié),這里建議設(shè)置為1,而不是0。
假設(shè)內(nèi)存真的不足,設(shè)置為 0 的話,在內(nèi)存耗盡的情況下,又不能 swap,則會(huì)突然中止某些進(jìn)程。設(shè)置個(gè) 1,起碼還能拖一下,如果有良好的監(jiān)控手段,還能給個(gè)機(jī)會(huì)發(fā)現(xiàn)一下,不至于突然中止。
RocketMQ & Kafka 對(duì)比
首先都是順序?qū)懭?,不過 RocketMQ 是把消息都存一個(gè)文件中,而 Kafka 是一個(gè)分區(qū)一個(gè)文件。
每個(gè)分區(qū)一個(gè)文件在遷移或者數(shù)據(jù)復(fù)制層面上來說更加得靈活。
但是分區(qū)多了的話,寫入需要頻繁的在多個(gè)文件之間來回切換,對(duì)于每個(gè)文件來說是順序?qū)懭氲?,但是從全局看其?shí)算隨機(jī)寫入,并且讀取的時(shí)候也是一樣,算隨機(jī)讀。而就一個(gè)文件的 RocketMQ 就沒這個(gè)問題。
從發(fā)送消息來說 RocketMQ 用到了 mmap + write 的方式,并且通過預(yù)熱來減少大文件 mmap 因?yàn)槿表?yè)中斷產(chǎn)生的性能問題。而 Kafka 則用了 sendfile,相對(duì)而言我覺得 kafka 發(fā)送的效率更高,因?yàn)樯倭艘淮雾?yè)緩存到 SocketBuffer 中的拷貝。
并且 swap 問題也可以通過系統(tǒng)參數(shù)來設(shè)置。
特別推薦一個(gè)分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長(zhǎng)按關(guān)注一下:
長(zhǎng)按訂閱更多精彩▼
如有收獲,點(diǎn)個(gè)在看,誠(chéng)摯感謝
ckquote>
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問題,請(qǐng)聯(lián)系我們,謝謝!