vivo魯班RocketMQ平臺的訊息灰度方案

vivo網際網路技術發表於2022-03-14

一、方案背景

RocketMQ(以下簡稱MQ)作為訊息中介軟體在事務管理,非同步解耦,削峰填谷,資料同步等應用場景中有著廣泛使用。當業務系統進行灰度釋出時,Dubbo與HTTP的呼叫可以基於業界通用的灰度方式在我們的微服務治理與閘道器平臺來實現,但MQ已有的灰度方案都不能完全解決訊息的隔離與切換銜接問題,為此,我們在魯班MQ平臺(包含根因分析、資源管理、訂閱關係校驗、延時優化等等的擴充套件)增加了MQ灰度功能的擴充套件實現。

二、RocketMQ技術特點

為什麼MQ的灰度方案遲遲沒有實現呢?我們先來回顧一下RocketMQ的幾個核心技術點。

2.1 儲存模型的簡述

圖片

(圖2.1  MQ的儲存模型)

CommitLog:訊息體實際儲存的地方,當我們傳送的任一業務訊息的時候,它最終會儲存在commitLog上。MQ在Broker進行叢集部署(這裡為也簡潔,不涉及主從部分)時,同一業務訊息只會落到叢集的某一個Broker節點上。而這個Broker上的commitLog就會儲存所有Topic路由到它的訊息,當訊息資料量到達1個G後會重新生成一個新的commitLog。

Topic:訊息主題,表示一類訊息的邏輯集合。每條訊息只屬於一個Topic,Topic中包含多條訊息,是MQ進行訊息傳送訂閱的基本單位。屬於一級訊息型別,偏重於業務邏輯設計。

Tag:訊息標籤,二級訊息型別,每一個具體的訊息都可以選擇性地附帶一個Tag,用於區分同一個Topic中的訊息型別,例如訂單Topic, 可以使用Tag=tel來區分手機訂單,使用Tag=iot來表示智慧裝置。在生產者傳送訊息時,可以給這個訊息指定一個具體的Tag, 在消費方可以從Broker中訂閱獲取感興趣的Tag,而不是全部訊息(注:嚴謹的拉取過程,並不全是在Broker端過濾,也有可能部分在消費方過濾,在這裡不展開描述)。

Queue:實際上Topic更像是一個邏輯概念供我們使用,在原始碼層級看,Topic以Queue的形式分佈在多個Broker上,一個topic往往包含多條Queue(注:全域性順序訊息的Topic只有一條Queue,所以才能保證全域性的順序性),Queue與commitLog存在對映關係。可以理解為訊息的索引,且只有通過指定Topic的具體某個Queue,才能找到訊息。(注:熟悉kafka的同學可以類比partition)。

消費組及其ID:表示一類Producer或Consumer,這類Producer或Consumer通常生產或消費同應用域的訊息,且訊息生產與消費的邏輯一致。每個消費組可以定義全域性維一的GroupID來標識,由它來代表消費組。不同的消費組在消費時互相隔離,不會影響彼此的消費位點計算。

2.2 訊息傳送與消費

圖片

(圖2.2  訊息傳送與拉取模型)

2.2.1 客戶端標識

在生產者或消費者叢集中,每一個MQ客戶端的執行例項,在MQ的客戶端會保證產生唯一的ClientID。注:同一應用例項中,既充當生產者,也充當消費者時,其ClientID實際上是同一個取值。

2.2.2 訊息傳送

當向某個Topic傳送訊息的時候,會首先獲得這個Topic的後設資料,後設資料會包括它有多少個Queue,每個Queue屬於哪個Broker。預設的情況下,傳送方會選擇一條Queue傳送當前訊息,演算法型別是輪詢,也就是下一條訊息會選擇另一條Queue進行傳送。另外MQ也提供了指定某條Queue或者自定義選擇Queue的方法進行訊息的傳送,自定義選擇Queue需實現MessageQueueSelector介面。

2.2.3 訊息消費

進行訊息的消費時,同一消費組(GroupID)的消費者會訂閱Topic,消費者首先獲得Topic的後設資料,也就是會獲得這個Topic的所有Queue資訊。然後這些Queue按規則分配給各個具體的客戶端(ClientID),各個客戶端根據分配到的Queue計算對應的需要拉取訊息的offset並生成PullRequest,拉取訊息並消費完成後,客戶端會生成ACK並更新消費進度。

