天天看點

消息隊列二十年

作者:騰訊技術工程

作者:blithe

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 變成了從節點。講到這裡,想必你已經對主從架構有了一個進一步的了解。

消息隊列二十年

我們先來看看消息隊列的大緻工作流程。

  1. 生産者 、消費者首先會和中繼資料中心(zookeeper)建立連接配接、并保持心跳,擷取服務的實況以及路由資訊。
  2. 消息會被 send 到 topic 下的任一分區中(這裡通過算法會保證每個 topic 下的分區盡可能均勻),一般情況下,資訊需要落盤才可以給上遊傳回 ack,保證了當機後的資訊的完整性。在資訊寫成功主分區後,系統會根據政策,選擇同步複制還是異步複制,以保證單節點故障時的資訊完整性。
  3. 消費者此時開始工作,拉取響應的資訊,并傳回 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 有兩點很大的不同:

  1. 中繼資料管理系統,從 zookeeper 變成了輕量級的獨立服務叢集。
  2. 服務節點變為 多主多從架構。
消息隊列二十年

zookeeper 與 namesrv

kafka 使用的 zookeeper 是 cp 強一緻架構的一種,其内部使用 zab 算法,進行資訊同步和容災,在資訊量較小的情況下,性能較好,當資訊互動變多,因為同步帶來的性能損耗加大,性能和吞吐量降低。如果 zookeeper 當機,會導緻整個叢集的不可用,對于一些交易場景,這是不可接受的,為了提高大資料場景下,消息發現系統的可用性與整體的吞吐量,相比 zookeeper,rocketmq 選擇了輕量級的獨立伺服器 namesrv,其有以下特點:

  1. 使用簡單的 k/v 結構儲存資訊。
  2. 支援叢集模式,每個 namesrv 互相獨立,不進行任何通信。
  3. 資料都儲存在記憶體當中,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 節點沒有任何的負擔,這裡的負擔有幾層含義:

  1. 容器化沒負擔,broker 節點不用考慮任何資料狀态帶來的麻煩。
  2. 擴容、縮容沒負擔,當請求量級突增或者降低的同時,可以随時的添加節點或者減少節點以動态的調整資源,使得整體在一種“合适”的狀态。
  3. 故障轉移沒負擔,當一個節點當機、服務不可用時,可以通快速地轉移所負責的 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. 一些想法

縱觀消息隊列的發展、技術的革新總是解決了某些問題、每種設計的背後都有着一種天然的平衡 ,無論優劣,針對不同的場景,選擇不同的産品,才是王道。