偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

別造輪子了,先來“抄”一下 RocketMQ 的文件編程藝術

開發(fā) 前端
學習 RocketMQ,我們學的不僅是 “術”(具體的實現技巧),更是 “道”(解決問題的思想和方法論)。希望通過本文的剖析,能為你打開一扇通往高性能存儲世界的大門,并在未來的系統(tǒng)設計中,能夠更加游刃有余地運用這些閃耀著智慧光芒的編程藝術。?

最近有朋友問我,如何從零開始設計一套高性能、高可靠、且支持快速查詢的文件存儲系統(tǒng)?

這是一個典型的后端面試高頻題,也是一個極具挑戰(zhàn)的真實工程難題。我的回答是:與其閉門造車,不如站在巨人的肩膀上——去“抄”一個頂級的開源實現。

他追問:“抄誰?”

我說:“當然是抄 RocketMQ!它在文件處理上的設計,堪稱 Java 領域的教科書,充滿了工程的智慧與藝術。無論是海量消息的順序寫入,還是高效的索引查詢,亦或是對內存和磁盤的極致運用,RocketMQ 的每一處細節(jié)都值得我們細細品味。下面,就讓我們一同深入這座寶庫,探索其背后的文件編程藝術。”

消息存儲格式看文件編程

commitlog 文件設計:學習文件編程的起點

我們知道,RocketMQ 的全量消息均存儲在 commitlog 文件中。由于每條消息的大小不一,我們面臨第一個挑戰(zhàn):如何高效地組織這些消息,以便在讀取時能夠準確地區(qū)分每條消息的邊界?

在基于文件的編程模型中,首要任務是定義一套清晰的消息存儲格式。一個通用的實踐是將數據結構設計為 Header + Body 的形式。其中,Header 部分采用定長設計,存放元數據信息;Body 部分則存放實際的數據。在 RocketMQ 的存儲協議中,我們可以將記錄消息總長度的 4 字節(jié)視為 Header,其后的所有字段則構成 Body,包含了與消息相關的業(yè)務屬性,它們共同按照預定格式組裝。

圖片圖片

對于 Header + Body 這種協議,提取一條完整消息通常分為兩步:首先,讀取定長的 Header,從中解析出 Body 的長度(在 RocketMQ 中即為消息的總長度)。然后,根據這個長度,從消息的起始位置 讀取完整的消息數據,并按照預定義的格式解析出各個業(yè)務字段。

那問題又來了,如果確定一條消息的開頭呢?難不成從文件的開始處開始遍歷?

正如關系型數據庫會為每條記錄分配一個唯一的 ID,在文件編程模型中,我們也為每條消息引入了一個關鍵的身份標識:消息物理偏移量(Physical Offset),它精確地指明了消息在文件中的起始存儲位置。

圖片圖片

因此,通過 物理偏移量 + 消息大?。⊿IZE) 這一組合,我們便能輕而易舉地從海量數據中精確定位并提取出任何一條完整的消息。

此外,commitlog 的文件組織還揭示了另一個通用實踐:文件通常以一個“魔數”(Magic Number)開頭,用于快速校驗文件類型和完整性。同時,在文件末尾,可能會使用特殊填充(PAD)來處理空間。例如,當文件剩余空間不足以容納一條完整的消息時,系統(tǒng)不會將消息拆分存儲,而是用 PAD 填充剩余部分,以保證下一條消息能從新文件的起始位置寫入,維持了消息的原子性。

consumequeue 文件設計:索引的藝術

commitlog 文件基于物理偏移量查詢消息效率極高,但若要按 Topic 進行檢索,則顯得力不從心。為了解決這一痛點,RocketMQ 精心設計了 consumequeue文件作為消費隊列索引。

圖片圖片

consumequeue 的設計極具巧思。其核心在于,每個索引條目都采用固定長度設計:8 字節(jié)的 commitlog 物理偏移量、4 字節(jié)的消息長度和 8 字節(jié)的 Tag 哈希值。這里存儲的是 Tag 的哈希值而非原始字符串,正是為了確保每個條目定長。這種設計使得consumequeue 文件可以像數組一樣,通過下標(queueOffset)進行快速隨機訪問,極大地提升了索引查詢性能。