這裡的消費進度是該批訊息未消費成功的最小offset,如圖2.3所示,一批訊息中如果1、5未消費,其餘的訊息已消費,此時更新的offset仍是1,消費者如果當機重啟,會從1號開始消費訊息,此時2、3、4號訊息將會重複消費。

圖片

(圖2.3  消費進度更新示意圖)

因此RocketMQ只保證了訊息不會丟失,無法保證訊息不會重複消費,訊息的冪等性需要業務自己實現。

另外,消費方可以指定消費某些Tag的訊息,在pullRequest進行拉取時,會在Broker裡會按照儲存模型的Queue索引資訊按hash值進行快速過濾,返回到消費方,消費方也會對返回的訊息進行精準的過濾。

2.3 訂閱關係一致性

在消費端,同一消費組內(GroupID相同,本節所描述的前提都是同一消費組內)的各個應用例項MQ客戶端需要保持訂閱關係的一致性,所謂訂閱關係的一致性就是同一消費組內的所有客戶端所訂閱的Topic和Tag必須完全一致,如果組內訂閱關係不一致,訊息消費的邏輯就會混亂,甚至導致訊息丟失。

2.3.1 訂閱關係的維護

每一個應用例項MQ客戶端都有獨立的ClientID,我們簡單地講解一下訂閱關係的維護:

  • 各個MQ消費客戶端會帶著它的ClientID向所有Broker傳送心跳包,心跳包中含有具體的本客戶端訂閱的Topic & Tag等資訊,registerConsumer方法會按照消費組將客戶端分組儲存,同一消費組內的ClientID資訊在同一個ConsumerGroupInfo中;

  • Broker在接收到任何消費客戶端發過來的心跳包後,會在ConsumerManager類中的ConcurrentMapconsumerTable按照消費組名稱GroupID作為key儲存不同的消費組資訊,同一消費組的訂閱資訊會在每一次收到心跳包後,都會按當前的訂閱Topic & Tag資訊進行更新維護,也就是相當於只儲存最新的心跳包訂閱資訊(心跳包中的subVersion會標記心跳包版本,當重平衡結果發生改變後,subVersion會更新,Broker只會儲存最新版本的心跳包中的訂閱資訊),不管這個心跳包來源於這個消費組的哪個ClientID。(由於Tag是依賴於Topic的屬性,Topic和Tag訂閱關係不一致時Broker對應的處理結果也略有不同,具體可見updateSubscription方法)。

2.3.2 訂閱不一致影響

在這裡使用例子的方式來闡述訂閱關係不一致帶來的部分問題。假設同一組內的消費者clientA訂閱了TOPIC_A,clientB訂閱了TOPIC_B;TOPIC_A、TOPIC_B均為4條Queue,在進行消費分配時,最終Queue的分配結果如下:

圖片

(表2.1  訊息費者的Queue分配結果表)

因為clientB沒有訂閱TOPIC_A,clientA也沒有訂閱TOPIC_B,所以TOPIA_A中的Queue-2、Queue-3,TOPIC_B中的Queue-0、Queue-1中的訊息則無法正常消費。而且在實際過程中,clientA也沒法正常消費TOPIC_B的訊息,這時會在客戶端看到很多異常,有些Queue的訊息得不到消費產生堆積。

此外,在訂閱關係中,不同client所訂閱的Tag不一樣時,也會發生拉取的訊息與所需要訂閱的訊息不匹配的問題。

三、業界MQ灰度方案

圖片

(圖3.1  灰度呼叫示意圖)

通常,業務灰度只嚴格地保證RPC服務之間的呼叫,部分訊息灰度流量的流失或錯誤是可以容忍的,如圖3-1所示,V_BFF產生的灰度訊息會被V_TRADE的正常版本與灰度版本收到並隨機消費,導致部分灰度流量沒有進入期望的環境,但整體RPC服務的呼叫還是隔離了灰度與非灰度環境。當業務對訊息消費的邏輯進行了更改,或者不想讓灰度的訊息影響線上的資料時,MQ的灰度就必須要實現。

由於訂閱關係的限制,當前的業界實現的MQ灰度方案都是正常版本與灰度版本使用不同的GroupID來實現。以下的方案均使用了不同的GroupID。

