文章目錄
- 頁緩存
- 順序寫
- 零拷貝
Kafka依賴于文件系統(更底層地來說就是磁盤)來存儲和緩存消息 。 那么kafka是如何讓自身在使用磁盤存儲的情況下達到高性能的?接下來主要從3各方面詳細解說。
頁緩存
頁緩存是操作系統實現的一種主要的磁盤緩存, 以此用來減少對磁盤I/0 的操作。 具體來說, 就是把磁盤中的數據緩存到內存中, 把對磁盤的訪間變為對內存的訪問。 為了彌補性能上的差異, 現代操作系統越來越 “ 激進地 ” 將內存作為磁盤緩存, 甚至會非常樂意將所有可用的內存用作磁盤緩存, 這樣當內存回收時也幾乎沒有性能損失, 所有對于磁盤的讀寫也將經由統一的緩存。
當一個進程準備讀取磁盤上的文件內容時, 操作系統會先查看待讀取的數據所在的頁
(page)是否在頁緩存(pagecache)中, 如果存在(命中)則直接返回數據, 從而避免了對物理磁盤的I/O操作;如果沒有命中, 則操作系統會向磁盤發起讀取請求并將讀取的數據頁存入頁緩存, 之后再將數據返回給進程。 同樣, 如果 一個進程需要將數據寫入磁盤, 那么操作系統也會檢測數據對應的頁是否在頁緩存中, 如果不存在, 則會先在頁緩存中添加相應的頁, 最后將數據寫入對應的頁。 被修改過后的頁也就變成了臟頁, 操作系統會在合適的時間把臟頁中的數據寫入磁盤, 以保持數據的一致性。
Linux操作系統中的vm.dirty_background_ra巨o參數用來指定當臟頁數量達到系統
內存的百分之多少之后就會觸發pdflush/flush/kdmflush等后臺回寫進程的運行來處理臟頁, 一般設置為小千10 的值即可,但不建議設置為0。與這個參數對應的還有一個vm.dirty_ratio參數, 它用來指定當臟頁數量達到系統內存的百分之多少之后就不得不開始對臟頁進行處理,在此過程中, 新的VO請求會被阻擋直至所有臟頁被沖刷到磁盤中。
對一個進程而言, 它會在進程內部緩存處理所需的數據, 然而這些數據有可能還緩存在操作系統的頁緩存中, 因此同 一份數據有可能被緩存了兩次。 并且, 除非使用DirectI/0的方式,否則頁緩存很難被禁止。 此外, 用過Java的人一般都知道兩點事實: 對象的內存開銷非常大,通常會是真實數據大小的幾倍甚至更多, 空間使用率低下; Java的垃圾回收會隨著堆內數據的增多而變得越來越慢。 基千這些因素, 使用文件系統并依賴于頁緩存的做法明顯要優于維護 一個進程內緩存或其他結構, 至少我們可以省去了一份進程內部的緩存消耗, 同時還可以通過結構緊湊的字節碼來替代使用對象的方式以節省更多的空間。如此, 我們可以在32GB的機器上使用28GB至30GB的內存而不用擔心GC所帶來的性能間題。 此外, 即使Kafka服務重啟,頁緩存還是會保持有效, 然而進程內的緩存卻需要重建。 這樣也極大地簡化了代碼邏輯, 因為維護頁緩存和文件之間的一致性交由操作系統來負責, 這樣會比進程內維護更加安全有效。
Kafka 中大量使用了頁緩存, 這是Kafka 實現高吞吐的重要因素之一。 雖然消息都是先被寫入頁緩存, 然后由操作系統負責具體的刷盤任務的, 但在Kafka中同樣提供了同步刷盤及間斷性強制刷盤( fsync )的功能,這些功能可以通過 log.flush.interval . messages 、log.flush .int erval .m s 等參數來控制。同步刷盤可以提高消息的可靠性,防止由于機器掉電等異常造成處于頁緩存而沒有及時寫入磁盤的消息丟失。不過筆者并不建議這么做,刷盤任務就應交由操作系統去調配,消息的可靠性應該由多副本機制來保障,而不是由同步刷盤這種嚴重影響性能的行為來保障 。
Linux 系統會使用磁盤的 一部分作為 swap 分區,這樣可以進行進程的調度:把當前非活躍的進程調入 swap 分區,以此把內存空出來讓給活躍的進程。對大量使用系統頁緩存的 Kafka而言,應當盡量避免這種內存的交換,否則會對它各方面的性能產生很大的負面影響 。我們可以通過修改 vm.swappiness 參數 ( Linux 系統參數〉來進行調節 。 vm. swappiηess 參數的上限為 100,它表示積極地使用 swap 分區,并把內存上的數據及時地搬運到 swap 分區中;vm.swappiness 參數的下限為 0 ,表示在任何情況下都不要發生交換( vm . swappiness=0的含義在不同版本的 Linux 內核中不太相同,這里采用的是變更后的最新解釋) ,這樣一來 ,當內存耗盡時會根據一定的規則突然中止某些進程。可以將這個參數的值設置為 1 ,這樣保留了 swap 的機制而又最大限度地限制了它對 Kafka 性能的影響 。
順序寫
Kafka順序寫磁盤是其實現高性能數據存儲的關鍵技術之一。
-
日志分段:Kafka的每個分區在磁盤上以日志文件的形式存儲,這些日志文件會被切分成多個日志段。新消息會不斷追加到當前活躍的日志段末尾,當達到一定條件(如文件大小達到閾值或時間間隔),就會創建新的日志段。
-
順序追加:生產者發送到Kafka的消息會按照到達的順序依次追加到分區日志文件中,不會隨機插入或修改中間的內容,保證了磁盤寫入是順序的。
優勢
-
提升寫入性能:與隨機寫相比,順序寫磁盤時磁頭移動距離小,減少了尋道時間和旋轉延遲,能充分利用磁盤的順序讀寫帶寬,大幅提升寫入速度,使Kafka能處理大量并發寫入請求。
-
提高數據可靠性:順序寫的方式使數據在磁盤上連續存儲,降低了數據碎片化和存儲錯誤的風險,同時也便于Kafka進行日志的分段、清理和壓縮等管理操作,有助于保證數據的完整性和一致性。
實現條件
-
底層文件系統支持:Kafka依賴底層文件系統提供的順序寫支持,如常見的ext4、XFS等文件系統都能很好地配合Kafka實現順序寫。
-
合理配置參數:通過合理設置 log.segment.bytes (日志段大小)、 log.roll.hours (日志滾動時間間隔)等參數,能確保Kafka按預期進行日志分段和順序寫入,避免因參數設置不當導致的寫入性能下降。
零拷貝
Kafka 還使用零拷 貝 ( Zero-Copy )技術來進一步提升性能 。 所謂的零拷貝是指將數據直接從磁盤文件復制到網卡設備中,而不需要經由應用程序之手 。零拷貝大大提高了應用程序的性能,減少了內核和用戶模式之間的上下文切換 。 對 Linux操作系統而言,零拷貝技術依賴于底層的 sendfile() 方法實現 。 對應于 Java 語言,
FileChannal.transferTo()方法的底層實現就是 sendfile()方法 。
傳統的文件讀寫中 文件經歷了 4 次復制的過程:
- (1)調用read()時, 文件A中的內容被復制到了內核態下的Read Bu?er中。
- (2)CPU控制將內核模式數據復制到用戶模式下。
- (3)調用write()時, 將用戶模式下的內容復制到內核模式下的SocketBu?er中。
- (4)將內核模式下的SocketBu?er的數據復制到網卡設備中傳送。
從上面的過程可以看出, 數據平白無故地從內核模式到用戶模式 “ 走了一 圈 ” , 浪費了2次復制過程: 第一次是從內核模式復制到用戶模式;第二次是從用戶模式再復制回內核模式,即上面4次過程中的第2步和第3步。 而且在上面的過程中, 內核和用戶模式的上下文的切換也是4次。
如果采用了零拷貝技術, 那么應用程序可以直接請求內核把磁盤中的數據傳輸給Socket,如圖所示。
零拷貝技術通過 DMA (Direct Memory Access) 技術將文件內容復制到內核模式下的 Read Bu?er 中。 不過沒有數據被復制到 Socket Bu?er, 相反只有包含數據的位置和長度的信息的文件描述符被加到 Socket Bu?er 中。 DMA 引擎直接將數據從內核模式中傳遞到網卡設備(協議引擎)。 這里數據只經歷了2次復制就從磁盤中傳送出去了, 并且上下文切換也變成了2次。零拷貝是針對內核模式而言的, 數據在內核模式下實現了零拷貝。