消息中間件架構討論


前言

接上一篇的《業務方對消息中間件的需求》,在可用性和可靠性的基礎上,討論各種架構的優缺點,最后給出自己關於消息中間件的架構思考。

Kafka

首先還是來看Kafka的系統架構(做消息中間件逃不開要去了解Kafka)。

Kafka ecosystem包含以下幾塊內容:

  • Producer
  • Consumer
  • Kafka cluster
  • ZooKeeper

其中ZooKeeper承當了NameServer的角色,同時用於保存系統的元數據,提供選主、協調等功能。

Broker是真正的服務端,用於存儲消息。

可用性

首先看外部依賴的可用性。如果你的系統“強依賴”了外部的其他服務,那么你的系統的可用性必然和外部服務的可用性相關。 (強依賴表示不可脫離依賴的服務保持正常運行)

從上面的架構可以看出Kafka只是依賴了ZooKeeper,而ZooKeeper本身是高可用的(2N+1個節點的ZK集群可以容忍N個節點故障),所以不會對整個集群的可用性造成影響。

接着看Kafka自身的可用性。談可用性必然就會涉及到備份問題,沒有備份就意味着存在單點問題,也就沒有高可用可言了。所以我們具體來看一下Kafka的備份策略。

(InfoQ一篇討論Kafka可用性的文章的配套)

Kafka Replication的數據流如上圖所示,從圖中可以得到的一些信息:

  1. 分區是有備份的,如topic1-part1上圖中有3個
  2. 分區的備份分布在不同的Broker上,上圖中topic1-part1分布在broker1、broker2、broker3上,其中broker1上的為Leader
  3. 分區的Leader是隨機分布的,上圖中topic1-part1的Leader在broker1,topic2-part1的Leader在Broker上,topic3-part1的Leader的Broker4上
  4. 消息寫入到Leader分區,之后通過Leader分區復制到Follower分區

更詳細的Kafka的Replication實現可以去看官網(后續也可能單獨寫一篇),這里不展開。

Kafak這樣的Replication策略,保證了任何一個Broker出現故障時,系統依舊是可用的。如broker1出現故障,此時會重新選舉topic1-part1的Leader,之后可能是broker2或者broker3上的topic1-part1成為Leader然后負責消息的寫入。

所以系統的可用性取決於分區備份的數量,這個備份數據是可配置的。

Kafka自身通過Replication實現了高可用,結合依賴的ZooKeeper也是高可用,所以整個系統的可用性得到了較好的保障。

可靠性

在消息中間件中,可靠性主要就是寫入的消息一定會被消費到,條消息不會丟失。

在分布式環境中消息不丟失有兩點:

  1. 消息在成功寫入一個節點后,消息會做持久化
  2. 消息會被備份到其他物理節點

只要做到上面兩點就可以保證除所有節點都發生永久性故障的情況下數據不會丟失。

Kafka Broker上寫入的消息都會刷盤(可以是異步刷盤也可以是同步刷盤),也會備份到其他物理節點,所以滿足以上兩點。

異步刷盤結合多節點的備份策略也能提供比較好的可靠性,除非是機房掉電之類的情況導致所有節點未刷盤的數據丟失。

當然,消息丟失不一定指消息真的從磁盤上被銷毀或者沒被存儲下來,如果消息被存儲下來了,但是沒辦法被消費,對客戶端來說也是消息丟失。比如Consumer收到消息后進行ACK之后再消費,如果在消費之前Crash了,那么下一次也不會拿到這條消息,也可以理解成消息丟了,但是這這篇文章中我們不討論這種情況。

評價

優點

  1. 部分功能托管給了ZK,自身只需要關注消息相關的內容,從這個角度上說是簡化了部分內容
  2. 機器利用率高。從上面備份的策略可以看出不同Broker之間數據是互為備份的,這樣的結構相對於主從模式提高了機器利用率(大部分主從模式,從都是無用狀態的)

缺點

  1. 引入了ZK,增加了外部依賴,增加了運維的復雜性

備份的方式從系統架構上說,互為主備是較好的方式,但是實現上會比較復雜,如果是自己去實現一個MQ,還是從主從的模式入手比較容易。

(Kafka的備份策略及基層WAL的實現就比較復雜了,這個以后有機會說)

