Kafka和RocketMQ底層存儲之那些你不知道的事

                        小編:管理員 75閱讀 2022.08.01

                        我們都知道 RocketMQ 和 Kafka 消息都是存在磁盤中的,那為什么消息存磁盤讀寫還可以這么快?有沒有做了什么優化?都是存磁盤它們兩者的實現之間有什么區別么?各自有什么優缺點?

                        今天我們就來一探究竟。

                        存儲介質-磁盤

                        一般而言消息中間件的消息都存儲在本地文件中,因為從效率來看直接放本地文件是最快的,并且穩定性最高。畢竟要是放類似數據庫等第三方存儲中的話,就多一個依賴少一份安全,并且還有網絡的開銷。

                        那對于將消息存入磁盤文件來說一個流程的瓶頸就是磁盤的寫入和讀取。我們知道磁盤相對而言讀寫速度較慢,那通過磁盤作為存儲介質如何實現高吞吐呢?

                        順序讀寫

                        答案就是順序讀寫。

                        首先了解一下頁緩存,頁緩存是操作系統用來作為磁盤的一種緩存,減少磁盤的I/O操作。

                        在寫入磁盤的時候其實是寫入頁緩存中,使得對磁盤的寫入變成對內存的寫入。寫入的頁變成臟頁,然后操作系統會在合適的時候將臟頁寫入磁盤中。

                        在讀取的時候如果頁緩存命中則直接返回,如果頁緩存 miss 則產生缺頁中斷,從磁盤加載數據至頁緩存中,然后返回數據。

                        并且在讀的時候會預讀,根據局部性原理當讀取的時候會把相鄰的磁盤塊讀入頁緩存中。在寫入的時候會后寫,寫入的也是頁緩存,這樣存著可以將一些小的寫入操作合并成大的寫入,然后再刷盤。

                        而且根據磁盤的構造,順序 I/O 的時候,磁頭幾乎不用換道,或者換道的時間很短。

                        根據網上的一些測試結果,順序寫盤的速度比隨機寫內存還要快。

                        當然這樣的寫入存在數據丟失的風險,例如機器突然斷電,那些還未刷盤的臟頁就丟失了。不過可以調用fsync強制刷盤,但是這樣對于性能的損耗較大。

                        因此一般建議通過多副本機制來保證消息的可靠,而不是同步刷盤。

                        可以看到順序 I/O 適應磁盤的構造,并且還有預讀和后寫。RocketMQ 和 Kafka 都是順序寫入和近似順序讀取。它們都采用文件追加的方式來寫入消息,只能在日志文件尾部寫入新的消息,老的消息無法更改。

                        mmap-文件內存映射

                        從上面可知訪問磁盤文件會將數據加載到頁緩存中,但是頁緩存屬于內核空間,用戶空間訪問不了,因此數據還需要拷貝到用戶空間緩沖區。

                        可以看到數據需要從頁緩存再經過一次拷貝程序才能訪問的到,因此還可以通過mmap來做一波優化,利用內存映射文件來避免拷貝。

                        簡單的說文件映射就是將程序虛擬頁面直接映射到頁緩存上,這樣就無需有內核態再往用戶態的拷貝,而且也避免了重復數據的產生。并且也不必再通過調用read或write方法對文件進行讀寫,可以通過映射地址加偏移量的方式直接操作。

                        sendfile-零拷貝

                        既然消息是存在磁盤中的,那消費者來拉消息的時候就得從磁盤拿。我們先來看看一般發送文件的流程是如何的。

                        簡單說下DMA是什么,全稱 Direct Memory Access ,它可以獨立地直接讀寫系統內存,不需要 CPU 介入,像顯卡、網卡之類都會用DMA。

                        可以看到數據其實是冗余的,那我們來看看mmap之后的發送文件流程是怎樣的。

                        可以看到上下文切換的次數沒有變化,但是數據少拷貝一份,這和我們上文提到的mmap能達到的效果是一樣的。

                        但是數據還是冗余了一份,這不是可以直接把數據從頁緩存拷貝到網卡不就好了嘛?sendfile就有這個功效。我們先來看看Linux2.1版本中的sendfile。

                        因為就一個系統調用就滿足了發送的需求,相比read + write或者mmap + write上下文切換肯定是少了的,但是好像數據還是有冗余啊。是的,因此 Linux2.4 版本的 sendfile + 帶 「分散-收集(Scatter-gather)」的DMA。實現了真正的無冗余。

                        這就是我們常說的零拷貝,在 Java 中FileChannal.transferTo()底層用的就是sendfile。

                        接下來我們看看以上說的幾點在 RocketMQ 和 Kafka中是如何應用的。

                        RocketMQ 和 Kafka 的應用RocketMQ

                        采用Topic混合追加方式,即一個 CommitLog 文件中會包含分給此 Broker 的所有消息,不論消息屬于哪個 Topic 的哪個 Queue 。

                        所以所有的消息過來都是順序追加寫入到 CommitLog 中,并且建立消息對應的 CosumerQueue ,然后消費者是通過 CosumerQueue 得到消息的真實物理地址再去 CommitLog 獲取消息的?梢詫 CosumerQueue 理解為消息的索引。

                        在 RocketMQ 中不論是 CommitLog 還是 CosumerQueue 都采用了 mmap。

                        在發消息的時候默認用的是將數據拷貝到堆內存中,然后再發送。我們來看下代碼。

                        可以看到這個配置transferMsgByHeap默認是 true ,那我們再看消費者拉消息時候的代碼。

                        可以看到 RocketMQ 默認把消息拷貝到堆內 Buffer 中,再塞到響應體里面發送。但是可以通過參數配置不經過堆,不過也并沒有用到真正的零拷貝,而是通過mapedBuffer 發送到 SocketBuffer 。

                        所以 RocketMQ 用了順序寫盤、mmap。并沒有用到 sendfile ,還有一步頁緩存到 SocketBuffer 的拷貝。

                        然后拉消息的時候嚴格的說對于 CommitLog 來說讀取是隨機的,因為 CommitLog 的消息是混合的存儲的,但是從整體上看,消息還是從 CommitLog 順序讀的,都是從舊數據到新數據有序的讀取。并且一般而言消息存進去馬上就會被消費,因此消息這時候應該還在頁緩存中,所以不需要讀盤。

                        而且我們在上面提到,頁緩存會定時刷盤,這刷盤不可控,并且內存是有限的,會有swap等情況。

                        而且mmap其實只是做了映射,當真正讀取頁面的時候產生缺頁中斷,才會將數據真正加載到內存中,這對于消息隊列來說可能會產生監控上的毛刺。

                        因此 RocketMQ 做了一些優化,有:文件預分配和文件預熱。

                        文件預分配

                        CommitLog 的大小默認是1G,當超過大小限制的時候需要準備新的文件,而 RocketMQ 就起了一個后臺線程AllocateMappedFileService,不斷的處理AllocateRequest,AllocateRequest其實就是預分配的請求,會提前準備好下一個文件的分配,防止在消息寫入的過程中分配文件,產生抖動。

                        文件預熱

                        有一個warmMappedFile方法,它會把當前映射的文件,每一頁遍歷多去,寫入一個0字節,然后再調用mlock和madvise(MADV_WILLNEED)。

                        我們再來看下this.mlock,內部其實就是調用了mlock和madvise(MADV_WILLNEED)。

                        mlock:可以將進程使用的部分或者全部的地址空間鎖定在物理內存中,防止其被交換到swap空間。

                        madvise:給操作系統建議,說這文件在不久的將來要訪問的,因此,提前讀幾頁可能是個好主意。

                        RocketMQ 小結

                        順序寫盤,整體來看是順序讀盤,并且使用了 mmap,不是真正的零拷貝。又因為頁緩存的不確定性和 mmap 惰性加載(訪問時缺頁中斷才會真正加載數據),用了文件預先分配和文件預熱即每頁寫入一個0字節,然后再調用mlock和madvise(MADV_WILLNEED)。

                        Kafka

                        Kafka 的日志存儲和 RocketMQ 不一樣,它是一個分區一個文件。

                        Kafka 的消息寫入對于單分區來說也是順序寫,如果分區不多的話從整體上看也算順序寫,它的日志文件并沒有用到 mmap,而索引文件用了 mmap。但發消息 Kafka 用到了零拷貝。

                        對于消息的寫入來說 mmap 其實沒什么用,因為消息是從網絡中來。而對于發消息來說 sendfile 對比 mmap+write 我覺得效率更高,因為少了一次頁緩存到 SocketBuffer 中的拷貝。

                        來看下Kafka發消息的源碼,最終調用的是FileChannel.transferTo,底層就是 sendfile。

                        從 Kafka 源碼中我沒看到有類似于 RocketMQ的 mlock 等操作,我覺得原因是首先日志也沒用到 mmap,然后 swap 其實可以通過 Linux 系統參數vm.swappiness來調節,這里建議設置為1,而不是0。

                        假設內存真的不足,設置為 0 的話,在內存耗盡的情況下,又不能 swap,則會突然中止某些進程。設置個 1,起碼還能拖一下,如果有良好的監控手段,還能給個機會發現一下,不至于突然中止。

                        RocketMQ & Kafka 對比

                        首先都是順序寫入,不過 RocketMQ 是把消息都存一個文件中,而 Kafka 是一個分區一個文件。

                        每個分區一個文件在遷移或者數據復制層面上來說更加得靈活。

                        但是分區多了的話,寫入需要頻繁的在多個文件之間來回切換,對于每個文件來說是順序寫入的,但是從全局看其實算隨機寫入,并且讀取的時候也是一樣,算隨機讀。而就一個文件的 RocketMQ 就沒這個問題。

                        從發送消息來說 RocketMQ 用到了 mmap + write 的方式,并且通過預熱來減少大文件 mmap 因為缺頁中斷產生的性能問題。而 Kafka 則用了 sendfile,相對而言我覺得 kafka 發送的效率更高,因為少了一次頁緩存到 SocketBuffer 中的拷貝。

                        并且 swap 問題也可以通過系統參數來設置。

                        關聯標簽:
                        少妇各种各样BBBⅩXX