天天看點

Kafka 是怎麼存儲的?為什麼速度那麼快?

文章收錄位址:Java-Bang  專注于系統架構、高可用、高性能、高并發類技術分享

Kafka 依賴于檔案系統(更底層地來說就是磁盤)來存儲和緩存消息。在我們的印象中,對于各個存儲媒體的速度認知大體同下圖所示的相同,層級越高代表速度越快。很顯然,磁盤處于一個比較尴尬的位置,這不禁讓我們懷疑 Kafka 采用這種持久化形式能否提供有競争力的性能。在傳統的消息中間件 RabbitMQ 中,就使用記憶體作為預設的存儲媒體,而磁盤作為備選媒體,以此實作高吞吐和低延遲的特性。然而,事實上磁盤可以比我們預想的要快,也可能比我們預想的要慢,這完全取決于我們如何使用它。

Kafka 是怎麼存儲的?為什麼速度那麼快?

有關測試結果表明,一個由6塊 7200r/min 的 RAID-5 陣列組成的磁盤簇的線性(順序)寫入速度可以達到 600MB/s,而随機寫入速度隻有 100KB/s,兩者性能相差6000倍。作業系統可以針對線性讀寫做深層次的優化,比如預讀(read-ahead,提前将一個比較大的磁盤塊讀入記憶體)和後寫(write-behind,将很多小的邏輯寫操作合并起來組成一個大的實體寫操作)技術。順序寫盤的速度不僅比随機寫盤的速度快,而且也比随機寫記憶體的速度快,如下圖所示。

Kafka 是怎麼存儲的?為什麼速度那麼快?

Kafka 在設計時采用了檔案追加的方式來寫入消息,即隻能在日志檔案的尾部追加新的消息,并且也不允許修改已寫入的消息,這種方式屬于典型的順序寫盤的操作,是以就算Kafka使用磁盤作為存儲媒體,它所能承載的吞吐量也不容小觑。但這并不是讓 Kafka 在性能上具備足夠競争力的唯一因素,我們不妨繼續分析。

頁緩存是作業系統實作的一種主要的磁盤緩存,以此用來減少對磁盤 I/O 的操作。具體來說,就是把磁盤中的資料緩存到記憶體中,把對磁盤的通路變為對記憶體的通路。為了彌補性能上的差異,現代作業系統越來越“激進地”将記憶體作為磁盤緩存,甚至會非常樂意将所有可用的記憶體用作磁盤緩存,這樣當記憶體回收時也幾乎沒有性能損失,所有對于磁盤的讀寫也将經由統一的緩存。

當一個程序準備讀取磁盤上的檔案内容時,作業系統會先檢視待讀取的資料所在的頁(page)是否在頁緩存(pagecache)中,如果存在(命中)則直接傳回資料,進而避免了對實體磁盤的 I/O 操作;如果沒有命中,則作業系統會向磁盤發起讀取請求并将讀取的資料頁存入頁緩存,之後再将資料傳回給程序。

同樣,如果一個程序需要将資料寫入磁盤,那麼作業系統也會檢測資料對應的頁是否在頁緩存中,如果不存在,則會先在頁緩存中添加相應的頁,最後将資料寫入對應的頁。被修改過後的頁也就變成了髒頁,作業系統會在合适的時間把髒頁中的資料寫入磁盤,以保持資料的一緻性。

Linux 作業系統中的 vm.dirty_background_ratio 參數用來指定當髒頁數量達到系統記憶體的百分之多少之後就會觸發 pdflush/flush/kdmflush 等背景回寫程序的運作來處理髒頁,一般設定為小于10的值即可,但不建議設定為0。與這個參數對應的還有一個 vm.dirty_ratio 參數,它用來指定當髒頁數量達到系統記憶體的百分之多少之後就不得不開始對髒頁進行處理,在此過程中,新的 I/O 請求會被阻擋直至所有髒頁被沖刷到磁盤中。對髒頁有興趣的讀者還可以自行查閱 vm.dirty_expire_centisecs、vm.dirty_writeback.centisecs 等參數的使用說明。

對一個程序而言,它會在程序内部緩存處理所需的資料,然而這些資料有可能還緩存在作業系統的頁緩存中,是以同一份資料有可能被緩存了兩次。并且,除非使用 Direct I/O 的方式,否則頁緩存很難被禁止。此外,用過 Java 的人一般都知道兩點事實:對象的記憶體開銷非常大,通常會是真實資料大小的幾倍甚至更多,空間使用率低下;Java 的垃圾回收會随着堆内資料的增多而變得越來越慢。基于這些因素,使用檔案系統并依賴于頁緩存的做法明顯要優于維護一個程序内緩存或其他結構,至少我們可以省去了一份程序内部的緩存消耗,同時還可以通過結構緊湊的位元組碼來替代使用對象的方式以節省更多的空間。如此,我們可以在32GB的機器上使用28GB至30GB的記憶體而不用擔心 GC 所帶來的性能問題。

此外,即使 Kafka 服務重新開機,頁緩存還是會保持有效,然而程序内的緩存卻需要重建。這樣也極大地簡化了代碼邏輯,因為維護頁緩存和檔案之間的一緻性交由作業系統來負責,這樣會比程序内維護更加安全有效。

Kafka 中大量使用了頁緩存,這是 Kafka 實作高吞吐的重要因素之一。雖然消息都是先被寫入頁緩存,然後由作業系統負責具體的刷盤任務的,但在 Kafka 中同樣提供了同步刷盤及間斷性強制刷盤(fsync)的功能,這些功能可以通過 log.flush.interval.messages、log.flush.interval.ms 等參數來控制。

同步刷盤可以提高消息的可靠性,防止由于機器掉電等異常造成處于頁緩存而沒有及時寫入磁盤的消息丢失。不過筆者并不建議這麼做,刷盤任務就應交由作業系統去調配,消息的可靠性應該由多副本機制來保障,而不是由同步刷盤這種嚴重影響性能的行為來保障。

Linux 系統會使用磁盤的一部分作為 swap 分區,這樣可以進行程序的排程:把目前非活躍的程序調入 swap 分區,以此把記憶體空出來讓給活躍的程序。對大量使用系統頁緩存的 Kafka 而言,應當盡量避免這種記憶體的交換,否則會對它各方面的性能産生很大的負面影響。

我們可以通過修改 vm.swappiness 參數(Linux 系統參數)來進行調節。vm.swappiness 參數的上限為100,它表示積極地使用 swap 分區,并把記憶體上的資料及時地搬運到 swap 分區中;vm.swappiness 參數的下限為0,表示在任何情況下都不要發生交換(vm.swappiness = 0 的含義在不同版本的 Linux 核心中不太相同,這裡采用的是變更後的最新解釋),這樣一來,當記憶體耗盡時會根據一定的規則突然中止某些程序。筆者建議将這個參數的值設定為1,這樣保留了 swap 的機制而又最大限度地限制了它對 Kafka 性能的影響。

Kafka 是怎麼存儲的?為什麼速度那麼快?