RocketMQ

(圖片取自RocketMQ_design文檔)

RocketMQ中包含以下幾塊內容:

  • Producer
  • Consumer
  • NameServer
  • Broker

Producer及Consumer和Kafka相同(所有的MQ都會提供Producer和Consumer),Rocket也是有Broker集群,和Kafka最大的區別是RocketMQ自己實現了一個集群模式的NameServer服務。

可用性

RocketMQ的可用性也分為NameServer和Broker兩塊討論。

NameServer是集群模式的,且“幾乎”是無狀態的,可以集群部署,所以不會存在可用性的問題。(無狀態意味着每個節點是獨立提供服務的,只需要部署多個節點就可以解決可用性的問題)

Broker的可用性又可以分為兩塊,對一個Topic而言,它可以分布在多個Master Broker上,這樣在其中一個Broker不可用之后,其他的Broker依舊可以提供服務,不影響寫入服務。在一個Master Broker掛掉之后雖然可以通過其他Master來保證寫入的可用性,但是已經寫入到故障Broker的部分數據可能會無法消費。RocketMQ通過Master-Slave的模式來解決這個問題。

Master永久故障后可以將Master上的讀取請求轉移到Slave上,這樣可以保證系統的可用性(Master-Slave之間是異步復制的,意味着可能少量數據還沒有從Master復制到Slave,這個在可靠性部分討論)。

綜合上面的兩點,RocketMQ也提供了高可用的特性,且可用性只取決於自身的服務,沒有像Kafka一樣引入額外的,像ZK這樣的服務。

可靠性

可靠性從單個Broker寫入消息的可靠性和消息備份兩個角度去考慮。

RocketMQ采用了同步刷盤的方式來持久化寫入的消息。

同步刷盤和異步刷盤的唯一差別是異步刷盤寫完pagecache直接返回,而同步刷盤需要等待刷盤完成之后才返回,寫入流程如下:

  1. 寫入pagecache,線程等待,通知刷盤線程進行刷盤
  2. 刷盤線程刷盤后,喚醒前端等待線程,可能是一批線程
  3. 前端等待線程想用戶返回寫入結果

(同步刷盤必然耗時要比異步刷盤要大,如何解決同步刷盤帶來的性能的損耗后面在談)

采用同步刷盤的方式,從單個節點的角度出發可靠性要比異步刷盤的方式要高,因為只要Producer收到消息寫入成功的反饋,那么這條消息必然刷盤了,不會應為掉電等原因導致消息丟失。

單個節點必然會面對單點問題,當一個節點永久故障無法恢復時,哪怕這條消息已經持久化了也是沒有意義的。相對於Kafka的互為備份的方式,RocketMQ采用的M-S的方式。

M-S方式就遇到了主從復制延遲的問題(異步復制永遠是延遲的),那么在Master不可用后可能會導致部分數據丟失。RocketMQ針對這種場景,提供了同步雙寫的模式。

評價

優點

  1. 無外部依賴(這個以為着你的系統不需要額外的服務,無論從運維或者可用性出發,這確實是一個優點)

缺點

  1. M-S結構帶來的機器利用率問題(大部分時候Slave可能是空閑的)

受限於M-S的機器利用率,實際上不會采用一主多從的模式,絕大部分是一主一從,部分可靠性要求沒那么高的業務甚至都是沒有掛載Slave的。這點得到了阿里內部開發同學的確認,這也是M-S模式的缺陷。

MQ的一些其他架構

Kafka引入了外部的ZK,而RocketMQ的主從模式又不夠“好”,那能不能結合一下兩種模式呢?

接着來討論幾種筆者考慮的架構。

結合Kafka和RocketMQ

這種架構主要就是在Kafka的基礎上移除對ZK的依賴。引入ZK主要是為了解決分布式系統的協調問題,另外Kafka會將元數據(Topic的配置、消費進度等信息存在ZK上)保存在ZK上,同時提供NameServer的服務。

在這點上比較贊同RocketMQ的做法。元數據其實都可以存儲在Broker上,因為Broker是有狀態的,所以在它上面的消費進度等信息其實和其他Broker是無關的(如果是有相互備份的需要同步這塊數據),所以NameServer可以很輕量級,做成無狀態的。RocketMQ確實也這樣去做了,NameServer的代碼大約1000行,還是比較簡單的。