由此可見,在高性能文件存儲設計中,為特定查詢場景構建高效的索引至關重要。而索引設計的關鍵原則之一,就是采用定長條目,從而實現類似數組的 O(1) 復雜度快速定位。

性能飛躍:內存映射與頁緩存

解決了數據存儲格式與唯一標識的問題后,下一個核心挑戰(zhàn)是如何提升I/O性能。在文件編程實踐中,為了便于管理和數據刪除(例如過期的消息),通常會采用固定大小的日志文件策略,將數據切分成多個大小相等的文件段。RocketMQ 的 commitlog文件夾中那些 1G 大小的文件正是這一思想的體現。

圖片圖片

采用定長文件的一個核心優(yōu)勢在于,它極大地簡化了內存映射(Memory Mapping)的實現。通過內存映射技術,我們可以將磁盤文件直接映射到進程的虛擬地址空間,之后就可以像訪問內存一樣讀寫磁盤上的數據,從而繞過傳統(tǒng) read/write 系統(tǒng)調用帶來的多次數據拷貝,顯著提升文件操作性能。

在 Java 中使用內存映射的示例代碼如下:

FileChannel fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
MappedByteBuffer mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);

實現要點如下:

  • 首先需要通過 RandomAccessFile 構建一個文件讀寫通道 FileChannel。
  • 再通過 FileChannel 的 map 方法創(chuàng)建內存映射區(qū)域 MappedByteBuffer。

在 Linux 系統(tǒng)中,MappedByteBuffer 的背后正是強大的頁緩存(Page Cache)。Linux 會盡可能地利用空閑物理內存作為頁緩存,以緩存磁盤數據。當應用程序讀寫文件時,實際上是在與頁緩存交互。如果數據在緩存中(緩存命中),則無需磁盤 I/O;如果不在(緩存未命中),系統(tǒng)會觸發(fā)一個缺頁中斷,透明地將數據從磁盤加載到頁緩存中。整個過程由操作系統(tǒng)自動管理,對應用層開發(fā)者完全透明,極大地簡化了高性能應用的開發(fā)。

通過內存映射寫入的數據,首先會進入頁緩存,并不會立即刷寫到磁盤。持久化操作由操作系統(tǒng)根據其策略在后臺異步完成。這意味著,即使 Broker 進程異常崩潰,只要操作系統(tǒng)仍然運行,頁緩存中的數據就不會丟失,最終會被寫入磁盤,保證了一定程度的可靠性。然而,如果發(fā)生機器斷電或操作系統(tǒng)宕機等災難性故障,尚未從頁緩存刷入磁盤的數據將會永久丟失,這是內存映射技術需要權衡的風險。

磁盤性能提升:順序寫

要進一步壓榨磁盤性能,另一個關鍵的設計原則是使用順序寫替代隨機寫。與隨機寫相比,磁盤順序寫的性能要高出幾個數量級。這一原則在所有高性能存儲系統(tǒng)中都得到了廣泛應用。以大家熟悉的 MySQL InnoDB 存儲引擎為例:當執(zhí)行一條更新語句時,數據首先在內存(Buffer Pool)中被修改。為了保證事務的持久性,InnoDB 并不會立即將修改后的數據頁(這會導致隨機 I/O)刷回磁盤,而是先將變更以 Redo Log 的形式順序追加到一個專用的日志文件中,并確保 Redo Log 被同步刷盤。數據文件本身的更新則可以在后臺異步、批量地進行。

圖片圖片

試想,如果沒有 Redo Log,每次更新都直接修改磁盤上的數據文件,那么更新不同表的數據就會導致磁頭在磁盤上大幅度來回尋道,產生大量的隨機 I/O,性能將慘不忍睹。通過引入 Redo Log,InnoDB 將多次離散的隨機寫操作,巧妙地轉換成了一次集中的順序寫操作,盡管多了一步寫日志的操作,但整體性能卻得到了質的飛躍。這個例子雄辯地證明了順序寫相比隨機寫的巨大優(yōu)勢。

因此,在設計文件存儲模型時,一個黃金法則是:盡可能地將操作設計為順序追加(Append-Only),避免原地更新(In-Place Update)。

