消息寫讀
在Kafka的數據存儲架構中,一個主題由一個或多個分區組成。在物理存儲上,每個主題-分區都對應着硬盤上的一個獨立目錄,而消息數據則以日誌段文件(Log Segment)的形式存儲在這些目錄中。隨着數據的不斷寫入,當一個日誌段文件達到預設的大小(例如1GB)或時間閾值時,它會被關閉並變為只讀,同時一個新的可寫日誌段文件會被創建。這個過程稱為日誌滾動(Log Rolling)。
從單個分區的微觀視角看,所有消息都是以追加(Append-only)的方式順序寫入當前活躍的日誌段文件。順序寫入幾乎消除了硬盤的尋道時間,其性能接近於內存的讀寫速度。再結合操作系統的頁緩存(Page Cache)機制以及零拷貝(Zero-Copy)技術,只要分區文件的總數在硬件承載範圍內,Kafka就能實現極高的數據吞吐量。
然而,當一個Kafka集羣中的分區數量失控時(例如,成千上萬個主題,每個主題又有數十個分區),問題就會浮現。從操作系統的全局視角來看,硬盤控制器需要在極短的時間內響應來自成百上千個不同文件的寫請求。這意味着物理硬盤的磁頭必須在這些文件的不同位置之間頻繁移動,即所謂的硬盤尋道。這種高併發的、對不同文件位置的寫入,使得宏觀上的硬盤I/O模式退化為事實上的隨機寫。
儘管寫入端存在這種潛在風險,但Kafka的多分區文件設計為消費端讀取消息帶來了顯著的優勢。首先,它天然支持批量讀取消息。消費者可以一次性從Broker拉取一個數據塊(例如1MB)。這種批量處理的方式極大地減少了網絡往返的開銷和系統調用的次數。更重要的是,當消費者順序消費一個分區時,當第一批數據從硬盤讀入頁面緩存後,後續的順序讀取請求極有可能直接命中緩存。

在RocketMQ中,所有主題的所有消息數據,無論其邏輯歸屬如何,都會被首先寫入到一個名為提交日誌(CommitLog)的中心化大文件中。這個CommitLog文件由多個固定大小(默認為1GB)的文件順序組成,當前只有一個文件處於可寫狀態。因為所有寫操作都集中在這一點,即便隨着主題和隊列數量的急劇增加,硬盤在同一時間也只對一個文件進行追加寫入,從而保證了絕對的順序寫。為了進一步提升I/O效率,RocketMQ採用內存映射(mmap)技術來讀寫CommitLog。
當消息需要被消費時,直接掃描龐大的CommitLog顯然是低效的。為此,RocketMQ為每個主題的每個消息隊列(ConsumeQueue)建立了一個獨立的、輕量級的消費隊列文件。每個ConsumeQueue條目都是固定長度的(20字節),其中存儲了該消息在CommitLog中的物理偏移量(Offset)(8字節)、消息總大小(Size)(4字節)以及消息Tag的哈希碼(8字節)。當消費者拉取消息時,它首先順序讀取對應ConsumeQueue文件中的索引條目,根據獲取到的物理偏移量,再到CommitLog中定位並讀取到完整的消息數據。這種“先讀索引,再讀數據”分離的模式,既保證了寫入的絕對順序性,又實現了消費時的高效查找。
此外,為了支持按消息Key或時間範圍等維度的快速查詢,RocketMQ還提供了可選的索引文件(IndexFile)。其底層數據結構本質上是一個存儲在硬盤上的哈希表。IndexFile由文件頭、哈希槽(Slot Table)和索引條目列表(Index Linked List)三部分組成。當根據Key查找時,先計算Key的哈希值並定位到對應的哈希槽,該槽內存儲了指向最新一條索引條目的指針。由於可能存在哈希衝突,具有相同哈希值的索引條目會通過前向指針形成一個鏈表。

零拷貝
零拷貝(Zero-Copy),其根本目標是減少甚至消除數據在內核空間(Kernel Space)和用户空間(User Space)之間不必要的拷貝。在傳統的數據傳輸流程中,數據從硬盤到網絡發送的路徑通常是:硬盤 -> 內核緩衝區 -> 用户緩衝區 -> 內核Socket緩衝區 -> 網卡。這個過程中,數據至少被拷貝了四次,並且伴隨着多次處理器上下文的切換(從用户態到內核態),這些操作都會大量消耗處理器和內存資源。
零拷貝技術通過更底層的系統調用,讓內核直接在不同的I/O設備之間傳遞數據,從而繞過用户空間的干預。其實現高度依賴於操作系統的支持,例如在LINUX中,最經典的系統調用是sendfile和splice,以及通過mmap實現的變相零拷貝。sendfile指令可以直接將數據從一個文件描述符(如硬盤文件)傳輸到另一個文件描述符(如網絡套接字),數據全程在內核空間中流轉,避免了進入用户空間,從而將拷貝次數從四次減少到兩次(內核緩衝區到Socket緩衝區)。
Kafka在向消費者發送數據時,廣泛使用了Java NIO庫中的FileChannel.transferTo()方法。在LINUX系統上,這個Java方法底層正是通過sendfile系統調用實現的。當消費者請求數據時,Kafka Broker可以直接將硬盤上的日誌段文件(通常已存在於操作系統的頁面緩存中)的數據塊直接複製到網卡緩衝區,整個過程數據沒有進入Kafka的Java虛擬機的堆內存。
RocketMQ 則主要通過內存映射來利用零拷貝的優勢。它使用Java的MappedByteBuffer將核心數據文件(如CommitLog)映射到內存。
1)寫入時:生產者發送的消息被寫入到這個內存映射區域,這幾乎等同於內存寫入,速度極快。後續由操作系統負責將這部分內存(髒頁)異步刷寫回硬盤。
2)讀取時:當消費者需要數據時,RocketMQ可以直接從內存映射區讀取。此外,RocketMQ還會主動對MappedByteBuffer進行預熱,即在服務啓動時就將文件內容提前加載到物理內存(頁面緩存)中,確保後續的讀寫操作都能命中內存。

未完待續
很高興與你相遇!如果你喜歡本文內容,記得關注哦!!!