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