3.1 影子Topic的方案

新建一系列新的Topic來處理隔離灰度的訊息。例如對於TOPIC_ORDER會建立TOPIC_ORDER_GRAY來給灰度環境使用。

傳送方在傳送時,對灰度的訊息寫入到影子Topic中。消費時,灰度環境只使用灰度的分組訂閱灰度的Topic。

3.2 Tag的方案

傳送方在傳送時,對灰度環境產生的訊息的Tag加註灰度標識。消費方,每次灰度版本釋出時只訂閱灰度Tag的訊息,正常的版本訂閱非灰度的Tag。

3.3 UserProperty的方案

傳送方在傳送時,對灰度環境產生的訊息的UserProperty加註灰度標識。消費方的客戶端需要進行改寫,根據UserProperty來過濾,正常的版本會跳過這類訊息,灰度的版本會處理灰度訊息。

3.4 當前的方案缺陷

以上三種方案各自的優勢在這裡不做比較,但都存在以下共同的缺陷(也有其它的缺陷或開發訴求,但不致命),無法真正實現灰度狀態切換回正常狀態時訊息不丟失處理,導致整個灰度方案都是從入門到放棄的過程:

  • 因為使用不同的消費組,那麼灰度版本驗證通過後,如何正確地銜接回原正常版本的消費組的消費位移,做到高效地不丟失資訊處理呢?

  • 灰度的訊息如何確保準確地消費完畢,做到落在灰度標識的訊息做到高效地不丟失資訊處理呢?

  • 開啟灰度時,灰度訊息的位點從那裡開始?狀態的細節化如何管控?

四、魯班MQ平臺的灰度方案

本質上,MQ灰度問題的核心就是高效地將灰度與非灰度的訊息隔離開,消費方按照自己的需求來準確獲取到對應版本的訊息;當灰度完成後,能夠正確地拼接回來訊息的位移,做到不丟失處理必要的訊息,也就是狀態細節上的管理。為了實現這個目的,本方案分別在以下幾點進行了改造。

本方案中涉及到的程式碼為測試程式碼,主要用於說明方案,實際程式碼會更精細處理。

4.1 Queue的隔離使用

圖片

(圖4.1  Queue的區分使用)

我們已經知道了Queue是topic的實際執行單元,我們的思路就是藉助Queue來實現v1(正常)訊息、v2(灰度)訊息的區分,我們可以固定首尾兩條【可配置】Queue專門用來傳送與接收灰度的訊息,其餘的Queue用來傳送正常的線上訊息。我們使用相同的消費組(也就是和業界的通用方案不一樣,我們會使用相同的GroupID),讓灰度消費者參與灰度Queue的重平衡,非灰度消費者參與非灰度Queue的重平衡。

這裡我們解決了訊息的儲存隔離問題。

4.2 Broker訂閱關係改造

灰度版本往往需要變更Topic或Tag,由於我們沒有新增獨立的灰度消費組,當灰度版本變更Topic/Tag時,消費組內訂閱關係就會不一致,前文也簡單解釋了訂閱關係一致性的原理,我們需要在Broker做出對應的改造,來相容灰度與非灰度訂閱關係不一致的情況。

同一消費組的訂閱資訊會在維護在ConsumerGroupInfo的subscriptionTable中,可以在ConsumerGroupInfo中增加建立一份graySubscriptionTable用來儲存灰度版本的訂閱資訊,客戶端向Broker傳送的心跳包會改造成帶有自身的灰度標記grayFlag,根據灰度標記grayFlag來選擇訂閱關係儲存在subscriptionTable還是graySubscriptionTable;在拉取訊息時,同樣向Broker傳來grayFlag來選擇從subscriptionTable還是graySubscriptionTable中獲取對應的訂閱資訊。

這裡我們解決了消費訂閱一致性問題。

4.3 Producer的改造

傳送方的改造相對簡潔,只需確定傳送的訊息是否為灰度訊息,通過實現MessageQueueSelector介面,將灰度訊息投遞到指定數量的灰度Queue即可。這裡我們把用於灰度的grayQueueSize定義到配置中心中去,當前更多是約定使用Broker的指定Queue號作為灰度使用。

