引言
消息隊列的存儲架構(gòu)是決定其可靠性、吞吐量、延遲性能的核心因素,直接影響業(yè)務場景適配能力。本文聚焦三款主流消息隊列 ——Kafka(LinkedIn 開源,側(cè)重高吞吐)、RocketMQ(阿里開源,金融級特性突出)、JMQ(京東開源,側(cè)重高可用與靈活性),從存儲模型、數(shù)據(jù)組織、索引設計等維度展開深度對比,為技術(shù)選型與架構(gòu)優(yōu)化提供參考。?
本文將從概念辨析出發(fā),系統(tǒng)拆解主流存儲模型與存儲引擎的設計邏輯,對比 JMQ、Kafka、RocketMQ的技術(shù)選型差異與架構(gòu)設計。?
一、Kafka存儲架構(gòu)
1.1 核心存儲模型:分區(qū)日志流

??
Topic - 主題
Kafka學習了數(shù)據(jù)庫里面的設計,在里面設計了topic(主題),這個東西類似于關系型數(shù)據(jù)庫的表,此時我需要獲取中國移動的數(shù)據(jù),那就直接監(jiān)聽中國移動訂閱的Topic即可。
Partition - 分區(qū)
Kafka還有一個概念叫Partition(分區(qū)),分區(qū)具體在服務器上面表現(xiàn)起初就是一個目錄,一個主題下面有多個分區(qū),這些分區(qū)會存儲到不同的服務器上面,或者說,其實就是在不同的主機上建了不同的目錄。這些分區(qū)主要的信息就存在了.log文件里面。跟數(shù)據(jù)庫里面的分區(qū)差不多,是為了提高性能。
至于為什么提高了性能,很簡單,多個分區(qū)多個線程,多個線程并行處理肯定會比單線程好得多。
Topic和partition像是HBASE里的table和region的概念,table只是一個邏輯上的概念,真正存儲數(shù)據(jù)的是region,這些region會分布式地存儲在各個服務器上面,對應于kafka,也是一樣,Topic也是邏輯概念,而partition就是分布式存儲單元。這個設計是保證了海量數(shù)據(jù)處理的基礎。我們可以對比一下,如果HDFS沒有block的設計,一個100T的文件也只能單獨放在一個服務器上面,那就直接占滿整個服務器了,引入block后,大文件可以分散存儲在不同的服務器上。
注意:
1.分區(qū)會有單點故障問題,所以我們會為每個分區(qū)設置副本數(shù)
2.分區(qū)的編號是從0開始的

??
Kafka 以「主題(Topic)- 分區(qū)(Partition)」為核心組織數(shù)據(jù),每個分區(qū)本質(zhì)是一個 append-only 的日志流,消息按生產(chǎn)順序追加存儲,保證分區(qū)內(nèi)消息有序性。?
優(yōu)點:可以充分利用磁盤順序讀寫高性能的特性。存儲介質(zhì)也可以選擇廉價的SATA磁盤,這樣可以獲得更長的數(shù)據(jù)保留時間、更低的數(shù)據(jù)存儲成本。
1.2 數(shù)據(jù)組織:分段日志文件
?每個分區(qū)拆分為多個 Segment 文件(默認 1GB),命名格式為「起始偏移量.log」(如 00000000000000000000.log)?,做這個限制目的是為了方便把.log加載到內(nèi)存去操作
?配套兩類索引文件:.index(偏移量→物理地址映射)、.timeindex(時間戳→偏移量映射)??

??
這個9936472之類的數(shù)字,就是代表了這個日志段文件里包含的起始offset,也就說明這個分區(qū)里至少都寫入了接近1000萬條數(shù)據(jù)了。
Kafka broker有一個參數(shù),log.segment.bytes,限定了每個日志段文件的大小,最大就是1GB,一個日志段文件滿了,就自動開一個新的日志段文件來寫入,避免單個文件過大,影響文件的讀寫性能,這個過程叫做log rolling,正在被寫入的那個日志段文件,叫做active log segment。
1.3 消息讀/寫過程

