RocketMQ Compaction Topic的設計與實現

ApacheRocketMQ發表於2023-01-04

本文作者:劉濤,阿里雲智慧技術專家。

 

01 Compaction Topic介紹

RocketMQ Compaction Topic的設計與實現

一般來說,訊息佇列提供的資料過期機制有如下幾種,比如有基於時間的過期機制——資料儲存多長時間後即進行清理,也有基於資料總量的過期機制——資料分割槽資料量達到一定值後進行清理。

而 Compaction Topic 是一種基於 key 的資料過期機制,即對於相同 key 的資料只保留最新值。

該特性的應用場景主要為維護狀態資訊,或者在需要用到 KV 結構時,可以透過 Compaction Topic 將 key-value 資訊直接儲存到 MQ,從而解除對外部資料庫的依賴。比如維護消費位點,可以將消費組加分割槽作為 key ,將消費位點做 offset ,以訊息形式傳送到 MQ ,壓縮之後,消費時獲取最新 offset 資訊即可。另外,像 connect 裡的 source 資訊比如 Binlog 解析位點或其他 source 處理的位點資訊均可存到 Compaction Topic。同時 Compaction Topic 也支援 儲存 RSQLDB 與 RStreams 的 checkpoint 資訊。

 

02 需要解決的問題

RocketMQ Compaction Topic的設計與實現

Compaction 過程中,需要解決如下幾個問題:

 

第一,資料寫入過程中,資料如何從生產者傳送到 broker 並且最終落盤,資料主備之間的 HA 如何保證?

第二,整個 compaction 的流程包括哪幾個步驟?如果資料量太大,如何最佳化?

第三,資料消費時如何索引訊息?如果找不到訊息指定的 offset 訊息,如何處理?

第四,如果有機器故障,如何恢復老資料?

 

03 方案設計與實現

RocketMQ Compaction Topic的設計與實現

第一,資料如何寫入。

首先寫入到 CommitLog,主要為複用 CommitLog 本身的 HA 能力。然後透過 reput執行緒將 CommitLog 訊息按照 Topic 加 partition 的維度拆分到不同檔案裡,按分割槽整理訊息,同時生成索引。這樣最終訊息就按 Topic 加 partition的粒度做了規整。

在 compaction 過程中,為什麼不在原先的 commitLog 上做規整,而是再額外按分割槽做規整?原因如下:

  1. 所有資料都會寫到 CommitLog ,因此單個 Topic 的資料不連續。如果要遍歷單個 topic 的所有資料,可能需要跳著讀,這樣就會導致大量冷讀,對磁碟 IO 影響比較大。
  2. CommitLog 資料有自動過期機制,會將老資料刪除,因此不能將資料直接寫到 CommitLog,而 CompactionLog 裡的老資料為按 key 過期,不一定會刪除。
  3. compact 以分割槽為維度進行。如果多個分割槽同時做 compact ,效率較低。因為很多分割槽的 key 同時在一個結構裡,會導致同一個分割槽能夠 compact 的資料比較少, 並且 compact 之後也需要重新寫一份麼,因此,索性就在 compact 之前將訊息透過 reput service 重新歸整一遍。
RocketMQ Compaction Topic的設計與實現

Compact 流程如下:

第一步,確定需要做 compaction 的資料檔案列表。一般大於兩個檔案,需要排除當前正在寫的檔案。

第二步,將上一步篩選出的檔案做遍歷,得到 key 到 offset 的對映關係。

第三步,根據對映關係將需要保留的資料重新寫到新檔案。

第四步,用新檔案替換老檔案,將老檔案刪除。

RocketMQ Compaction Topic的設計與實現

第二步的構建 OffsetMap 主要目的在於可以知道哪檔案需要被保留、哪檔案需要被刪除,以及檔案的前後關係,這樣就可以確定寫入的佈局,確定佈局之後,就可以按照append 的方式將需要保留的資料寫到新檔案。

此處記錄的並非 key 到 value 的資訊,而是 key 到 Offset 的資訊。因為 value 的資料 body 可能較長,比較佔空間,而 offset 是固定長度,且透過 offset 資訊也可以明確訊息的先後順序。另外,key 的長度也不固定,直接在 map 儲存原始 key 並不合適。因此我們將 MD5 作為新 key ,如果 MD5 相同 key 認為也相同。

RocketMQ Compaction Topic的設計與實現

做 compaction 時會遍歷所有訊息,將相同 key 且 offset 小於 OffsetMap 的值刪除。最終透過原始資料與 map 結構得到壓縮之後的資料檔案。

RocketMQ Compaction Topic的設計與實現

上圖為目錄結構展示。寫入時上面為資料檔案,下面為索引,要 compact 的是標紅兩個檔案。壓縮後的檔案儲存於子目錄,需要將老檔案先標記為刪除,將子目錄檔案與 CQ 同時移到老的根目錄。注意,檔案與 CQ 檔名一一對應,可以一起刪除。

RocketMQ Compaction Topic的設計與實現

隨著資料量越來越大,構建的 OffsetMap 也會越來越大,導致無法容納。

因此不能使用全量構建方式,不能將所有要 compact 的檔案的 OffsetMap 一次性構建,需要將全量構建改為增量構建,構建邏輯也會有小的變化。

RocketMQ Compaction Topic的設計與實現

第一輪構建:如上圖,先構建上面部分的 OffsetMap ,然後遍歷檔案,如果 offset 小於 OffsetMap 中對應 key 的 offset 則刪除,如果等於則保留。而下面部分的訊息的offset 肯定大於 OffsetMap 內的 offset ,因此也需要保留。

RocketMQ Compaction Topic的設計與實現

第二輪構建:從上一次結束的點開始構建。如果上一輪中的某個 key 在新一輪中不存在,則保留上一輪的值;如果存在,則依然按照小於刪除、大於保留的原則進行構建。

將一輪構建變為兩輪構建後, OffsetMap 的大小顯著降低,構建的資料量也顯著降低。

RocketMQ Compaction Topic的設計與實現

原先的索引為 CommitLog Position、Message Size 和 Tag Hush,而現在我們複用了bcq 結構。由於 Compact 之後資料不連續,無法按照先前的方式直接查詢資料所在物理位置。由於 queueOffset 依然為單調增排列,因此可以透過二分查詢方式將索引找出。

二分查詢需要 queueoffset 資訊,索引結構也會發生變化,而 bcq 帶有 queueoffse 資訊,因此可以複用 bcq 的結構。

RocketMQ Compaction Topic的設計與實現

Queueoffset 在 compact 前後保持不變。如果 queueoffset 不存在,則獲取第一個大於 queueoffset 的訊息,然後從頭開始將所有全量資料傳送給客戶端。

機器故障導致訊息丟失時,需要做備機的重建。因為 CommitLog 只能恢復最新資料,而 CompactionLog 需要老資料。之前的 HA 方式下,資料檔案可能在 compact 過程中被被刪除,因此也不能基於複製檔案的方式做主備間同步。

RocketMQ Compaction Topic的設計與實現

因此,我們實現了基於 message 的複製。即模擬消費請求從 master 上拉取訊息。拉取位點一般從 0 開始,大於等於 commitLog 最小offset 時結束。拉取結束之後,再做一次 force compaction 將 CommitLog 資料與恢復時的資料做一次 compaction ,以保證保留的資料是被壓縮之後的資料。後續流程不變。

 

04 使用說明

RocketMQ Compaction Topic的設計與實現

生產者側使用現有生產者介面,因為要按分割槽做 compact ,因此需要將相同 key 路由到相同的 MessageQueue,需要自己實現相關演算法。

RocketMQ Compaction Topic的設計與實現

消費者側使用現有消費者介面,消費到訊息後,存入本地類 Map 結構中再進行使用。我們的場景大多為從頭開始拉資料,因此需要在開始時將消費位點重置到0。拉取完以後,將訊息 key 與 value 傳入本地 kv 結構,使用時直接從該結構拿取即可。

 

相關文章