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

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

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

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

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

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

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

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