1. 程式人生 > 實用技巧 >Kafka和RocketMQ底層儲存之那些你不知道的事

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

大家好,我是yes。

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

今天我們就來一探究竟。

先說下快的主要原因就是順序讀寫、mmap、sendfile。我們先來看看這幾點,然後再盤一下 RocketMQ 和 Kafka是如何應用的。

儲存介質-磁碟

一般而言訊息中介軟體的訊息都儲存在本地檔案中,因為從效率來看直接放本地檔案是最快的,並且穩定性最高。畢竟要是放類似資料庫等第三方儲存中的話,就多一個依賴少一份安全,並且還有網路的開銷。

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

順序讀寫

答案就是順序讀寫

首先了解一下頁快取,頁快取是作業系統用來作為磁碟的一種快取,減少磁碟的I/O操作。

在寫入磁碟的時候其實是寫入頁快取中,使得對磁碟的寫入變成對記憶體的寫入。寫入的頁變成髒頁,然後作業系統會在合適的時候將髒頁寫入磁碟中。

在讀取的時候如果頁快取命中則直接返回,如果頁快取 miss 則產生缺頁中斷,從磁碟載入資料至頁快取中,然後返回資料。

並且在讀的時候會預讀,根據區域性性原理當讀取的時候會把相鄰的磁碟塊讀入頁快取中。在寫入的時候會後寫,寫入的也是頁快取,這樣存著可以將一些小的寫入操作合併成大的寫入,然後再刷盤。

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

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

當然這樣的寫入存在資料丟失的風險,例如機器突然斷電,那些還未刷盤的髒頁就丟失了。不過可以呼叫 fsync 強制刷盤,但是這樣對於效能的損耗較大。

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

可以看到順序 I/O 適應磁碟的構造,並且還有預讀和後寫。 RocketMQ 和 Kafka 都是順序寫入和近似順序讀取。它們都採用檔案追加的方式來寫入訊息,只能在日誌檔案尾部寫入新的訊息,老的訊息無法更改。

mmap-檔案記憶體對映

從上面可知訪問磁碟檔案會將資料載入到頁快取中,但是頁快取屬於核心空間,使用者空間訪問不了,因此資料還需要拷貝到使用者空間緩衝區。

可以看到資料需要從頁快取再經過一次拷貝程式才能訪問的到,因此還可以通過mmap來做一波優化,利用記憶體對映檔案來避免拷貝。

簡單的說檔案對映就是將程式虛擬頁面直接對映到頁快取上,這樣就無需有核心態再往使用者態的拷貝,而且也避免了重複資料的產生。並且也不必再通過呼叫readwrite方法對檔案進行讀寫,可以通過對映地址加偏移量的方式直接操作

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位元組,然後再呼叫mlockmadvise(MADV_WILLNEED)

我們再來看下this.mlock,內部其實就是呼叫了mlockmadvise(MADV_WILLNEED)

mlock:可以將程序使用的部分或者全部的地址空間鎖定在實體記憶體中,防止其被交換到swap空間。

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

RocketMQ 小結

順序寫盤,整體來看是順序讀盤,並且使用了 mmap,不是真正的零拷貝。又因為頁快取的不確定性和 mmap 惰性載入(訪問時缺頁中斷才會真正載入資料),用了檔案預先分配和檔案預熱即每頁寫入一個0位元組,然後再呼叫mlockmadvise(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 問題也可以通過系統引數來設定。

最後

這篇文章中間寫 RocketMQ 卡殼了,原始碼還是不太熟,有點繞。 多虧丁威大佬的點撥。不然我就陷入了死衚衕出不來了。

最後再推薦下丁威大佬和周繼鋒大佬的《RocketMQ技術內幕:RocketMQ架構設計與實現原理》。對 RocketMQ 有興趣的同學可以看看。

文章如果哪裡有紕漏請抓緊聯絡我,感謝!


我是 yes,從一點點到億點點,我們下篇見

往期推薦:

訊息佇列面試熱點一鍋端

圖解+程式碼|常見限流演算法以及限流在單機分散式場景下的思考

表弟面試被虐我教他快取連招

面試官:說說Kafka處理請求的全流程

Kafka索引設計的亮點

Kafka日誌段讀寫分析