重新理解RocketMQ Commit Log儲存協議

蓝易云發表於2024-10-23

本文作者:李偉,社群裡大家叫小偉,Apache RocketMQ Committer,RocketMQ Python客戶端專案Owner ,Apache Doris Contributor,騰訊雲RocketMQ開發工程師。

最近突然感覺:很多軟體、硬體在設計上是有root reason的,不是by desgin如此,而是解決了那時、那個場景的那個需求。一旦瞭解後,就會感覺在和設計者對話,瞭解他們的思路,學習他們的方法,思維同屏:活到老學到老。

1大家思考

1.1 Consumer Queue Offset是連續的嗎, 為什麼?

1.2 Commit Log Offset是連續的嗎, 為什麼?

1.3 Java寫的檔案,預設是大端序還是小端序,為什麼?

2Commit Log真實分佈

在大家思考之際, 我們回想下commit log是怎麼分佈的呢?

在Broker配置的儲存根目錄下,透過檢視Broker實際生成的commit log檔案可以看到類似下面的資料檔案分佈:

圖片

Broker真實資料檔案儲存分佈

可以看到,真實的儲存檔案有多個, 每一個都是以一串類似數字的字串作為檔名的,並且大小1G。

我們結合原始碼可以知道,實際的抽象模型如下:

圖片

Commit Log儲存檔案分佈抽象

由上圖得知:

  • Commit Log是一類檔案的稱呼,實際上Commit Log檔案有很多個, 每一個都可以稱為Commit Log檔案。

如圖中表示了總共有T個Commit Log檔案,他們按照由過去到現在的建立時間排列。

  • 每個Commit Log檔案都儲存訊息, 並且是按照訊息的寫入順序儲存的,並且總是在寫建立時間最大的檔案,並且同一個時刻只能有一個執行緒在寫。

如圖中第1個檔案,1,2,3,4...表示這個檔案的第幾個訊息,可以看到第1234個訊息是第1個Commit Log檔案的最後一個訊息,第1235個訊息是第2個Commit Log的第1個訊息。

說明1:每個Commit Log檔案裡的全部訊息實際佔用的儲存空間大小<=1G。這個問題大家自行思考下原因。

說明2:每次寫Commit Log時, RocketMQ都會加鎖,程式碼片段見https://github.com/apache/rocketmq/blob/7676cd9366a3297925deabcf27bb590e34648645/store/src/main/java/org/apache/rocketmq/store/CommitLog.java#L676-L722

圖片

append加鎖

我們看到Commit Log檔案中有很多個訊息,按照既定的協議儲存的,那具體協議是什麼呢, 你是怎麼知道的呢?

3Commit Log儲存協議

關於Commit Log儲存協議,我們問了下ChatGPT, 它是這麼回覆我的,雖然不對,但是這個回覆格式和說明已經非常接近答案了。

圖片

ChatGPT回覆

我們翻看原始碼,具體說明下:https://github.com/apache/rocketmq/blob/rocketmq-all-4.9.3/store/src/main/java/org/apache/rocketmq/store/CommitLog.java#L1547-L1587

圖片

Commit Log儲存協議

我整理後, 如下圖;

圖片

我理解的Commit Log儲存協議

說明1:我整理後的訊息協議編號和程式碼中不是一致的,程式碼中只是標明瞭順序, 真實物理檔案中的儲存協議會更詳細。

說明2:在我寫的《RocketMQ分散式訊息中介軟體:核心原理與最佳實踐》中,這個圖缺少了Body內容,這裡加了,也更詳細的補充了其他資料。

這裡有幾個問題需要說明下:

  • 二進位制協議存在位元組序,也就是常說的大端、小端。大小端這裡不詳細說明感興趣的同學自己google或者問ChatGPT,回答肯定比我說的好。

  • 在java中, 一個byte佔用1個位元組,1個int佔用4個位元組,1個short佔用2個位元組,1個long佔用8個位元組。

  • Host的編碼並不是簡單的把IP:Port作為字串直接轉化為byte陣列,而是每個數字當作byte依次編碼。在下一節的Golang程式碼中會說明。

  • 擴充套件資訊的編碼中,使用了不可見字元作為分割,所以擴充套件欄位key-value中不能包含那2個不可見字元。具體是哪2個,大家找找?

我們看到這個協議後,如何證明你的物理檔案就是按照這個協議寫的呢?

4用Golang解開RocketMQ Commit Log

RocketMQ是用java寫的,根據上文描述的儲存協議,我用Golang編寫了一個工具,可以解開Commit Log和Cosumer Queue,程式碼地址:

https://github.com/rmq-plus-plus/rocketmq-decoder

這個工具目前支援2個功能:

  • 指定Commit Log位點,直接解析Commit Log中的訊息,並且列印。
  • 指定消費位點,先解析Consumer Queue,得到Commit Log Offset後,再根據Commit Log Offset直接解析Commit Log,並且列印。

在Golang中沒有依賴RocketMQ的任何程式碼,純粹是依靠協議解碼。

圖片

golang-import

這裡貼了一段golang中解析Commit Log Offset的例子:在java中這個offset是一個long型別,佔用8個位元組。

在golang中,讀取8個位元組長度的資料,並且按照大端序解碼為int64,就可以得到正常的Commit Log Offset。

圖片

Golang-demo

我跑了一個demo結果,大家參考:

圖片

讀取consumer-queue-commit-log

5回答最初的問題

以下為個人見解,大家參考:

1.1 Consumer Queue Offset是連續的嗎, 為什麼?

是連續的。

consumer queue offset,是指每個queue中索引訊息的下標,下標當然是連續的。消費者也是利用了這個連續性,避免消費位點提交空洞的。

每個索引訊息佔用相同空間,都是20位元組,結構如下:

圖片

consumer-queue索引訊息結構

這裡物理位點也就是Commit Log Offset。

1.2 Commit Log Offset是連續的嗎, 為什麼?

不是連續的。

Commit Log Offset是指的每個訊息在全部Commit Log檔案中的位元組偏移量, 每個訊息的大小是不確定的,所以Commit Log Offset,也即是位元組偏移量肯定是不一樣的。

並且可以知道,每兩個偏移量的差的絕對值就是前一個訊息的訊息位元組數總長度。

並且上文中圖 “Commit Log儲存檔案分佈抽象”中的有誤解,每個小方格的大小其實是不一樣的。

1.3 Java寫的檔案,預設是大端序還是小端序,為什麼?

大端序。位元組序其實有資料儲存順序和網路傳輸順序兩種,java中預設用的大端序,保持和網路傳輸一樣,這樣方便編解碼。

每段網路傳輸層的資料包文最前面的位元組是表達後面的資料是用什麼協議傳輸的,這樣資料接收者在接受資料時, 按照位元組順序,先解析協議,再根據協議解碼後面的位元組序列,符合人類思考和解決問題的方式。

以上是我的理解,有任何問題,可以進社群群細聊。

討論說明:由於RocketMQ一些版本可能有差異,本文在4.9.3版本下討論。

相關文章