訊息佇列線上遷移實戰 | 最佳實踐

前言

訊息佇列(Message Queue,下文簡稱MQ)是分散式網際網路架構中必不可少的核心元件,包括RocketMQ、Kafka、RabbitMQ等在業界廣泛使用的產品,在訊息分發、非同步解耦、削峰填谷、廣播通知等領域發揮著巨大的作用。

在MQ的使用過程中,線上對MQ元件進行遷移是一個非常普遍的需求,在如下的幾個場景中,都會涉及到MQ的線上遷移:

(1)規格升級。比如將3 Broker的Kafka叢集替換成6 Broker的Kafka叢集。

(2)更換另一種MQ產品。比如將RabbitMQ替換為效能和擴充套件性更強的RocketMQ。

(3)使用雲服務替換自建MQ叢集。比如將自建的RocketMQ叢集替換為雲上商業版RocketMQ服務。

在MQ遷移的過程中,存在3個非常重要的需求:

操作簡單。

風險可控。

不影響業務系統的正常執行。

如何讓MQ的線上遷移同時滿足這3個重要的需求呢?本文將對幾種可行的方式進行深入探討。

理論基礎

在涉及到MQ線上遷移的所有方案中,都存有一個很重要的原則:對於發往MQ的每一條訊息,如果已經被它的消費者成功接收並得到處理,這條訊息就不再具有業務含義。已經被成功接收並得到處理的訊息,只體現出統計方面的價值,並不需要隨著MQ本身的遷移而進行資料遷移。

在資料庫遷移的場景中,新舊DB之間的資料遷移是非常重要的工作,這是因為DB中的資料是持久化的資料,需要伴隨著資料庫的生命週期而長期存在。

而對於MQ而言,訊息一旦被訊息者接收並得到處理,就不再是持久化的資料,可以直接刪除或歸檔。因此在MQ線上遷移的場景中,對已經處理過的訊息,是沒有資料遷移必要的。這樣就將問題簡化為:如何在遷移的過程中確保每一條訊息被成功接收並得到處理。

在系統維護期停業務遷移

我們先從一個最簡單方案開始體驗MQ線上遷移是如何進行的。對於不要求7*24小時連續執行的業務系統,可以利用系統維護期的時間視窗,透過停業務的方式來實現訊息佇列的遷移。這種遷移方式因為不需要保證業務的連續性,操作起來就非常簡單。

(1)初始狀態

訊息佇列線上遷移實戰 | 最佳實踐

進入維護視窗期後,關閉生產者應用,這個時候不會再有的新的訊息寫入MQ:

訊息佇列線上遷移實戰 | 最佳實踐

在這個狀態下保持一段時間,當MQ上的所有訊息都被消費者接收併成功處理後,就可以對消費者進行版本釋出,使其從新的MQ上接收訊息:

訊息佇列線上遷移實戰 | 最佳實踐

接下來再啟動生產者,使其指向新的MQ,整個操作就已經完成。當系統執行穩定後,可以對原MQ例項進行相關資料歸檔後直接下線:

訊息佇列線上遷移實戰 | 最佳實踐

在停業務遷移方案中,最關鍵一步,在於如何在系統維護視窗期之內,確保原MQ上的所有訊息被消費者接收併成功處理。因為生產者關閉之後,不會有新的訊息寫入原MQ,只要預留足夠長的時間,原MQ上堆積的訊息一定會被消費者取走。在這個方案中,新MQ和原MQ也可以是不同的產品,比如從RabbitMQ遷移到RocketMQ也是可以支援的,因為生產者和消費者都經過了版本釋出的動作,只需要在新版本中對API和收發邏輯進行修改就可以實現。

雙訂閱方案

在網際網路領域,能夠容忍維護期將業務暫停的系統越來越少了,7*24不間斷服務是行業的趨勢,上述的停業務遷移方案就不再適用了,如何在MQ遷移的過程中確保業務持續執行呢?

有一個非常棒的idea是讓消費者同時具有從原MQ和新MQ接收訊息的能力,這樣不管生產方往哪一個MQ傳送訊息,都能夠確保訊息得到及時的處理。這是一種不需要暫停業務的方案,我們來看一下具體的步驟。

首先對消費者進行改造,使其同時連上新老兩個MQ,具備同時從新老MQ接收訊息的能力,這就是所謂的“雙訂閱”:

訊息佇列線上遷移實戰 | 最佳實踐

接下來對生產者進行釋出,使其往新MQ傳送訊息,等原MQ上堆積的所有訊息被消費者接收併成功處理後,就可以對原MQ下線:

訊息佇列線上遷移實戰 | 最佳實踐