??
寫消息:
?Index文件寫入,Index文件較小,可以直接用mmap進行內(nèi)存映射,避免頻繁的磁盤I/O操作,提高寫入性能;由于Index文件是稀疏索引,只需要記錄關鍵位置的偏移量,因此即使使用mmap,寫入的開銷也相對較低。
?Segment文件寫入,Segment文件較大,可以采用普通的寫操作(FileChannel.write),由于Segment文件是順序?qū)懭氲?,并且Kafka會利用操作系統(tǒng)的PageCache(頁緩存)機制,寫入操作會先寫入到內(nèi)存中,然后由操作系統(tǒng)在后臺異步刷新到磁盤,可以進一步提高寫入的性能。
讀消息:
?Index文件讀取,通常使用mmap方式讀取,由于Index文件較小,且是稀疏索引,缺頁中斷的可能性較小。
?Segment文件讀取,通常使用sendfile系統(tǒng)調(diào)用來實現(xiàn)零拷貝讀取和發(fā)送,減少數(shù)據(jù)在用戶空間與內(nèi)核空間之間的拷貝次數(shù),提高數(shù)據(jù)傳輸?shù)男省?/p>
1.4 關鍵技術(shù)
Kafka 作為高性能的消息中間件,其超高吞吐量的核心秘訣之一就是深度依賴 PageCache + 順序 I/O + mmap 內(nèi)存映射的組合。
PageCache,中文名稱為頁高速緩沖存儲器。它是將磁盤上的數(shù)據(jù)加載到內(nèi)存中,當系統(tǒng)需要訪問這些數(shù)據(jù)時,可以直接從內(nèi)存中讀取,而不必每次都去讀取磁盤。這種方式顯著減少了磁盤I/O操作,從而提高了系統(tǒng)性能。
mmap(Memory-mapped file)是操作系統(tǒng)提供的一種將磁盤文件與進程虛擬地址空間建立映射關系的核心技術(shù),本質(zhì)是讓進程通過直接操作內(nèi)存地址的方式讀寫文件,無需傳統(tǒng)的 read/write 系統(tǒng)調(diào)用。核心價值在于零拷貝和內(nèi)存式文件訪問,尤其適合大文件、高吞吐、隨機訪問的場景。
將日志段(.log)文件映射到內(nèi)存,生產(chǎn)者寫入時直接寫內(nèi)存(內(nèi)核異步刷盤),消費者讀取時直接從內(nèi)存讀取,實現(xiàn)超高吞吐(Kafka 的 “順序?qū)?+ mmap” 是其高性能核心);