TOPIC_V_ORDER共有6條Queue,如圖4.2所示,灰度訊息只會傳送至首尾的0號與5號Queue,非灰度訊息則會選擇其餘的4條Queue傳送訊息。

圖片

圖片

(圖4.2  傳送結果)

這裡我們解決了生產者正確投遞的問題。

4.4 Consumer的改造

消費方涉及的改造點主要是灰度Queue與非灰度Queue的重平衡分配策略,與各個客戶端灰度標記grayFlag的更新與同步。

灰度重平衡策略的核心就是分類處理灰度和非灰度的Queue,要將灰度的Queue分配至灰度ClientID,將非灰度的Queue分配至非灰度的ClientID,因此,在重平衡之前,會通過Namesrv獲取同組內的所有客戶端clientId對應最新的grayFlag(也就是狀態會記錄到Namesrv)。

當灰度版本需要變更為線上版本時,各客戶端會同步grayFlag到Namesrv,同時,為了避免灰度訊息還未消費完成,在更新grayFlag之前會先判斷灰度Queue中是否存在未消費的訊息,在保證灰度訊息消費完成後才會進行grayFlag的更新。

消費者需使用AllocateMessageQueueGray作為重平衡策略,傳入灰度Queue的數量,灰度消費者setGrayFlag為true,可以看出只消費了首尾的0號與5號Queue的訊息,非灰度消費者setGrayFlag為false,可以看出只會消費中間的4條Queue的訊息,在控制檯也可以非常清晰的看到Queue的分配結果,grayFlag為true的v2客戶端分配到了首尾的Queue,grayFlag為false的v1客戶端則分配到了中間的4條Queue。

圖片

圖片

圖片

(圖4.3  消費與訂閱結果)

當灰度版本需要切換至線上版本時,只需呼叫updateClientGrayFlag來更新狀態即可,可以看出在呼叫updateClientGrayFlag後,原先v2的兩個灰度客戶端在消費完灰度Queue的訊息後,grayFlag才真正變為false【狀態在namesrv儲存】,加入到中間的4條非灰度Queue的重平衡中,原先首尾的2條灰度Queue則沒有消費者訂閱。

圖片

圖片

圖片

(圖4.4  grayFlag更新)

這裡我們解決了狀態切換的細節控制處理問題。

4.5 Namesrv的改造

前文提到過,消費者在重平衡時是需要獲取組內所有客戶端的灰度標識grayFlag,因此,我們需要一個地方來持久化儲存這些grayFlag,這個地方是每個消費者都可以訪問的,我們選擇將這些資訊儲存在Namesrv。

  • Namesrv相對比較輕量,穩定性很好;

  • 消費者本身就會與Namesrv建立長連線,如果該namesrv掛掉,消費者會自動連線下一個Namesrv,直到有可用連線為止;

  • Broker是實際儲存訊息的地方,自身執行壓力就相對較大,用來做灰度資料的同步一定程度上會加大Broker的壓力。

但是Namesrv本身是無狀態的節點,節點之間是不會進行資訊同步的,灰度資料的一致性需要藉助資料庫來保證,Namesrv共同訪問同一套資料庫就好了,資料庫持久化儲存灰度資訊,每次更新v1、v2的灰度狀態時,通過Namesrv修改資料庫的資料,在每次重平衡之前,再通過Namesrv拉取自己消費組內的所有例項的灰度狀態即可。

圖片

(圖4.5  Namesrv儲存灰度資料示意圖)

這裡我們解決了狀態儲存與同步的問題。

五、灰度場景的校驗

測試是校驗方案可行性的真理,下面用簡單的demo來驗證魯班平臺的MQ灰度方案。

5.1 灰度版本Topic & Tag不變

這種場景在4.3、4.4時已經做了驗證,不再贅述。

5.2 灰度版本Topic增加

假設v1、v2的訂閱資訊如表5.1所示,則Topic訂閱結果如圖5.1所示,TOPIC_V_ORDER被v1、v2同時訂閱,首尾兩條Queue分配給灰度v2的客戶端,中間4條Queue則分配給非灰度v1的客戶端;TOPIC_V_PAYMENT只被灰度版本v2訂閱,則只會將首尾兩條Queue分配給v2的客戶端,其餘四條Queue不會被客戶端訂閱。我們向TOPIC_V_ORDER分別傳送4條非灰度訊息和灰度訊息,向TOPIC_V_PAYMENT傳送4條灰度訊息,從圖5.2中可以看出TOPIC_V_ORDER中的非灰度訊息由v1的兩個客戶端成功消費,TOPIC_V_ORDER與TOPIC_V_PAYMENT的灰度訊息則由v2的兩個客戶端成功消費。