在對原MQ下線的時候,因為消費者還保持著雙訂閱的狀態,所以最好先切斷消費者與原MQ的連線,再關閉原MQ,否則會造成一些異常(取決於SDK的實現)。如果消費者的訂閱邏輯實現的足夠優雅,可以在不重啟消費者的情況下,透過一個指令線上切斷消費者與原MQ的連線。

這個方案看似可以用一種對業務無損的方式線上遷移MQ,在實際操作中可行性卻很低,其根本原因在於:同時從兩個MQ接收訊息的改造工作量極大。一般情況下,每一個消費者都在引入MQ產品對應的SDK,並透過MQ提供接入點與MQ建立連線後,接下來就只需要圍繞業務邏輯完成所需要的訊息訂閱操作。這個時候要想同時從一個新的MQ接入訊息,需要在程式碼層面對所有的訂閱邏輯進行改造,這是一項非常複雜的工作。

在新MQ和原MQ是不同訊息佇列產品的情況下,消費者需要同時引入兩套不同的SDK,改造難度會變得更大。基於雙訂閱方案完成MQ的遷移後,還需要考慮將來清理掉消費者從原MQ接收處理訊息的遺留程式碼,這也是需要一定工作量的。

如果在MQ和消費者中間,能有一箇中間件來實現雙訂閱的邏輯,是不是消費者的程式碼就不需要改造呢?答案是肯定的。但引入這樣的中介軟體本身就是一項非常有挑戰的工作,還增加了整個系統的複雜度,如果僅僅是為了MQ的線上遷移而引入一個新的元件,是得不償失的。

基於工作量和風險的考慮,儘量不要使用雙訂閱方案。

最優方案

雙訂閱的本質,在於存在一個消費者可以同時接收新舊兩個MQ訊息的中間狀態,在這個狀態下,不管生產者往哪個MQ傳送訊息,訊息都可以得到及時的處理。能不能有一種更簡單的方式讓消費者可以同時接收新舊兩個MQ的訊息呢?當然有,而且實現方式更加的簡單。

在一個可靠的分散式微服務系統中,應用都可以透過增加節點的方式而進行水平擴容,為了確保整套系統的高可用性,每一個應用都不應該長期處於單例項執行狀態,而是透過多個無狀態的應用例項組成一個應用叢集。因此,在真實環境下,不管是訊息的生產者還是消費者,都至少有2個例項在執行。在遷移MQ的過程中,“消費者可以同時接收新舊兩個MQ的訊息”的中間狀態,並不一定要讓消費者的每一個例項都透過雙訂閱來實現,其實只要讓一部分例項從原MQ接收訊息,另一部分例項從新MQ接收訊息就能滿足了。透過這樣的思路,能極大程度上簡化MQ遷移的工作量,而且在遷移的過程中確保業務不受任何影響。

在遷移之前,需要先把元資料資訊從原MQ複製到新MQ叢集,不同的訊息佇列產品之前元資料的格式不一樣,需要根據業務場景進行元資料的轉換,元資料包括Topic、Queue、消費組、基礎配置等資訊。

訊息佇列線上遷移實戰 | 最佳實踐

首先,透過灰度釋出機制,讓一部分消費例項連上新的MQ。如果之前的消費者是單例項,這個時候也可以增加一個新的消費例項來完成這個步驟:

訊息佇列線上遷移實戰 | 最佳實踐

接下來,讓生產者往新的MQ傳送訊息,這個操作並不一定需要採取一刀切的方式,也可以透過灰度釋出的方式讓訊息的轉向逐步轉移到新的MQ上來。最終,原MQ將不再接收新的訊息,它上面堆積的訊息總將會被成功接收處理,這個時候可以繼續透過灰度釋出的方式解除消費者與原MQ的連線,連線全部解除完之後,原MQ就可以關閉了。

訊息佇列線上遷移實戰 | 最佳實踐

在這個方案中,對於單個的生產者和消費者,都不存在同時連線新舊兩個MQ的情況,因此在改造工作量非常小。而且遷移的過程透過灰度的方式實現,既不會影響業務,又可以進一步的降低風險,是訊息佇列線上遷移的通用方案。

常見問題

問: 為什麼不在新舊MQ之間進行訊息資料的同步?

答:對於MQ而言,訊息一旦被訊息者接收並得到處理,就不再是持久化的資料,可以直接刪除或歸檔。在遷移的過程中,新舊MQ之間訊息資料同步是沒有必要的,反而會增加遷移的難度,並導致訊息被重複接收。

問:遷移過程中需要驗證訊息冪等性嗎?

答: RocketMQ、Kafka等大多數訊息佇列產品都沒有從訊息佇列服務端本身確保訊息只投遞一次,需要消費者自行實現對冪等性的保證。因此,不管在訊息佇列的遷移過程中,還是正常使用中,都應該藉助資料庫、Redis等外部系統確保訊息的冪等性,否則會造成業務邏輯重複處理。