這種架構在實現上有一個最大的問題就是移除了ZK之后,內部采用互為主備的方式需要對每個Topic的Partition選出Leader。在無中心節點的架構中自己來實現選主是一件非常困難的事情,包括要去處理網絡分區等問題。當我們在以上架構中取解決這個問題其實可以通過一些妥協,比如可以先選出中心節點,然后由中心節點來負責剩余的選主相關的問題。

中心節點可以簡單的通過人工指定的方式,中心節點本身的可用性其實並不是非常重要,因為脫離中心節點系統是可以正常運行的,只是無法進行選主。系統的可用性取決與是否中心節點故障同時有其他節點發生故障(犧牲了一些自動化運維,因為沒有考慮中心節點的高可用,但是除去了外部依賴,系統設計總是會有tradeoff)。

移除NameServer

在深入考慮一下:

  • 元數據信息無非Topic配置、消費進度,數據量不會很大,完全可以存儲直接存儲在Broker上
  • 且Broker本身已經是多節點的,天然的就可以實現元數據的備份

將元數據存儲在Broker上之后會面臨一個問題:每一個Broker必須擁有所有的元數據,那么所有Broker之間需要通信來獲取Topic數據(如果只是數據可用新,只是幾個Broker之間備份)。

這個問題可以引入Gossip之類的協議來實現,所以架構可以去掉上面的NameServer,演變成如下結構:

到這里,架構其實就剩下一個Broker集群,Broker之間的數據采用Kafka的備份策略,Broker之間的元數據通過Gossip協議來完成復制。

到這里其實系統架構非常簡單了,感覺也沒有可以移除和變更的內容了(筆者的信仰——簡單即美)。

但是其實一直忽略了一個問題就是上面的tradeoff,最終對於一個系統,我們肯定希望足夠自動化,所以我們還是要去解決中心節點的高可用問題。

如何在Broker中選出一個唯一的Leader,這個其實就是分布式系統的一致性問題,只要引入一個可以解決分布式系統一致性問題的協議即可,比如Raft、Paxos之類。

所以這個架構理論上是可行的:

  • 無NameServer;
  • Broker之間采用互為主備的方式來保證系統的可用性和可靠性;
  • 引入Gossip協議來復制元數據;
  • 引入一致性協議來解決選主的問題;
    • 簡單點可以用一致性協議來選中心節點,由中心節點負責協調其他問題
    • 本身也可以通過一致性協議直接來進行每一個Topic的Partition的選主問題

如果我們自己去寫一個MQ

之前說過公眾號希望寫一個類似《從入門到XXX》的系列文章,所以並不希望一上來就將系統實現設計的太復雜,以致於自己都無法實現。還是選擇一個更簡單的架構來便於我們探討實現MQ的核心問題以及真的利用業務時間去做一些嘗試。

所以后續的文章會在以下的架構的基礎上去展開(這個架構之上的內容講完后再開始引入各種協議來簡化架構或提升系統的可用性、可靠性)。

類似RocketMQ的架構,並做簡化:

  • 單節點的NameServer(NameServer自身的服務發現可以通過DNS去做)
  • Broker之間采用主從的模式
  • 元數據存儲在Broker上,匯報到NameServer(每個Broker只存儲部分元數據,在NameServer上聚合)

這個架構實現上會比較簡單,但是依舊有較高的可用性和可靠性。因為本身NameServer是無狀態的,且NameServer的故障不會影響系統核心的服務(消息的發送和消息),所以可以容忍單節點。Broker類似RocketMQ的實現,同步刷盤加上主備的模式也是能提供比較好的可用性和可靠性,只是利用率不夠(基於寫這一系列文章的初衷,就先不考慮使用率的問題了)。

結語

本篇主要是介紹Kafka和RocketMQ的架構以及討論可用性和可靠性的實現,綜合兩者給出筆者思考的MQ的架構。

而文末給出了之后一系列討論的內容的架構基礎,即選擇最容易實現的模式來探討后續的問題,這是需要在寫之后的文章前達成的一個共同約定。

下一篇計划寫一下《Broker的內部實現總覽》。

歡迎關注此公眾號,堅持不懈的寫MQ相關的技術文章,希望和更多的朋友交流。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM