歷史原因,公司存在多個 MQ 同時使用的問題,我們中介軟體團隊在去年下半年開始支援對 Kafka 和 Rabbit 能力的進行封裝,初步能夠完全支撐業務團隊使用。
鑑於在之前已經基本完全實施 Kafka 管控平臺、以及 Kafka 叢集遷移管控,我們基本可以認為團隊對於 Kafka 的把控能力初具規模。
因此,考慮到以下幾點原因,我們決定對 RabbitMQ 不再做維護和支援。
原因
使用混亂和維護困難
基於我們的資料統計和分析發現,基本上沒有服務使用我們自己封裝的 RabbitMQ 能力,用到的基本上是spring-amqp
或者原生的Rabbit 使用方式,存在使用混亂,方式不統一的問題,對於排查問題方面存在更多的問題。
另外考慮到對於 MQ 能力支援要做雙份,Kafka 和 Rabbit 都要支援相同的功能,對於人力資源方面存在浪費,當然也由於本身目前沒有對 RabbitMQ 非常精通的同學,所以對於維護能力這方面存在擔憂。
分割槽容錯問題
RabbitMQ 叢集對於網路分割槽的容錯性不高,根據調查發現,系統中 RabbitMQ 高可用方案使用映象佇列,而當 RabbitMQ 出現網路分割槽時,不同分割槽裡的節點會認為不屬於自身所在分割槽的節點都已經掛了,對於佇列、交換器、繫結的操作僅對當前分割槽有效。
而且,如果原叢集中配置了映象佇列,而這個映象佇列又牽涉兩個或者更多個網路分割槽中的節點時,每一個網路分割槽中都會出現一個 master 節點,對於各個網路分割槽,此佇列都是相互獨立的。
在預設的情況下,架構本身存在腦裂的風險,在 3.1 版本下是無法自動恢復的,之後的版本才會自動探測網路分割槽,人工介入存在資料丟失的風險。
效能瓶頸
映象佇列解決了 Rabbit 高可用的問題,但是並不能增加負載和效能,線上曾經出現過 RabbitMQ 在高流量下的效能問題,就是因為佇列由單個節點承載流量,在高併發情況在叢集中單個節點存在效能瓶頸。
即便我們目前大部分場景下 MQ 流量不高,但是一旦出現問題,將成為整個系統的效能瓶頸。
另外我們對 Rabbit 做了一些效能方面的測試:
測試叢集一共有 4 臺磁碟節點,其中每臺 16 核,如果我們不做 Sharding,單佇列最高 TPS 在 5K 左右,如果是記憶體節點,官方可以給出的處理極限為 50K/s,如果做 Sharding,單佇列處理能力可以達到 10K/s。
上述結論都是以訊息能夠被正常快速消費為前提,實際上在高流量或者大量訊息積壓的情況會導致叢集效能急劇下降。
運維&管控
基於以上現有的問題和難點,我們決定對 Rabbit 進行全量遷移至 Kafka,以便能在業務高速發展過程中能夠保障對於穩定性、高可用、高效能方面的追求。
在方法論和理論體系層面,我們對業務生產有三板斧:可灰度、可監控、可回滾。
同樣,對於訊息中介軟體平臺運維我們希望有三板斧:可運維、可觀測、可管控,那麼目前基於 Kafka 的叢集管控和 Kafka Manager 的能力我們已經基本做到了上述幾點。
- 高可用:根據自身經驗,Kafka 本身擁有極高的平臺可用性
- 高效能:Kafka 可支撐極高的 TPS,並且支援水平擴充套件,可快速滿足業務的流量增長需求
- 功能支援:在原有兩個 MQ 能力基礎上,基礎支援順序訊息、延時訊息、灰度訊息、訊息軌跡等
- 運維管控:基於 Kafka Manager 基礎上進行二次開發,豐富管控能力和運維支撐能力,提供給開發、運維、測試更好的使用體驗和運維能力。
模型對比
RabbitMQ
Exchange:生產者將訊息傳送到Exchange,由交換器將訊息透過匹配Exchange Type、Binding Key、Routing Key後路由到一個或者多個佇列中。
Queue:用於儲存訊息,消費者直接繫結Queue進行消費訊息
Routing Key:生產者傳送訊息給 Exchange 會指定一個Routing Key。
Binding Key:在繫結Exchange與Queue時會指定一個Binding Key。
Exchange Type:
- Direct:把訊息路由到那些 Binding Key 和 Routing Key 完全匹配的佇列中
- Fanout:把訊息轉發給所有與它繫結的佇列上,相當於廣播模式
- Topic:透過對訊息的 Routing Key 和 Exchange、Queue 進行匹配,將訊息路由給一個或多個佇列,釋出/訂閱模式
- Headers:根據訊息的 Header 將訊息路由到不同的佇列,和 Routing Key 無關
Kafka
Topic:傳送訊息的主題,對訊息的組織形式
Broker:Kafka 服務端
Consumer Group:消費者組
Partition:分割槽,topic 會由多個分割槽組成,通常每個分割槽的訊息都是按照順序讀取的,不同的分割槽無法保證順序性,分割槽也就是我們常說的資料分片sharding機制,主要目的就是為了提高系統的伸縮能力,透過分割槽,訊息的讀寫可以負載均衡到多個不同的節點上
遷移方案
綜上,我們將要對系統中所有使用RabbitMQ的服務進行遷移操作,整個遷移我們應該保證以下 3 點:
- 操作便捷,不能過於複雜,複雜會帶來更多的不可控風險
- 風險可控,盡最大可能降低遷移對業務的影響
- 不影響業務正常執行
消費者雙訂閱
- 對消費者進行改造,同時監聽 Rabbit 和 Kafka 訊息
- 對生產者進行改造,遷移至Kafka傳送訊息
- 等待 Rabbit 遺留訊息消費完畢之後,直接下線即可
優點:可以做到無損遷移
缺點:
- 需要同時維護兩套監聽程式碼,可能有大量的工作量,遷移完成之後還需要再進行一次老程式碼下線
- 訊息無法保證順序性
基於灰度單訂閱
這是基於雙訂閱模式的最佳化,透過使用我們的灰度/藍綠髮布的能力,做到可以不雙訂閱,不用同時監聽兩個訊息佇列的訊息。
- 直接修改消費者程式碼,釋出灰度/藍節點,監聽 Kafka 訊息
- 生產者改造,往 Kafka 傳送訊息
- 等待老的 Rabbit 訊息消費完畢,下線,這裡存在一個問題就是在進行灰度之後全量的過程中可能造成訊息丟失的情況,對於這個問題的解決方案要區分來看,如果業務允許少量的丟失,那麼直接全量即可,否則需要對業務做一定的改造,比如增加開關,全量之前關閉傳送訊息,等待存量訊息消費完畢之後再全量。
優點:
- 基於雙訂閱方案改造,可以做到不同時監聽兩個佇列的訊息,減少工作量
- 可以做到無損遷移
缺點:同樣無法保證訊息有序性
實際場景問題
上述只是針對現狀的遷移方案考慮,那麼還有一些跟實際和複雜的問題可能需要考慮。
比如訊息的場景有可能不是這種簡單的釋出/訂閱關係,可能存在網狀、環狀的釋出/訂閱關係,該如何處理?
其實是一樣的道理,只要我們能夠梳理清楚每個 Exchange 之間的釋出/訂閱的關係,針對每個 Exchange 進行操作,就能達到一樣的平滑遷移效果。
我們要做的就是針對每個 Exchange 進行遷移,而不是針對服務,否則遷移是無法進行下去的,但是這樣帶來的另外一個問題就是每個服務需要釋出多次,而且如果碰到多個複雜消費或者生產的情況要特別小心。
實施細節
基於現狀,我們對所有 Rabbit Exchange 的情況進行了詳細的統計,將針對不同的 Exchange 和型別以及功能使用以下方式處理。
- 無用的Exchange、無生產者或者無消費者,還有沒有任何流量的,可以直接刪除
- Fanout 型別,Exchange 對應 Topic,Queue 對應 Consumer Group,還有存在使用隨機佇列的,需要對應多個Consumer Group(單獨做一個簡單的能力封裝處理)
- Direct 型別,RoutingKey 對應 Topic,Queue 對應 Consumer Group
- Topic 型別,RoutingKey 對應 Topic,Queue 對應 Consumer Group,實際並未發現使用到萬用字元情況
- 延遲佇列、重試等功能,基於 spring-kafka 做二次封裝
驗證&監控&灰度&回滾
驗證
- 遷移後針對 Rabbit 驗證,透過管理平臺流量或者日誌輸出來確認,而且現狀是大部分 Exchange 流量都比較小,所以可能需要自行傳送訊息驗證遷移效果。
- 遷移後針對 Kafka 流量進行驗證可以透過 Kafka Manager 平臺或者日誌
監控
監控透過 Kafka Manager 平臺或者現有監控
灰度
方案本身 Consumer 和 Producer 都可以直接灰度釋出,預發驗證
回滾
服務回滾,按照發布順序控制回退順序
巨人的肩膀: