RocketMQ 多級儲存設計與實現
*作者:張森澤*
隨著 RocketMQ 5.1.0 的正式釋出,多級儲存作為 RocketMQ 一個新的獨立模組到達了 Technical Preview 里程碑:允許使用者將訊息從本地磁碟解除安裝到其他更便宜的儲存介質,可以用較低的成本延長訊息保留時間。本文詳細介紹 RocketMQ 多級儲存設計與實現。
## 設計總覽
RocketMQ 多級儲存旨在**不影響熱資料讀寫的前提下**將資料解除安裝到其他儲存介質中,適用於兩種場景:
1. 冷熱資料分離:RocketMQ 新近產生的訊息會快取在 page cache 中,我們稱之為**熱資料**;當快取超過了記憶體的容量就會有熱資料被換出成為**冷資料**。如果有少許消費者嘗試消費冷資料就會從硬碟中重新載入冷資料到 page cache,這會導致讀寫 IO 競爭並擠壓 page cache 的空間。而將冷資料的讀取鏈路切換為多級儲存就可以避免這個問題;
1. 延長訊息保留時間:將訊息解除安裝到更大更便宜的儲存介質中,可以用較低的成本實現更長的訊息儲存時間。同時多級儲存支援為 topic 指定不同的訊息保留時間,可以根據業務需要靈活配置訊息 TTL。
RocketMQ 多級儲存對比 Kafka 和 Pulsar 的實現最大的不同是我們使用準實時的方式上傳訊息,而不是等一個 CommitLog 寫滿後再上傳,主要基於以下幾點考慮:
1. 均攤成本:RocketMQ 多級儲存需要將全域性 CommitLog 轉換為 topic 維度並重新構建訊息索引,一次性處理整個 CommitLog 檔案會帶來效能毛刺;
1. 對小規格例項更友好:小規格例項往往配置較小的記憶體,這意味著熱資料會更快換出成為冷資料,等待 CommitLog 寫滿再上傳本身就有冷讀風險。採取準實時上傳的方式既能規避訊息上傳時的冷讀風險,又能儘快使得冷資料可以從多級儲存讀取。
## Quick Start
多級儲存在設計上希望降低使用者心智負擔:使用者無需變更客戶端就能實現無感切換冷熱資料讀寫鏈路,透過簡單的修改服務端配置即可具備多級儲存的能力,只需以下兩步:
1. 修改 Broker 配置,指定使用 org.apache.rocketmq.tieredstore.TieredMessageStore 作為 messageStorePlugIn
1. 配置你想使用的儲存介質,以解除安裝訊息到其他硬碟為例:配置 tieredBackendServiceProvider 為 org.apache.rocketmq.tieredstore.provider.posix.PosixFileSegment,同時指定新儲存的檔案路徑:tieredStoreFilepath
> *可選項:支援修改 tieredMetadataServiceProvider 切換後設資料儲存的實現,預設是基於 json 的檔案儲存*
更多使用說明和配置項可以在 GitHub 上檢視多級儲存的 **README **[** **1]****
## 技術架構
*![image.png](~tplv-k3u1fbpfcp-zoom-1.image "image.png")*
*architecture*
**接入層**:TieredMessageStore/TieredDispatcher/TieredMessageFetcher
接入層實現 MessageStore 中的部分讀寫介面,併為他們增加了非同步語意。TieredDispatcher 和 TieredMessageFetcher 分別實現了多級儲存的上傳/下載邏輯,相比於底層介面這裡做了較多的效能最佳化:包括使用獨立的執行緒池,避免慢 IO 阻塞訪問熱資料;使用預讀快取最佳化效能等。
**容器層**:TieredCommitLog/TieredConsumeQueue/TieredIndexFile/TieredFileQueue
容器層實現了和 DefaultMessageStore 類似的邏輯檔案抽象,同樣將檔案劃分為 CommitLog、ConsumeQueue、IndexFile,並且每種邏輯檔案型別都透過 FileQueue 持有底層物理檔案的引用。有所不同的是多級儲存的 CommitLog 改為 queue 維度。
**驅動層**:TieredFileSegment
驅動層負責維護邏輯檔案到物理檔案的對映,透過實現 TieredStoreProvider 對接底層檔案系統讀寫介面(Posix、S3、OSS、MinIO 等)。目前提供了 PosixFileSegment 的實現,可以將資料轉移到其他硬碟或透過 fuse 掛載的物件儲存上。
### 訊息上傳
RocketMQ 多級儲存的訊息上傳是由 dispatch 機制觸發的:初始化多級儲存時會將 TieredDispatcher 註冊為 CommitLog 的 dispacher。這樣每當有訊息傳送到 Broker 會呼叫 TieredDispatcher 進行訊息分發,TieredDispatcher 將該訊息寫入到 upload buffer 後立即返回成功。整個 dispatch 流程中不會有任何阻塞邏輯,確保不會影響本地 ConsumeQueue 的構建。
![image.png](~tplv-k3u1fbpfcp-zoom-1.image "image.png")
*TieredDispatcher*
TieredDispatcher 寫入 upload buffer 的內容僅為訊息的引用,不會將訊息的 body 讀入記憶體。因為多級儲存以 queue 維度構建 CommitLog,此時需要重新生成 commitLog offset 欄位。
![image.png](~tplv-k3u1fbpfcp-zoom-1.image "image.png")
*upload buffer*
觸發 upload buffer 上傳時讀取到每條訊息的 commitLog offset 欄位時採用拼接的方式將新的 offset 嵌入到原訊息中。
#### 上傳進度控制
每個佇列都會有兩個關鍵位點控制上傳進度:
1. dispatch offset:已經寫入快取但是未上傳的訊息位點
1. commit offset:已上傳的訊息位點
*![image.png](~tplv-k3u1fbpfcp-zoom-1.image "image.png")*
*upload progress*
類比消費者,dispatch offset 相當於拉取訊息的位點,commit offset 相當於確認消費的位點。commit offset 到 dispatch offset 之間的部分相當於已拉取未消費的訊息。
### 訊息讀取
TieredMessageStore 實現了 MessageStore 中的訊息讀取相關介面,透過請求中的邏輯位點(queue offset)判斷是否從多級儲存中讀取訊息,根據配置(tieredStorageLevel)有四種策略:
- DISABLE:禁止從多級儲存中讀取訊息;
- NOT_IN_DISK:不在 DefaultMessageStore 中的訊息從多級儲存中讀取;
- NOT_IN_MEM:不在 page cache 中的訊息即冷資料從多級儲存讀取;
- FORCE:強制所有訊息從多級儲存中讀取,目前僅供測試使用。
```
/**
* Asynchronous get message
* @see #getMessage(String, String, int, long, int, MessageFilter)
getMessage
*
* @param group Consumer group that launches this query.
* @param topic Topic to query.
* @param queueId Queue ID to query.
* @param offset Logical offset to start from.
* @param maxMsgNums Maximum count of messages to query.
* @param messageFilter Message filter used to screen desired
messages.
* @return Matched messages.
*/
CompletableFuture<GetMessageResult> getMessageAsync(final String group, final String topic, final int queueId,
final long offset, final int maxMsgNums, final MessageFilter
messageFilter);
```
需要從多級儲存中讀取的訊息會交由 TieredMessageFetcher 處理:首先校驗引數是否合法,然後按照邏輯位點(queue offset)發起拉取請求。TieredConsumeQueue/TieredCommitLog 將邏輯位點換算為對應檔案的物理位點從 TieredFileSegment 讀取訊息。
```
// TieredMessageFetcher#getMessageAsync similar with
TieredMessageStore#getMessageAsync
public CompletableFuture<GetMessageResult> getMessageAsync(String
group, String topic, int queueId,
long queueOffset, int maxMsgNums, final MessageFilter
messageFilter)
```
TieredFileSegment 維護每個儲存在檔案系統中的物理檔案位點,並透過為不同儲存介質實現的介面從中讀取所需的資料。
```
/**
* Get data from backend file system
*
* @param position the index from where the file will be read
* @param length the data size will be read
* @return data to be read
*/
CompletableFuture<ByteBuffer> read0(long position, int length);
```
#### 預讀快取
TieredMessageFetcher 讀取訊息時會預讀一部分訊息供下次使用,這些訊息暫存在預讀快取中。
```
protected final Cache<MessageCacheKey /* topic, queue id and queue
offset */,
SelectMappedBufferResultWrapper /* message data */> readAheadCache;
```
預讀快取的設計參考了 TCP Tahoe 擁塞控制演算法,每次預讀的訊息量類似擁塞視窗採用加法增、乘法減的機制控制:
- 加法增:從最小視窗開始,每次增加等同於客戶端 batchSize 的訊息量。
- 乘法減:當快取的訊息超過了快取過期時間仍未被全部拉取,在清理快取的同時會將下次預讀訊息量減半。
預讀快取支援在讀取訊息量較大時分片併發請求,以取得更大頻寬和更小的延遲。
某個 topic 訊息的預讀快取由消費這個 topic 的所有 group 共享,快取失效策略為:
1. 所有訂閱這個 topic 的 group 都訪問了快取
1. 到達快取過期時間
### 故障恢復
上文中我們介紹上傳進度由 commit offset 和 dispatch offset 控制。多級儲存會為每個 topic、queue、fileSegment 建立後設資料並持久化這兩種位點。當 Broker 重啟後會從後設資料中恢復,繼續從 commit offset 開始上傳訊息,之前快取的訊息會重新上傳並不會丟失。
![image.png](~tplv-k3u1fbpfcp-zoom-1.image "image.png")
## 開發計劃
面向雲原生的儲存系統要最大化利用雲上儲存的價值,而物件儲存正是雲端計算紅利的體現。RocketMQ 多級儲存希望一方面利用物件儲存低成本的優勢延長訊息儲存時間、擴充資料的價值;另一方面利用其共享儲存的特性在多副本架構中兼得成本和資料可靠性,以及未來向 Serverless 架構演進。
### tag 過濾
多級儲存拉取訊息時沒有計算訊息的 tag 是否匹配,tag 過濾交給客戶端處理。這樣會帶來額外的網路開銷,計劃後續在服務端增加 tag 過濾能力。
### 廣播消費以及多個消費進度不同的消費者
預讀快取失效需要所有訂閱這個 topic 的 group 都訪問了快取,這在多個 group 消費進度不一致的情況下很難觸發,導致無用的訊息在快取中堆積。
需要計算出每個 group 的消費 qps 來估算某個 group 能否在快取失效前用上快取的訊息。如果快取的訊息預期在失效前都不會被再次訪問,那麼它應該被立即過期。相應的對於廣播消費,訊息的過期策略應被最佳化為所有 Client 都讀取這條訊息後才失效。
### 和高可用架構的融合
目前主要面臨以下三個問題:
1. 後設資料同步:如何可靠的在多個節點間同步後設資料,slave 晉升時如何校準和補全缺失的後設資料;
1. 禁止上傳超過 confirm offset 的訊息:為了避免訊息回退,上傳的最大 offset 不能超過 confirm offset;
1. slave 晉升時快速啟動多級儲存:只有 master 節點具有寫許可權,在 slave 節點晉升後需要快速拉起多級儲存斷點續傳。
**相關連結:**
[1] README
*<https://github.com/apache/rocketmq/blob/develop/tieredstore/README.md>*
點選[此處]()檢視訊息佇列 RocketMQ 產品詳情
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69953029/viewspace-2946220/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- RocketMQ(十):資料儲存模型設計與實現MQ模型
- 【RocketMQ】RocketMQ儲存結構設計MQ
- 小紅書自研KV儲存架構如何實現萬億量級儲存與跨雲多活架構
- RocketMQ Compaction Topic的設計與實現MQ
- RocketMQ高效能之底層儲存設計MQ
- Java中的多級快取設計與實現Java快取
- 分散式塊儲存系統Ursa的設計與實現分散式
- 實現鍵值對儲存(四):API設計API
- PHP實現多儲存過程呼叫PHP儲存過程
- RocketMQ(六):nameserver與佇列儲存定位解析MQServer佇列
- 【RocketMQ】訊息的儲存MQ
- MySQL如何實現萬億級資料儲存?MySql
- RocketMQ Flink Catalog 設計與實踐MQ
- [MAUI程式設計]介面多型與實現UI程式設計多型
- 大模型儲存實踐:效能、成本與多雲大模型
- 億級流量實驗平臺設計與實現
- SpringCloud Alibaba實戰(3:儲存設計與基礎架構設計)SpringGCCloud架構
- 實現領域驅動設計 - 使用ABP框架 - 儲存庫框架
- 360自研分散式海量小檔案儲存系統的設計與實現分散式
- Spark+Hbase 億級流量分析實戰(日誌儲存設計)Spark
- RocketMQ -- 訊息傳送儲存流程MQ
- Redis 設計與實現 (五)--多機資料庫的實現Redis資料庫
- 多通道儲存出現鬼盤
- 資料儲存--面向列的儲存設計
- 圖的儲存與遍歷C++實現C++
- 如何實現檔案傳輸系統的多儲存
- 仿金蝶,物料庫存系統設計與實現思路
- Android,java,xml,xml讀取與儲存,基於AndroidXML解析與儲存的實現AndroidJavaXML
- 交易日均千萬訂單的儲存架構設計與實踐架構
- 如何實現百萬TPS?詳解JMQ4的儲存設計MQ
- 儲存系統設計指南之儲存分類
- 輕量級工作流引擎的設計與實現
- 《RocketMQ技術內幕:RocketMQ架構設計與實現原理》—第1章閱讀原始碼前的準備MQ架構原始碼
- 學習筆記:InnoDB儲存結構及多版本實現筆記
- 區塊鏈資訊儲存是如何實現安全儲存區塊鏈
- Redis設計與實現Redis
- 《redis設計與實現》Redis
- 小程式 LRU 儲存設計