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