資源管理的智慧:引用計數器

在基于 NIO 的文件編程中,我們頻繁地與 ByteBuffer 打交道。為了在不進行數據拷貝的前提下共享緩沖區(qū)內容,slice() 方法被廣泛使用。它能創(chuàng)建一個與原 ByteBuffer 共享同一塊內存區(qū)域,但擁有獨立 position、limit 和 mark 指針的新 ByteBuffer。

圖片圖片

上圖 selectMappedBuffer 方法的作用,正是從一個內存映射文件(如 commitlog)的指定位置“切割”出一段數據。這種零拷貝操作雖然高效,但也引入了復雜的資源管理問題:被主 MappedByteBuffer 切割(slice)出的多個子 ByteBuffer,它們的生命周期各不相同。我們必須確保在所有子 ByteBuffer 都使用完畢之前,主 MappedByteBuffer 不能被釋放,否則將導致懸空指針和程序崩潰。

RocketMQ 如何優(yōu)雅地解決這個問題呢?答案是引入引用計數器(Reference Counting)。

其核心思想是:每次調用 slice() 派生出一個新的 ByteBuffer 時,就將主對象的引用計數加一;當任何一個派生對象使用完畢被釋放時,就將引用計數減一。只有當引用計數歸零時,才真正執(zhí)行底層的資源釋放操作。

圖片圖片

結合 selectMappedBuffer 方法的實現,我們可以看到:

  1. 調用 hold() 方法來增加引用計數,這標志著 MappedByteBuffer 又被“借用”了一次。
  2. 返回的 SelectMappedBufferResult 對象中封裝了派生出的 ByteBuffer。當使用者調用其 release() 方法時,內部會調用 ReferenceResource 的 release(),使引用計數減一。

可靠與性能的權衡:同步與異步刷盤

內存映射機制極大地提升了寫入性能,但其“數據先到內存,后到磁盤”的特性也帶來了新的抉擇:當 Broker 接收到消息后,是應該在數據寫入頁緩存后就向客戶端返回成功,還是必須等到數據被持久化到磁盤后才返回?

這本質上是系統(tǒng)性能與數據可靠性之間的權衡。為此,RocketMQ 提供了兩種持久化策略:同步刷盤和異步刷盤。

所謂的 “刷盤”,在代碼層面其實就是調用 FileChannel 或 MappedByteBuffer 的 force() 方法,強制操作系統(tǒng)將頁緩存中對應的數據寫入物理磁盤。

圖片圖片

同步刷盤

同步刷盤策略要求 Broker 將消息寫入內存后,必須立即將其持久化到磁盤,然后才能向客戶端返回成功響應。

但這里有一個關鍵問題:同步刷盤是每條消息都單獨刷一次盤嗎?答案是否定的。

RocketMQ 的同步刷盤實現隱藏著一個重要的優(yōu)化:組提交(Group Commit)。其入口位于 GroupCommitService 類中,從類名就能看出其設計思想。

圖片圖片

實現中有兩個關鍵點:

  • 組提交:一次刷盤操作會將當前所有待刷盤的消息(一個消息組)一次性寫入磁盤,而不是一條一條地刷。這極大地減少了磁盤 I/O 次數。
  • 同步轉異步:實現上采用 CountDownLatch 將同步調用轉換為異步處理模式。主線程提交一個刷盤請求后,會限時等待 (await) 后臺刷盤線程的結果,從而實現業(yè)務邏輯的解耦。

接下來繼續(xù)探討組提交的設計理念。

圖片圖片

判斷一條刷盤請求成功的條件:當前已刷盤指針大于該條消息對應的物理偏移量,這里使用了刷盤重試機制。然后喚醒主線程并返回刷盤結果。

所謂的組提交,其核心理念理念是調用刷盤時使用的是 MappedFileQueue.flush 方法,該方法并不是只將一條消息寫入磁盤,而是會將當期未刷盤的數據一次性刷寫到磁盤,既組提交,故即使在同步刷盤情況下,也并不是每一條消息都會被執(zhí)行 flush 方法,為了更直觀的展現組提交的設計理念,給出如下流程圖:

圖片圖片

