7张图揭晓RocketMQ存储设计的精髓
RocketMQ 作為一款基于磁盤存儲的中間件,具有無限積壓能力,并提供高吞吐、低延遲的服務能力,其最核心的部分必然是它優雅的存儲設計。
存儲概述
RocketMQ 存儲的文件主要包括 Commitlog 文件、ConsumeQueue 文件、Index 文件。
RocketMQ 將所有主題的消息存儲在同一個文件中,確保消息發送時按順序寫文件,盡最大能力確保消息發送的高可用性與高吞吐量。
但消息中間件一般都是基于主題的訂閱與發布模式,消息消費時必須按照主題進行帥選消息,顯然從 Commitlog 文件中按照 topic 去篩選消息會變得及其低效,為了提高根據主題檢索消息的效率,RocketMQ 引入了 ConsumeQueue 文件,俗成消費隊列文件。
關系型數據庫可以按照字段屬性進行記錄檢索,作為一款主要面向業務開發的消息中間件,RocketMQ 也提供了基于消息屬性的檢索能力,底層的核心設計理念是為 Commitlog 文件建立哈希索引,并存儲在 Index 文件中。
在 RocketMQ 中順序寫入到 Commitlog 文件后,ConsumeQueue 與 Index 文件都是異步構建的,其數據流向圖如下:
存儲文件組織方式
RocketMQ 在消息寫入過程中追求極致的磁盤順序寫。所有主題的消息全部寫入一個文件,即 Commitlog 文件。所有消息按抵達順序依次追加到文件中,消息一旦寫入,不支持修改。Commitlog 文件的具體布局如下圖所示:
基于文件編程與基于內存編程有一個很大的不同是在基于內存的編程模式中我們有現成的數據結構,例如 List、HashMap,對數據的讀寫非常方便,那么一條一條消息存入文件 Commitlog 后,該如何查找呢?
正如關系型數據會為每一條數據引入一個?ID?字段,在基于文件編程的模型中,也會為一條消息引入一個身份標志:消息物理偏移量,即消息存儲在文件的起始位置。
正是有了物理偏移量的概念,Commitlog 的文件名命名也是極具技巧性,使用了存儲在該文件的第一條消息在整個 Commitlog 文件組中的偏移量來命名,例如第一個? Commitlog 文件為?
0000000000000000000,第二個文件為
00000000001073741824,然后依次類推。
這樣做的好處是給出任意一個消息的物理偏移量,例如消息偏移量為 73741824,可以通過二分法進行查找,快速定位這個文件在第一個文件中,然后用消息的物理偏移量減去該文件的名稱所得到的差值,就是在該文件中的絕對地址。
Commitlog 文件的設計理念是追求極致的消息寫,但我們知道消息消費模型是基于主題的訂閱機制,即一個消費組是消費特定主題的消息。如果根據主題從 commitlog 文件中檢索消息,我們會發現這絕不是一個好主意,只能從文件的第一條消息逐條檢索,其性能可想而知,故為了解決基于 topic 的消息檢索問題,RocketMQ 引入了 consumequeue 文件,consumequeue 的結構如下圖所示。
ConsumeQueue 文件是消息消費隊列文件,是 Commitlog 文件基于 Topic 的索引文件,主要用于消費者根據 Topic 消費消息,其組織方式為/topic/queue,同一個隊列中存在多個文件。
Consumequeue 的設計極具技巧,每個條目長度固定(8 字節 commitlog 物理偏移量、4 字節消息長度、8 字節 tag hashcode)。
這里不是存儲 tag 的原始字符串,而選擇存儲 hashcode,目的就是確保每個條目的長度固定,可以使用訪問類似數組下標的方式快速定位條目,極大地提高了 ConsumeQueue 文件的讀取性能。
試想一下,消息消費者根據 topic、消息消費進度(consumeuqe 邏輯偏移量),即第幾個 Consumeque 條目,這樣的消費進度去訪問消息的方法為使用邏輯偏移量 logicOffset * 20 即可找到該條目的起始偏移量(consumequeue 文件中的偏移量),然后讀取該偏移量后 20 個字節即得到一個條目,無須遍歷 consumequeue 文件。
RocketMQ 與 Kafka 相比具有一個強大的優勢,就是支持按消息屬性檢索消息,引入 consumequeue 文件解決了基于 topic 查找的問題,但如果想基于消息的某一個屬性查找消息,consumequeue 文件就無能為力了。
RocketMQ 引入了 Index 索引文件,實現基于文件的哈希索引。IndexFile 的文件存儲結構如下圖所示:
IndexFile 文件基于物理磁盤文件實現 Hash 索引。其文件由 40 字節的文件頭、500萬 個 Hash 槽,每個 Hash 槽 4 個字節,最后由 2000萬 個 Index 條目,每個條目由 20個 字節構成,分別為 4 字節索引 key 的 hashcode、8 字節消息物理偏移量、4 字節時間戳、4 字節的前一個 Index 條目(Hash 沖突的鏈表結構)。
即建立了索引 Key 的 hashcode 與物理偏移量的映射關系,根據 key 先快速定義到 commitlog 文件。
順序寫
基于磁盤的讀寫,提高其寫入性能的另外一個設計原理是磁盤順序寫。
磁盤順序寫廣泛用在基于文件的存儲模型中,大家不妨思考一下 MySQL Redo 日志的引入目的,我們知道在 MySQL InnoDB 的存儲引擎中,會有一個內存 Pool,用來緩存磁盤的文件塊,當更新語句將數據修改后,會首先在內存中進行修改,然后將變更寫入到 redo 文件(刷寫到磁盤),然后定時將 InnoDB 內存池中的數據刷寫到磁盤。
為什么不一有數據變更,就直接更新到指定的數據文件中呢?以 MySQL InnoDB 中一個庫存在上千張,每一個張的數據會使用單獨的文件存儲,如果每一個表的數據發生變更,就刷寫到磁盤,就會存在大量的隨機寫入,性能無法得到提升,故引入一個 redo 文件,順序寫 redo 文件,從表面上多了一步刷盤操作,但由于是順序寫,相比隨機寫,帶來的性能提升是非常顯著的。
內存映射機制
雖然基于磁盤的順序寫可以極大提高 IO 的寫效率,但如果基于文件的存儲采用常規的 JAVA 文件操作 API,例如 FileOutputStream 等,其性能提升會很有限,RocketMQ 引入了內存映射,將磁盤文件映射到內存中,以操作內存的方式操作磁盤,性能又提升了一個檔次。
在 JAVA 中可通過 FileChannel 的 map 方法創建內存映射文件。
在 Linux 服務器中由該方法創建的文件使用的就是操作系統的 pagecache,即頁緩存。
Linux 操作系統中的內存使用策略時會盡可能地利用機器的物理內存,并常駐內存中,就是所謂的頁緩存。在操作系統的內存不夠的情況下,采用緩存置換算法,例如 LRU 將不常用的頁緩存回收,即操作系統會自動管理這部分內存。
如果 RocketMQ Broker 進程異常退出,存儲在頁緩存中的數據并不會丟失,操作系統會定時將頁緩存中的數據持久化到磁盤,做到數據安全可靠。不過如果是機器斷電等異常情況,存儲在頁緩存中的數據就有可能丟失。
靈活多變的刷盤策略
有了順序寫和內存映射的加持,RocketMQ 的寫入性能得到了極大的保證,但凡事都有利弊,引入了內存映射和頁緩存機制,消息會先寫入到頁緩存,此時消息并沒有真正持久化到磁盤。那么 broker 收到客戶端的消息發送后,是存儲到頁緩存中就直接返回成功,還是要持久化到磁盤中才返回成功呢?
這是一個“艱難”的抉擇,是在性能與消息可靠性方面進行權衡。為此,RocketMQ 提供了多種策略:同步刷盤、異步刷盤。
1、同步刷盤
同步刷盤在 RocketMQ 的實現中成為組提交,并不是每一條消息都必須刷盤。其設計理念如圖所示:
采用同步刷盤,每一個線程將數據追到到內存后,并向刷盤線程提交刷盤請求,然后會阻塞;刷盤線程從任務隊列中獲取一個任務,然后觸發一次刷盤,但并不只刷與請求相關的消息,而是會直接將內存中待刷盤的所有消息一次批量刷盤,然后就可以喚醒一組請求線程,實現組刷盤。
2、異步刷盤
同步刷盤的優點是能保證消息不丟失,即向客戶端返回成功就代表這條消息已被持久化到磁盤,即消息非??煽?#xff0c;但這是以犧牲寫入響應延遲性能為代價的,由于 RocketMQ 的消息是先寫入 pagecache,故消息丟失的可能性較小,如果能容忍一定幾率的消息丟失,可以考慮使用異步刷盤。
異步刷盤指的是 broker 將消息存儲到 pagecache 后就立即返回成功,然后開啟一個異步線程定時執行 FileChannel 的 forece 方法,將內存中的數據定時刷寫到磁盤,默認間隔為 500ms。
內存級讀寫分離
RocketMQ 為了降低 pagecache 的使用壓力引入了 transientStorePoolEnable 機制,即內存級別的讀寫分離機制。
默認情況下 RocketMQ 將消息寫入 pagecache,消息消費時從 pagecache 中讀取,這樣在高并發時 pagecache 的壓力會比較大,容易出現瞬時 broker busy,故 RocketMQ 還引入了 transientStorePoolEnable,將消息先寫入堆外內存并立即返回,然后異步將堆外內存中的數據提交到 pagecache,再異步刷盤到磁盤中。其工作機制如下圖所示:
消息在消費讀取時不會嘗試從堆外內存中讀,而是從 pagecache 中讀取,這樣就形成了內存級別的讀寫分離,即消息寫入時主要面對堆外內存,而讀消息時主要面對 pagecache。
該方案的優點是消息是直接寫入堆外內存,然后異步寫入 pagecache。相比每條消息追加直接寫入 pagechae,其最大的優勢是將消息寫入 pagecache 操作批量化。
該方案的缺點是如果由于某些意外操作導致 Broker 進程異常退出,那么存儲在堆外內存的數據會丟失,但如果是放入 pagecache,broke r異常退出并不會丟失消息。
原文鏈接:https://developer.aliyun.com/article/799550?
版權聲明:本文內容由阿里云實名注冊用戶自發貢獻,版權歸原作者所有,阿里云開發者社區不擁有其著作權,亦不承擔相應法律責任。具體規則請查看《阿里云開發者社區用戶服務協議》和《阿里云開發者社區知識產權保護指引》。如果您發現本社區中有涉嫌抄襲的內容,填寫侵權投訴表單進行舉報,一經查實,本社區將立刻刪除涉嫌侵權內容。總結
以上是生活随笔為你收集整理的7张图揭晓RocketMQ存储设计的精髓的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《Saas模式云原生数据仓库应用场景实践
- 下一篇: 基于 RocketMQ 构建阿里云事件驱