2020 年我有幸加入騰訊 tdmq 初創團隊,當時 tdmq 還正在上雲公測階段,我第一次從一個使用工具的人轉變成了開發工具的人, 這個過程使我沉澱了很多消息隊列知識與設計藝術。 後來在業務中台的實踐中,也頻繁地使用到了 MQ,比如最常見的消息推送,異常信息的重試等等, 過程中也對消息隊列有了更加 ...
2020 年我有幸加入騰訊 tdmq 初創團隊,當時 tdmq 還正在上雲公測階段,我第一次從一個使用工具的人轉變成了開發工具的人,
這個過程使我沉澱了很多消息隊列知識與設計藝術。
後來在業務中台的實踐中,也頻繁地使用到了 MQ,比如最常見的消息推送,異常信息的重試等等,
過程中也對消息隊列有了更加深刻的瞭解。
此篇文章,我會站在一個時間維度的視角上去講解這二十年每款 MQ 誕生的背景以及解決了何種問題。
1. 消息隊列發展歷程
2003 至今有很多優秀的消息隊列誕生,其中就有被大家所熟知的就是 kafka、阿裡自研的 rocketmq、以及後起之秀 pulsar。首先我們先來瞭解一下每一時期消息隊列誕生的背景以及要解決的核心問題是什麼?