異步刷盤

同步刷盤提供了最高級別的數據可靠性,但犧牲了寫入性能??紤]到 RocketMQ 的消息首先寫入 PageCache,在非極端掉電情況下數據丟失的概率很小,因此,如果業(yè)務能容忍極低概率的數據丟失以換取更高的吞吐量,可以選擇異步刷盤。

異步刷盤模式下,Broker 將消息寫入 PageCache 后會立即向客戶端返回成功,然后由一個后臺線程(FlushRealTimeService)定時將臟頁數據刷入磁盤,默認間隔為 500ms。

你可能會猜測這是用 ScheduledExecutorService 之類的定時任務實現的,但 RocketMQ 的實現更為精妙。它同樣利用了帶超時時間的 CountDownLatch.await()。這種方式的好處在于:

  • 在沒有新消息寫入時,線程會安靜地等待 500ms,避免了空輪詢帶來的 CPU 消耗。
  • 一旦有新消息寫入(wakeup() 被調用),線程會立即被喚醒執(zhí)行刷盤,而無需等待 500ms 周期結束,保證了刷盤的及時性。

保障數據一致性:文件恢復機制

圖片圖片

在 RocketMQ 的架構中,commitlog 是主數據文件,而 consumequeue 和 indexFile 等索引文件是根據 commitlog 的內容異步構建的。既然是異步構建,就必然存在數據不一致的風險窗口。例如,Broker 在將數據轉發(fā)到 consumequeue 之前異常關閉,重啟后如何保證數據的一致性?

在探討恢復機制前,我們先設想幾個典型的異常場景:

  • 消息已同步刷盤到 commitlog,但在轉發(fā)到 consumequeue 之前,機器斷電。
  • 一次批量刷盤操作(例如 100MB 數據)在執(zhí)行到一半(例如 50MB)時,機器斷電,導致 commitlog 文件末尾存在一條不完整的消息。
  • commitlog 刷盤成功,但在更新 checkpoint 文件(記錄各文件刷盤點)之前,進程退出。

在 RocketMQ 中,文件恢復分為正常停止和異常停止兩種場景。

兩種場景定位恢復起點的邏輯略有不同,但一旦定位到起始恢復文件,后續(xù)的文件校正思路是統(tǒng)一的:

  • 首先嘗試恢復 consumequeue:遍歷 consumequeue 文件,根據其定長格式(8字節(jié)偏移量 + 4字節(jié)長度 + 8字節(jié)Tag哈希碼),找到最后一條完整條目所指向的 commitlog 物理偏移量,記為 maxPhysicalOfConsumequeue。
  • 然后嘗試恢復 commitlog:遍歷 commitlog 文件,校驗魔數,并根據消息存儲格式找到最后一條完整的、校驗合格的消息,記錄其物理偏移量 physicalOffset。
  • 對比與校正:

如果 commitlog 的有效偏移量小于 consumequeue 中記錄的最大偏移量,說明 consumequeue 中存在無效的“超前”索引,需要被截斷。

如果 commitlog 的有效偏移量大于 consumequeue 中記錄的最大偏移量,說明有部分消息還未建立索引,需要從 commitlog 中重新讀取這部分消息,并重建 consumequeue 和其他索引文件。

那么,如何高效地定位到可能需要恢復的文件呢?

正常退出定位文件

在 RocketMQ 啟動時候會創(chuàng)建一個名為 abort 的文件,并在正常關閉時將其刪除。因此,通過檢查 abort 文件是否存在,即可判斷上次是否為異常退出。

圖片圖片

對于正常退出場景,恢復策略相對樂觀:

  • ConsumeQueue 的恢復從每個主題的第一個文件開始。
  • commitlog 的恢復從倒數第三個文件開始向后檢查。因為正常退出時,大部分文件都已完整寫入并刷盤。

異常退出定位文件

異常退出時,不確定性大大增加,恢復策略必須更加嚴謹。此時,checkpoint 文件就派上了用場。該文件記錄了 commitlog、consumequeue 等文件的最后一次刷盤時間戳。