??
零拷貝流程示意圖
零拷貝過程:
1.用戶進程發(fā)起sendfile系統(tǒng)調(diào)用,上下文(切換1)從用戶態(tài)轉(zhuǎn)向內(nèi)核態(tài)
2.DMA控制器,把數(shù)據(jù)從硬盤中拷貝到內(nèi)核緩沖區(qū)。
3.CPU將讀緩沖區(qū)中數(shù)據(jù)拷貝到socket緩沖區(qū)
4.DMA控制器,異步把數(shù)據(jù)從socket緩沖區(qū)拷貝到網(wǎng)卡,
5.上下文(切換2)從內(nèi)核態(tài)切換回用戶態(tài),sendfile調(diào)用返回。
1.5 設計優(yōu)勢
?順序?qū)懘疟P:Segment 文件僅追加寫入,規(guī)避隨機 IO,吞吐量極高(單分區(qū)可達 10 萬 + TPS)??
?索引輕量化:僅維護偏移量與時間戳索引,降低存儲開銷?
?副本同步:基于 ISR 機制,僅同步已提交消息,兼顧一致性與可用性
二、RocketMQ存儲架構(gòu)
Kafka的每個Partition都是一個完整的、順序?qū)懭氲奈募擯artition數(shù)量增多時,從操作系統(tǒng)的角度看,這些寫入操作會變得相對隨機,這可能會影響寫入性能。
2.1 核心存儲模型:分離式設計
RocketMQ采用「CommitLog + ConsumeQueue + IndexFile」三層結(jié)構(gòu),徹底分離數(shù)據(jù)存儲與索引查詢:?
?CommitLog:全局單一日志文件(默認 1GB / 個,循環(huán)覆蓋),存儲所有主題的原始消息??
?ConsumeQueue:按主題 - 隊列維度拆分的索引文件,存儲「消息物理地址 + 偏移量 + 長度」,供消費者快速查詢?
?IndexFile:哈希索引文件,支持按消息 Key 查詢
CommitLog:消息的原始日記本
CommitLog是RocketMQ存儲消息的物理文件,所有消息都會按到達順序?qū)懭脒@個文件。你可以把它想象成一本不斷追加的日記本——每條消息都是按時間順序記錄的新日記。
// 消息存儲的核心邏輯簡化示例(非源碼)
publicvoidputMessage(Message message){
// 1. 將消息序列化為字節(jié)數(shù)組
byte[] data = serialize(message);
// 2. 計算消息物理偏移量
longoffset = commitLog.getMaxOffset();
// 3. 將數(shù)據(jù)追加到CommitLog文件末尾
commitLog.append(data);
// 4. 返回消息的全局唯一物理偏移量
returnoffset;
}
消息寫入CommitLog時有三個關鍵特性:
1.順序?qū)懭?/strong>:所有消息按到達順序追加到文件末尾,避免磁盤隨機尋址
2.內(nèi)存映射:通過MappedByteBuffer實現(xiàn)文件映射,減少數(shù)據(jù)拷貝次數(shù)
3.文件分割:單個CommitLog文件默認1GB,寫滿后創(chuàng)建新文件(文件名用起始偏移量命名)
舉個例子,當生產(chǎn)者發(fā)送三條消息時,CommitLog文件可能長這樣:
0000000000000000000(文件1,1GB) 2|--消息A(offset=0) 3|--消息B(offset=100) 4|--消息C(offset=200) 500000000001073741824(文件2,起始偏移量1073741824)
溫馨提示:雖然CommitLog是順序?qū)?,但讀取時需要配合索引結(jié)構(gòu),否則遍歷文件找消息就像大海撈針。
消費隊列ConsumeQueue:消息的快速目錄
如果每次消費都要掃描CommitLog,性能會慘不忍睹。于是RocketMQ設計了ConsumeQueue——它是基于Topic和Queue的二級索引文件。
每個ConsumeQueue條目包含三個關鍵信息(固定20字節(jié)):
1| CommitLog Offset (8字節(jié)) |Message Size (4字節(jié))| Tag Hashcode (8字節(jié)) |
這相當于給CommitLog里的消息做了一個目錄:
TopicA-Queue0的ConsumeQueue 2|--0(對應CommitLog偏移0的消息A) 3|--100(對應CommitLog偏移100的消息B) 4|--200(對應CommitLog偏移200的消息C)
當消費者拉取TopicA-Queue0的消息時:
1.先查ConsumeQueue獲取消息的物理位置
2.根據(jù)CommitLog Offset直接定位到CommitLog文件
3.讀取指定位置的消息內(nèi)容
關鍵設計點:
?ConsumeQueue采用內(nèi)存映射+異步刷盤,保證高性能
?單個文件存儲30萬條索引,約5.72MB(30萬*20字節(jié))
?通過hashCode快速過濾Tag,實現(xiàn)消息過濾
索引文件IndexFile:消息的全局字典
如果需要根據(jù)MessageID或Key查詢消息,ConsumeQueue就不夠用了。這時候就要用到IndexFile這個全局索引。
IndexFile的結(jié)構(gòu)類似HashMap:
1.Slot槽位(500萬個):存儲相同hash值的Index條目鏈表頭
2.Index條目(2000萬條):包含Key的hash值、CommitLog偏移量、時間差等信息
當寫入消息時:
// 索引構(gòu)建過程簡化示意
publicvoidbuildIndex(Message message){
// 計算Key的hash值
inthash = hash(message.getKey());
// 定位到對應的Slot槽位
intslotPos = hash % slotNum;
// 在Index區(qū)域追加新條目
indexFile.addEntry(hash, message.getCommitLogOffset());
}
查詢時通過兩次查找快速定位:
1.根據(jù)Key的hash值找到Slot槽位
2.遍歷Slot對應的鏈表,比對CommitLog中的實際Key值
性能優(yōu)化必知:
?消息體積差異大時,CommitLog仍然保持順序?qū)?,但ConsumeQueue可能出現(xiàn)「稀疏索引」(相鄰索引指向的物理位置間隔大)
?生產(chǎn)環(huán)境中CommitLog建議放在單獨SSD磁盤,ConsumeQueue和IndexFile可放普通磁盤
?遇到消息堆積時,優(yōu)先檢查消費者速度,而不是無腦擴容Broker存儲
理解這些底層機制,下次遇到消息查詢性能問題或者磁盤IO瓶頸時,就知道該從CommitLog的寫入模式還是ConsumeQueue的索引結(jié)構(gòu)入手排查了。
2.2 數(shù)據(jù)流轉(zhuǎn)機制
?生產(chǎn)者寫入 CommitLog,生成全局唯一偏移量(PHYOFFSET)?
?后臺線程異步構(gòu)建 ConsumeQueue 索引,同步消息元數(shù)據(jù)?
?消費者通過 ConsumeQueue 定位 CommitLog 中的消息,避免全量掃描
存儲過程全景圖
現(xiàn)在把各個模塊串起來看消息的生命周期:
1.生產(chǎn)者發(fā)送消息到Broker
2.Broker將消息順序?qū)懭隒ommitLog
3.異步線程同時構(gòu)建ConsumeQueue和IndexFile
4.消費者通過ConsumeQueue快速定位消息
5.按需查詢IndexFile實現(xiàn)消息回溯
整個過程就像圖書館的管理系統(tǒng):
?CommitLog是藏書庫(按入庫時間擺放)
?ConsumeQueue是分類目錄(按題材/出版社分類)
?IndexFile是檢索電腦(支持按書名/作者查詢)
2.4 設計優(yōu)勢
?讀寫分離:CommitLog 僅負責寫入,ConsumeQueue 負責查詢,提升并發(fā)性能?
?事務支持:通過 CommitLog 中的事務狀態(tài)標記 + 回查機制,實現(xiàn)分布式事務消息?
?刷盤策略:支持「異步刷盤(高吞吐)」「同步刷盤(金融級可靠性)」動態(tài)切換
三、JMQ存儲架構(gòu)
JMQ的消息存儲分別參考了Kafka和RocketMQ存儲設計上優(yōu)點,并根據(jù)京東內(nèi)部的應用場景進行了改進和創(chuàng)新。
3.1 核心存儲模型:分區(qū)日志 + 隊列兼容