如圖所示,我把消息隊列的發展切分成了三個大的階段。
第一階段:解耦合
就是從 2003 年到 2010 年之前,03 年可以說電腦軟體行業剛剛興起,解決系統間強耦合變成了程式設計的一大難題,這一階段以 activemq 和 rabbitmq 為主的消息隊列致力於解決系統間解耦合和一些非同步化的操作的問題,這也是所有消息隊列被使用的最多的功能之一。
第二階段:吞吐量與一致性
在 2010 年到 2012 年期間,大數據時代正式到來,實時計算的需求越來越高,數據規模也越來越大。由於傳統消息隊列已無法滿足大數據的需求,消息隊列設計的關鍵因素逐漸轉向為吞吐量和併發程度。在這一背景下,Kafka 應運而生,併在日誌收集和數據通道領域占據了重要地位。
然而,隨著阿裡電商業務的興起,Kafka 在可靠性、一致性、順序消息、事務消息支持等方面已經無法滿足阿裡電商場景的需求。因此,RocketMQ 誕生了,阿裡在自研消息隊列的過程中吸收了 Kafka 的很多設計理念,如順序寫盤、零拷貝、end-to-end 壓縮方式,併在此基礎上解決了 Kafka 的一些痛點問題,比如強依賴 Zookeeper。後來,阿裡將 RocketMQ 捐贈給了 Apache,並最終成為了 Apache RocketMQ。
第三階段:平臺化
"沒有平臺的產品是沒用的,再精確一點,去平臺化的產品總是被平臺化的產品所取代" 這句話並不是我說的,而是來自一篇來自 2011 年的文章《steve 對亞馬遜和 google 的吐槽》( https://coolshell.cn/articles/5701.html 推薦閱讀)。
2012 以後,隨著雲計算、k8s、容器化等新興的技術興起,如何把基本底層技術能力平臺化成為了眾多公司的攻堅方向,阿裡雲、騰訊雲、華為雲的入場都證明瞭這點,在這種大背景下,Pulasr 誕生了。
雅虎起初啟動 pulsar 項目是為瞭解決以下三個問題:
-
公司內部多團隊重覆建造輪子。
-
當前主流 mq 的租戶隔離機制都支持的不是很好。
-
數據遷移、恢復、故障轉移個個都是頭疼的問題,消息隊列運維成本極高。
這三個問題的答案都指向了一個方向:平臺化。
2. 消息隊列的通用架構及基本概念
第一節我們從時間線上介紹了主流的消息隊列產生的背景,這一小節我們先從生活場景入手,理解消息隊列最最基本的概念,
主題(topic)生產者(producer)消費者(consumer)
場景:食堂吃飯
我們可以把吃飯抽象成三步
-
第一步:當你進入飯堂,首先你想的是我今天吃什麼,選擇合適的檔口,比如有米飯、有面、有麻辣香鍋。這裡的米飯、面、麻辣香鍋就是 topic 主題的概念 -
第二步:當你選擇了面檔,下一步就是排隊,排隊預設的就是站在一個隊伍的隊尾,你加入隊伍的這一過程統稱為入隊,此時對於 MQ 則是成功生產了一條消息。 -
第三步:經過了等待,終於排到了你,並把飯菜成功拿走。這個過程稱為出隊,檔口相當於消費者,消費了"你"這一條信息。
通過這個例子,你可以很好的理解,主題(topic)、生產者(producer)、消費者(consumer)這三個概念。
分區(partition)
分區概念是電腦世界對真實世界很好的一層抽象,理解了分區,對於理解消息隊列極其重要。如果你申請過雲上的消息隊列,平臺會讓你填寫一個分區大小的參數選項,那分區到底是什麼意思,我們接著向下看。
我們還是舉學習食堂的例子,假如有一天學校大面積擴招,一下次多來了一萬名學生,想象一下,你每次去食堂吃面,排上喜歡吃的東西,估計要等個一個小時,學校肯定也會想辦法,很簡單,就是把一個檔口變成多個檔口,對食堂進行擴建(擴容)。擴容前,賣面的檔口只有一個,人多人少你都要排隊。擴容後,你只要選擇(路由)一個人少的檔口就 ok。這裡多個隊伍就是多個分區。當理解了分區,就可以很好理解:分區使得消息隊列的寫吞吐量有了橫向擴展的能力,這裡也是 kafka 為什麼可以高吞吐的本質原因。
3. 主流消息隊列存儲分析
特性與性能是存儲結構的一種顯化表現。特性是表相,存儲才是本質。我們要搞清楚每款消息的特性,很有必要去瞭解它們在架構上的設計。這章節,我們先會去介紹 kafka、rocketmq、pulsar 各自的架構特點,然後再去對比架構上的不同帶來了什麼功能上的不同。
3.1. kafka
架構圖
對於 kafka 架構,需要首先說明的一點,kafka 的服務節點並沒有主從之分,主從的概念是針對 topic 下的某個 partition。對於存儲的單位,巨集觀上來說就是分區,通過分區散落在各個節點的方式的不同,可以組合出各種各樣的架構圖。以下是生產者數量為 1 消費者數量為 1 分區數為 2 副本數為 3 服務節點數為 3 架構圖。圖中兩塊綠色圖案分別為 topic1-partition1 分區和 topic1-partition2 分區,淺綠色方塊為他們的副本,此時對於服務節點 1 topic1-partition1 就是主節點,服務節點 2 和 3 為從節點;但是對於服務節點 2 topic1-partition2 有是主分區,服務節點 1 和服務節點 3 變成了從節點。講到這裡,想必你已經對主從架構有了一個進一步的理解。

我們先來看看消息隊列的大致工作流程。
-
生產者 、消費者首先會和元數據中心(zookeeper)建立連接、並保持心跳,獲取服務的實況以及路由信息。 -
消息會被 send 到 topic 下的任一分區中(這裡通過演算法會保證每個 topic 下的分區儘可能均勻),一般情況下,信息需要落盤才可以給上游返回 ack,保證了宕機後的信息的完整性。在信息寫成功主分區後,系統會根據策略,選擇同步複製還是非同步複製,以保證單節點故障時的信息完整性。 -
消費者此時開始工作,拉取響應的信息,並返回 ack,此時 offset+1。
好的設計
下來我們來瞭解一下 kafka 架構優秀的設計理念。
-
磁碟順序寫盤。
Kafka 在底層設計上強依賴於文件系統(一個分區對應一個文件系統),本質上是基於磁碟存儲的消息隊列,在我們固有印象中磁碟的讀寫速度是非常慢的,慢的原因是因為在讀寫的過程中所有的進程都在搶占“磁頭”這把鎖,磁頭在讀寫之前需要將其移動到合適的位置,這個“移動”極其耗費時間,這也就是磁碟慢的原因,但是如何不用移動磁頭呢,順序寫盤就誕生了。
Kafka 消息存儲在分區中,每個分區對應一組連續的物理空間。新消息追加到磁碟文件末尾。消費者按順序拉取分區數據消費。Kafka 的讀寫是順序的,可以高效地利用 PageCache,解決磁碟讀寫的性能問題。
以下是一張磁碟、ssd、記憶體的寫入性能對比圖,我們可以明顯的看出順序寫入的性能快於 ssd 的順序/隨機寫,與記憶體的順序/隨機性能也相差不大,這一特性非常重要,很多組件的底層存儲設計都會用到這點,理解好這點對理解消息隊列尤為重要。(推薦閱讀《 The Pathologies of Big Data 》 https://queue.acm.org/detail.cfm?id=1563874 ))

一些問題
任何事物都有兩面性,順序寫盤的設計也帶來一些其他的問題。
-
topic 數量不能過大
kafka 的整體性能收到了 topic 數量的限制,這和底層的存儲有密不可分的關係,我們上面講過,當消息來的時候,底層數據使用追加寫入的方式,順序寫盤,使得整體的寫性能大大提高,但這並不能代表所有情況,當我們 topic 數量從幾個變成上千個的時候,情況就有所不同了,如下圖所示。

以上左圖代表了,隊列中從頭到尾的信息為:topic1、topic1、topic1、topic2,在這種情況下,很好地運用了順序寫盤的特性,磁頭不用去移動,但是對於右邊圖的情況,隊列中從頭到尾的信息為:topic1、topic2、topic3、topic4,當隊列中的信息變的很分散的時候,這個時候我們會發現,似乎沒有辦法利用磁碟的順序寫盤的特性,因為每次寫完一種信息,磁頭都需要進行移動,讀到這裡,你就很好理解,為什麼當 topic 數量很大時,kafka 的性能會急劇下降了。
當然會有小伙伴問,沒有其他辦法了嗎,當然有。我們可以把存儲換成速度更快 ssd 或者針對每一個分區都搞一塊磁碟,當然這都是錢!很多時候,系統的 6 個 9、7 個 9,並不是有多好的設計,而是用真金白銀換來的,這是一種 trade off,失去什麼得到什麼,大家可以對比看看自己的系統,大多數情況是什麼換什麼。
3.2. rocketmq
架構圖
以下是 rocketmq 雙主雙從的架構,對比 kafka,rocketmq 有兩點很大的不同:
-
元數據管理系統,從 zookeeper 變成了輕量級的獨立服務集群。 -
服務節點變為 多主多從架構。

zookeeper 與 namesrv
kafka 使用的 zookeeper 是 cp 強一致架構的一種,其內部使用 zab 演算法,進行信息同步和容災,在信息量較小的情況下,性能較好,當信息交互變多,因為同步帶來的性能損耗加大,性能和吞吐量降低。如果 zookeeper 宕機,會導致整個集群的不可用,對於一些交易場景,這是不可接受的,為了提高大數據場景下,消息發現系統的可用性與整體的吞吐量,相比 zookeeper,rocketmq 選擇了輕量級的獨立伺服器 namesrv,其有以下特點:
-
使用簡單的 k/v 結構保存信息。 -
支持集群模式,每個 namesrv 相互獨立,不進行任何通信。 -
數據都保存在記憶體當中,broker 的註冊過程通過迴圈遍歷所有 namesrv 進行註冊。
局部順序寫(kafka) 與 完全順序寫(rocketmq)
kafka 寫流程中會把不同分區寫入對應的文件系統中,其設計理念保證了 kafka 優秀的水平擴容能力。RocketMQ 的設計理念則是追求極致的消息寫,將所有的 topic 消息存儲在同一個文件中,確保所有消息發送時按順序寫文件,盡最大能力確保消息發送的高可用性與高吞吐量,但是有利有弊,這種 topic 共用文件的設計會使得 rocketmq 不支持刪除指定 topic 功能,這也很好理解,對於某個 topic 的信息,在磁碟上的表現是一段非連續的區域,而不像 kafka,一個 topic 就是一段連續的區域。如下圖所示。

rocketmq 存儲結構
下麵我們來重點介紹 rocketmq 的存儲結構,通過對存儲結構的瞭解,你將會更好的對讀寫性能有一個更深的認識。
不同 topic 共用一個文件的模式帶來了高效的寫性能,但是單看某一 topic 的信息,相對於磁碟上的表現為非連續的若幹片段,這樣使得定位指定 topic 下 msg 的信息,變成了一個棘手的問題。
rocketmq 在生產過程中會把所有的 topic 信息順序寫入 commitlog 文件中,消費過程中,使用 ConsumeQueue、IndexFile 索引文件實現數據的高效率讀取。下麵我們重點介紹這三類文件。
-
Commitlog
從物理結構上來看,所有的消息都存儲在 CommitLog 裡面,單個 CommitLog 文件大小預設 1G,文件名長度為 20 位,左邊補零,剩餘為起始偏移量。
比如 00000000000000000000 代表了第一個文件,起始偏移量為 0 文件大小為 1G=1073741824;當第一個文件寫滿了,第二個文件為 00000000001073741824,起始偏移量為 1073741824,以此類推。消息主要是順序寫入日誌文件,當文件滿了,寫入下一個文件。CommitLog 順序寫,可以大大提高寫入效率。
-
ConsumeQueue (索引文件 1)
ConsumeQueue 文件可以看成是基於 topic 的 commitlog 索引文件。Consumer 即可根據 ConsumeQueue 來查找待消費的消息。
因為 ConsumeQueue 里只存偏移量信息,大部分的 ConsumeQueue 能夠被全部讀入記憶體,速度極快。在定位 msg 信息時直接讀取偏移量,在 commitlog 文件中使用二分查找到對應的全量信息即可。
-
IndexFile(索引文件 2)
IndexFile 是另一種可選索引文件,提供了一種可以通過 key 或時間區間來查詢消息的方法。IndexFile 索引文件其底層實現為 hash 索引,Java 的 HashMap,可以快速通過 key 找到對應的 value。
講到這裡,我們在想想 kafka 是怎麼做的,對的,kafka 並沒有類似的煩惱,因為所有信息都是連續的!以下是文件在目錄下的存儲示意圖。

3.3. pulsa
架構圖(分層+分片)

pulsar 相比與 kafka 與 rocketmq 最大的特點則是使用了分層和分片的架構,回想一下 kafka 與 rocketmq,一個服務節點即是計算節點也是服務節點,節點有狀態使得平臺化、容器化困難、數據遷移、數據擴縮容等運維工作都變的複雜且困難。
分層:Pulsar 分離出了 Broker(服務層)和 Bookie(存儲層)架構,Broker 為無狀態服務,用於發佈和消費消息,而 BookKeeper 專註於存儲。
分片 : 這種將存儲從消息服務中抽離出來,使用更細粒度的分片(Segment)替代粗粒度的分區(Partition),為 Pulsar 提供了更高的可用性,更靈活的擴展能力。
服務層設計
Broker 集群在 Pulsar 中形成無狀態服務層。服務層是“無狀態的”,所有的數據信息都存儲在了 BookKeeper 上,所有的元信息都存儲在了 zookeeper 上,這樣使得一個 broker 節點沒有任何的負擔,這裡的負擔有幾層含義:
-
容器化沒負擔,broker 節點不用考慮任何數據狀態帶來的麻煩。 -
擴容、縮容沒負擔,當請求量級突增或者降低的同時,可以隨時的添加節點或者減少節點以動態的調整資源,使得整體在一種“合適”的狀態。 -
故障轉移沒負擔,當一個節點宕機、服務不可用時,可以通快速地轉移所負責的 topic 信息到別的基節點上,可以很好做到故障對外無感知。

存儲層設計
pulsar 使用了類似於 raft 的存儲方案,數據會併發的寫入多個存儲節點上,下圖為四存儲節點、三副本架構。

broker2 節點當前需要寫入 segment1 到 segment4 數據,流程為:segment1 併發寫入 b1、b2、b3 數據節點、segment2 併發寫入 b2、b3、b4 數據節點、segment3 併發寫入 b3、b4、b1 數據節點、segment4 併發寫入 b1、b2、b4 數據節點。這種寫入方式稱為條帶化的寫入方式、這種方式潛在的決定了數據的分佈方式、通過路由演算法,可以很快的找到對應數據的位置信息,在數據遷移與恢復中起到重要的作用。
擴容
當存儲節點資源不足的時候,常規的運維操作就是動態擴容,相比 kafka 與 rocketmq、pulsar 不用考慮原數據的"人為"搬移工作,而是動態新增一個或者多個節點,broker 在寫入數據時通過路有演算法優先寫入資源充足的節點,使得整體的資源利用力達到一個平衡的狀態,如圖所示。

以下是一張 kafka 分區和 pulsar 分片的一張對比圖,左圖是 kafka 的數據存儲特點,因為數據和分區的強綁定,導致了第三艘小船沒有任何的數據,而相比 pulsar,數據不和任何存儲節點綁定,而是實時的動態寫入,從數據分佈和資源利用來說,要做的更好。

容災
當 bookie4 存儲節點宕機不可用時,如何恢復節點數據?這裡只需要增加新的存儲節點,並且拷貝 bookie2 與 bookie3 上的數據即可,這個過程對外是無感知的,實現了平滑切換,如圖所示。

4. 一些想法
縱觀消息隊列的發展、技術的革新總是解決了某些問題、每種設計的背後都有著一種天然的平衡 ,無論優劣,針對不同的場景,選擇不同的產品,才是王道。
作者:blithe
本文來自博客園,作者:古道輕風,轉載請註明原文鏈接:https://www.cnblogs.com/88223100/p/Message-queue-for-twenty-years.html