圖片圖片

  • physicMsgTimestamp:commitlog 文件最后的刷盤的時間點
  • logicsMsgTimestamp: consumequeue 文件最后的刷盤時間點
  • indexMsgTimestamp: indexfile 文件最后的刷盤時間點

checkpoint 文件的更新總是在 commitlog 刷盤成功之后進行。

圖片圖片

這意味著 checkpoint 中記錄的刷盤點是“絕對可靠”的,早于該時間點的數據一定已經落盤。基于此,異常退出時的恢復策略是:

  • ConsumeQueue 是按照 topic 進行恢復的,從第一文件開始恢復。
  • commitlog 的恢復從最后一個文件開始,逐個向前掃描。讀取每個文件的第一條消息的存儲時間,與 checkpoint 中記錄的 physicMsgTimestamp 進行比較。一旦找到一個文件的起始時間小于等于 checkpoint 的時間戳,那么就從這個文件開始執(zhí)行恢復流程。

文件恢復的入口位于 DefaultMessageStore#recover 方法,讀者可根據上述理念,自行探索源碼,定會事半功倍。

終極性能優(yōu)化:Java 零拷貝

在高性能網絡文件服務中,“零拷貝”(Zero-Copy)是一個高頻詞匯。這里我們不贅述其底層原理,而是直接看 RocketMQ 在消息消費場景下,如何結合 Netty 實現零拷貝,將磁盤文件高效地發(fā)送到網絡。

圖片圖片

零拷貝的關鍵實現要點:

  • 當需要發(fā)送消息時,RocketMQ 首先通過內存映射從 commitlog 文件中獲取一個代表消息數據的 ByteBuf。重要的是,這個 ByteBuf 僅僅是一個引用,數據本身仍在頁緩存中,并未被加載到 Java 堆內存。
  • 然后,將這個 ByteBuf 包裝成一個 Netty 的 FileRegion 對象,并最終調用其 transferTo 方法。該方法的底層實現委托給了 FileChannel.transferTo()。

在 ManyMessageTransfer 類中可以看到具體的 transferTo 實現:

圖片圖片

FileChannel.transferTo() 在 Linux 系統(tǒng)上會觸發(fā) sendfile() 系統(tǒng)調用。這個系統(tǒng)調用可以直接在內核空間中,將數據從文件句柄(頁緩存)拷貝到套接字緩沖區(qū),避免了數據在內核態(tài)和用戶態(tài)之間的來回拷貝,實現了真正的零拷貝,極大地提升了數據傳輸效率。

本文從文件編程的視角,跟隨 RocketMQ 的設計學習了諸多優(yōu)秀的工程技巧。希望這些內容能對你有所啟發(fā)

結語

學習 RocketMQ,我們學的不僅是 “術”(具體的實現技巧),更是 “道”(解決問題的思想和方法論)。希望通過本文的剖析,能為你打開一扇通往高性能存儲世界的大門,并在未來的系統(tǒng)設計中,能夠更加游刃有余地運用這些閃耀著智慧光芒的編程藝術。

責任編輯:武曉燕 來源: JAVA日知錄
相關推薦

2015-08-06 10:14:15

造輪子facebook

2020-09-08 08:45:39

jupyter插件代碼

2022-04-27 20:02:22

Dubbo注冊中心開發(fā)

2023-02-06 17:27:48

2023-02-14 12:40:44

ChatGPTAI聊天

2022-12-07 10:34:45

AST前端編譯

2018-09-03 14:05:08

編程語言Python編程技巧

2022-05-13 09:16:49

Python代碼

2021-02-26 13:59:41

RocketMQProducer底層

2021-10-09 18:26:59

二叉樹多叉樹搜索

2024-04-07 09:34:59

集群RemoteTransform

2012-01-13 16:00:05

愛國者馮軍蘋果

2021-10-28 19:10:51

RustPythonjs

2021-04-09 10:26:43

Python編程技術

2020-06-11 18:06:03

電腦電路板元件

2021-11-08 22:38:44

電腦科技C盤

2022-06-29 10:04:01

PiniaVuex

2024-05-14 08:11:56

ReactuseState造輪子

2024-07-01 08:01:45

API網關接口

2023-09-21 11:03:31

開發(fā)輪子工具
點贊
收藏

51CTO技術棧公眾號