??
JMQ存儲的基本單元是PartitionGroup。在同一個Broker上,每個PartitionGroup對應一組消息文件(Journal Files),順序存放這個Topic的消息。
與Kafka類似,每個Topic包含若干Partition,每個Partition對應一組索引文件(Index Files),索引中存放消息在消息文件中的位置和消息長度。消息寫入時,收到的消息按照對應的PartitionGroup寫入依次追加寫入消息文件中,然后異步創(chuàng)建索引并寫入對應Partition的索引文件中。
以PartionGroup為基本存儲單元的設計,在兼顧靈活性的同時,具有較好的性能,并且單個PartitionGroup可以支持更多的并發(fā)。
3.2 消息讀/寫過程

??
寫消息:
JMQ的寫操作使用DirectBuffer作為緩存,數(shù)據(jù)先寫入DirectBuffer,再異步通過FileChannel寫入到文件中。
?消息寫入DirectBuffer后,默認寫入該節(jié)點成功(數(shù)據(jù)的高可靠是通過Raft協(xié)議復制,用多個內(nèi)存副本來保證),相對Kafka的寫操作來看,JMQ響應寫入請求的處理過程沒有發(fā)生系統(tǒng)調(diào)用,在京東內(nèi)部的大量單條同步發(fā)送的場景下開銷更低、性能更優(yōu)。
?同時也避免使用MappedByteBuffer(Mmap方式)產(chǎn)生Page Fault中斷,OS在中斷中將該頁對應磁盤中的數(shù)據(jù)拷貝到內(nèi)存中,在對文件進行追加寫入的情況下,這一無法避免的過程是完全沒有必要,反而增加了寫入的耗時的問題。
讀消息:
JMQ采用定長稠密索引設計,每個索引固定長度。
?定長設計的好處是,直接根據(jù)索引序號就可以計算出索引在文件中的位置:索引位置 = 索引序號 * 索引長度。這樣,消息的查找過程就比較簡單了,首先計算出索引所在的位置,直接讀取索引,然后根據(jù)索引中記錄的消息位置讀取消息。
?在京東內(nèi)部應用場景中,單條消息處理耗時高是比較常見的,微服務架構(gòu)下用戶一般會申請更多的消費節(jié)點,讓每個消費節(jié)點單次拉取較小批量的消息進行處理,以提升消費并行度,這樣消費拉取請求的次數(shù)會比較多,稠密索引的設計會更適用內(nèi)部的應用場景。
JMQ消費讀操作99%以上都能命中緩存(JMQ設計的堆外內(nèi)存與文件映射的一種緩存機制),避免了Kafka可能遇到的Cache被污染,影響性能和吞吐的問題。同時直接讀內(nèi)存也規(guī)避了RocketMQ在讀取消息存儲的日志數(shù)據(jù)文件時容易產(chǎn)生較多的隨機訪問讀取磁盤,影響性能的問題。(當沒有命中緩存時,會默認降級為通過Mmap的方式讀取消息)。
四、競品對比分析
|
? |
JMQ | Kafka |
| 存儲模型 | 以PartitionGroup為基本存儲單元,支持高并發(fā)寫入 | 以Partition為基本存儲單元,支持靈活的數(shù)據(jù)復制和遷移 |
| 消息寫入性能 | - 單副本異步寫入性能與 Kafka 相當 - 三副本異步寫入性能優(yōu)于 Kafka | - 單副本異步寫入性能與 JMQ 相當 - 三副本異步寫入性能略低于 JMQ |
| 同步寫入性能 | - 同步寫入性能穩(wěn)定,幾乎不受網(wǎng)絡延遲影響 | - 同步寫入性能受網(wǎng)絡延遲影響較大,穩(wěn)定性略遜于 JMQ |
| 多分區(qū)性能 | - 多分區(qū)異步寫入性能與 Kafka 相當 - 同步寫入性能略低于 Kafka | - 多分區(qū)同步寫入性能更穩(wěn)定,適合高并發(fā)場景 |
| 副本機制 | 支持異步復制,副本間數(shù)據(jù)同步性能較好 | 支持異步和同步復制,副本機制成熟,適合復雜部署 |
| 跨機房部署 | - 同步寫入性能基本不受影響 - 異步寫入性能下降 | - 同步寫入性能受網(wǎng)絡延遲影響較大 - 異步寫入性能下降 |
| 適用場景 | - 對同步寫入性能要求高 - 副本異步吞吐要求高 - 大規(guī)模微服務集群 | - 復雜分區(qū)的高并發(fā)同步寫入 - 大規(guī)模分布式系統(tǒng) - 多語言生態(tài)支持豐富 |
在單副本場景下,JMQ與Kafka的單機寫入性能均十分出色,均可達到網(wǎng)絡帶寬上限。
然而,在更貼近生產(chǎn)環(huán)境的三副本場景中,兩者特性出現(xiàn)分化:
JMQ在三副本異步寫入下的極限吞吐優(yōu)勢明顯,且在跨機房部署時,其同步寫入性能表現(xiàn)良好,幾乎不受網(wǎng)絡延遲影響;而Kafka則在多分區(qū)同步寫入場景下展現(xiàn)出更穩(wěn)定的性能,衰減小于JMQ。在大部分異步吞吐場景及不同消息體下的性能趨勢上,兩者表現(xiàn)相當。
綜上所述,JMQ尤其適合對同步寫入性能和副本異步吞吐有極高要求的場景,而Kafka在復雜分區(qū)的高并發(fā)同步寫入方面適應性更廣。
審核編輯 黃宇
-
存儲
+關注
關注
13文章
4777瀏覽量
90014 -
kafka
+關注
關注
0文章
55瀏覽量
5566
發(fā)布評論請先 登錄
什么是BSP工程師
想成為硬件工程師?我教你?。∧愕孟葘W會這些...... #硬件工程師 #電子工程師 #電子愛好者 #電子行業(yè)
嵌入式工程師的進階之路
Kafka生產(chǎn)環(huán)境應用方案
電子工程師自學成才手冊.提高篇
硬件工程師看了只會找個角落默默哭泣#硬件工程師 #MDD #MDD辰達半導體 #產(chǎn)品經(jīng)理 #軟件工程師
新編電氣工程師手冊
電子工程師自學速成 —— 提高篇
電子工程師自學速成——入門篇
硬件工程師手冊(全套)
工程師之夜系列分享第三十九篇:Kafka、RocketMQ、JMQ 存儲架構(gòu)深度對比
評論