圖片

(表5.1  訂閱資訊表)

圖片

(圖5.1  訂閱結果)

圖片

(圖5.2  消費結果)

5.3 灰度版本Topic減少

假設v1、v2的訂閱資訊如表5.2所示,則Topic訂閱結果如圖5.3所示,TOPIC_V_ORDER被v1、v2同時訂閱,首尾兩條Queue分配給灰度v2的客戶端,中間4條Queue則分配給非灰度v1的客戶端;TOPIC_V_PAYMENT只被非灰度版本v1訂閱,則只會將中間的四條Queue分配給v1的客戶端,首尾兩條Queue不會被客戶端訂閱。我們向TOPIC_V_ORDER分別傳送4條非灰度訊息和灰度訊息,向TOPIC_V_PAYMENT傳送4條非灰度訊息,從圖5.4中可以看出TOPIC_V_ORDER與TOPIC_V_PAYMENT的非灰度訊息由v1的兩個客戶端成功消費,TOPIC_V_ORDER中的灰度訊息則由v2的兩個客戶端成功消費

圖片

(表5.2  訂閱資訊表)

圖片

(圖5.3  訂閱結果)

圖片

(圖5.4  消費結果)

5.4 灰度版本Tag變化

假設v1、v2的訂閱資訊如表5.3所示,則Topic訂閱結果如圖5.5所示,TOPIC_V_ORDER被v1、v2同時訂閱,首尾兩條Queue分配給灰度v2的客戶端,中間4條Queue則分配給非灰度v1的客戶端,我們向TOPIC_V_ORDER分別傳送4條Tag=v1的非灰度訊息和Tag=v2的灰度訊息,從圖5.6中可以看出Tag為v1的非灰度訊息由v1的兩個客戶端成功消費,Tag為v2的灰度訊息則由v2的兩個客戶端成功消費

圖片

(表5.3  訂閱資訊表)

圖片

(圖5.5  訂閱結果)

圖片

(圖5.6  消費結果)

5.5 灰度版本Topic & Tag混合變化

假設v1、v2的訂閱資訊如表5.4所示,則Topic訂閱結果如圖5.7所示,與5.2情況相同不再贅述。我們向TOPIC_V_ORDER分別傳送4條Tag=v1的非灰度訊息和Tag=v2的灰度訊息,向TOPIC_V_PAYMENT傳送4條灰度訊息,消費結果如圖5.8所示,可以看出v2的兩個客戶端成功消費了TOPIC_V_PAYMENT及TOPIC_V_ORDER中Tag=v2的灰度訊息,而v1的兩個客戶端則只消費了TOPIC_V_ORDER中Tag=v1的非灰度訊息

圖片

(表5.4  訂閱資訊表)

圖片

(圖5.7  訂閱結果)

圖片

(圖5.8  消費結果)

六、結語

實際的MQ灰度版本,我們還對MQ的傳送與消費方做了統一的封裝,業務方只需配置graySwitch、grayFlag即可,graySwtich標記是否需要開啟灰度訊息,在graySwitch開啟的前提下,grayFlag才會生效,用來標記當前客戶端是否為灰度客戶端。

在多系統互動時,業務系統可通過開關graySwitch來控制是否全量消費其他系統的灰度與非灰度訊息,通過grayFlag來控制是單獨消費灰度訊息還是非灰度訊息。graySwitch、grayFlag引數可放在配置中心做到熱生效,當需要切換灰度流量時,可開發相應的指令碼統一化更改grayFlag,實現全鏈路灰度流量的無損切換。

另外,我們對於切換狀態藉助Namesrv做了充分細節上的控制,保證在真正執行切換前,未消費完的訊息會被消費完畢才真正的執行切換。

在此,也非常感謝阿里開源RocketMQ這個訊息中介軟體!

作者:vivo流程IT團隊-Ou Erli、Xiong Huanxin

相關文章