vivo 消息中間件測試環(huán)境項目多版本實(shí)踐
一、背景
在2022年8月份 vivo 互聯(lián)網(wǎng)中間件團(tuán)隊完成了互聯(lián)網(wǎng)在線業(yè)務(wù)的MQ引擎升級,從RabbitMQ 到 RocketMQ 的平滑升級替換。
在業(yè)務(wù)使用消息中間件的過程中,提出了開發(fā)測試環(huán)境項目多版本隔離的訴求。本文將介紹我們基于 RocketMQ 如何實(shí)現(xiàn)的多版本環(huán)境隔離。
二、消息中間件平臺主體架構(gòu)
在正式展開項目多版本實(shí)踐之前,先大致介紹下我們消息中間件平臺的主體架構(gòu)。
由上圖可知,我們消息中間件平臺的核心組件 mq-meta、RabbitMQ-SDK、mq-proxy,以及RocketMQ集群。
1. mq-meta
主要負(fù)責(zé)平臺元數(shù)據(jù)管理,以及業(yè)務(wù)SDK啟動時的鑒權(quán)尋址操作。
業(yè)務(wù)進(jìn)行topic申請時,會自動分配創(chuàng)建到兩個不同機(jī)房的broker上。
鑒權(quán)尋址時會根據(jù)業(yè)務(wù)接入Key找到所在 MQ 集群下的proxy節(jié)點(diǎn)列表,經(jīng)過機(jī)房優(yōu)先+分片選取+負(fù)載均衡等策略,下發(fā)業(yè)務(wù)對應(yīng)的proxy節(jié)點(diǎn)列表。
2. RabbitMQ-SDK
目前業(yè)務(wù)使用的消息中間件SDK仍為原有自研的RabibitMQ SDK,通過AMQP協(xié)議收發(fā)消息。
與proxy之間的生產(chǎn)消費(fèi)連接,遵循機(jī)房優(yōu)先原則,同時亦可以人為指定優(yōu)先機(jī)房策略。
3. mq-proxy
消息網(wǎng)關(guān)組件,負(fù)責(zé)AMQP協(xié)議與RocketMQ Remoting協(xié)議之間的相互轉(zhuǎn)換,對于業(yè)務(wù)側(cè)目前僅開放了AMQP協(xié)議。
具備讀寫分離能力,可配置只代理生產(chǎn)、只代理消費(fèi)、代理生產(chǎn)消費(fèi)這三種角色。
與broker之間的生產(chǎn)消費(fèi),遵循機(jī)房優(yōu)先原則。
機(jī)房優(yōu)先的實(shí)現(xiàn):
- 生產(chǎn):proxy優(yōu)先將消息發(fā)送到自己本機(jī)房的broker,只有在發(fā)送失敗降級時,才會將消息發(fā)送到其他機(jī)房broker;通過擴(kuò)展MQFaultStrategy+LatencyFaultTolerance,并結(jié)合快手負(fù)載均衡組件simple-failover-java實(shí)現(xiàn)機(jī)房優(yōu)先+機(jī)房級別容災(zāi)的負(fù)載均衡策略。
- 消費(fèi):在進(jìn)行隊列分配時,先輪詢分配自己機(jī)房的隊列;再將不存在任何消費(fèi)的機(jī)房隊列,進(jìn)行輪詢分配。通過擴(kuò)展AllocateMessageQueueStrategy實(shí)現(xiàn)。
4. RocketMQ集群
每個MQ集群會由多個機(jī)房的broker組成。
每個topic則至少會分配到兩個不同機(jī)房的broker上。實(shí)現(xiàn)業(yè)務(wù)消息發(fā)送與消費(fèi)的機(jī)房級別的容災(zāi)。
每個broker部署兩節(jié)點(diǎn),采用主從架構(gòu)部署,并基于zookeeper實(shí)現(xiàn)了一套自動主從切換的高可用機(jī)制。通過異步刷盤+同步雙寫來保證性能與消息的可靠性。
namesrv則為跨機(jī)房broker+mq-proxy之間的公共組件,為集群提供路由發(fā)現(xiàn)功能。
三、項目多版本實(shí)踐
3.1 現(xiàn)狀
后端服務(wù)通常采用微服務(wù)架構(gòu),各服務(wù)之間的通信,通常是同步與異步兩種調(diào)用場景。其中同步是通過RPC調(diào)用完成,而異步則是通過MQ(RocketMQ)生產(chǎn)消費(fèi)消息實(shí)現(xiàn)。
在多版本環(huán)境隔離中,同步調(diào)用場景,一些RPC框架都能有比較好的支持(如Dubbo的標(biāo)簽路由);但在異步調(diào)用場景,RocketMQ并不具備完整的版本隔離方案,需要通過組合一些功能自行實(shí)現(xiàn)。
最初消息中間件平臺支持的多版本環(huán)境隔離大致如下:
- 平臺提供固定幾個MQ邏輯集群(測試01、測試02、測試03...)來支持版本隔離。
- 業(yè)務(wù)在進(jìn)行多版本的并行測試時,需關(guān)注版本環(huán)境與MQ邏輯集群的對應(yīng)關(guān)系,一個版本對應(yīng)到一個MQ邏輯集群。
- 不同MQ邏輯集群下用到的MQ資源(Topic、Group)自然就是不同的。
該方式主要存在如下兩個問題:
1、使用成本較高
- 業(yè)務(wù)需在消息中間件平臺進(jìn)行多套環(huán)境(集群)的資源申請。
- 業(yè)務(wù)在部署多版本時,每個版本服務(wù)都需要配置一份不同的MQ資源接入Key,配置過程繁瑣且容易出錯。
2、環(huán)境維護(hù)成本較高
- 在一個項目中,業(yè)務(wù)為了測試完整的業(yè)務(wù)流程,可能會涉及到多個生產(chǎn)方、消費(fèi)方服務(wù)。盡管在某次版本中只改動了生產(chǎn)方服務(wù),但仍需要在版本環(huán)境中一并部署業(yè)務(wù)流程所需的生產(chǎn)與消費(fèi)方服務(wù),增加了機(jī)器與人力資源成本。
為解決上述問題,提升多版本開發(fā)測試過程中的研發(fā)效率,中間件團(tuán)隊開始了RocketMQ多版本環(huán)境隔離方案的調(diào)研。
3.2 方案調(diào)研
注釋:
1、物理隔離:即機(jī)器層面的隔離,MQ的物理隔離,則意味著使用完全不同的MQ物理集群。
2、資源邏輯隔離:屬于同一MQ物理集群,但采用不同的邏輯集群,業(yè)務(wù)側(cè)需關(guān)注不同邏輯集群下相應(yīng)的topic和group資源配置。
3、基線版本:通常為當(dāng)前線上環(huán)境的版本或者是當(dāng)前的主開發(fā)版本,為穩(wěn)定版本。
4、項目版本:即項目并行開發(fā)中的多版本,非基線版本。
5、消息回落:針對消費(fèi)而言,若消費(fèi)方?jīng)]有對應(yīng)的項目版本,則會回落到基線版本來進(jìn)行消費(fèi)。
3.3 方案選擇
基于我們需解決的問題,并對實(shí)現(xiàn)成本與業(yè)務(wù)使用成本的綜合考量,我們僅考慮【基于消息維度的user-property】與【基于topic的messageQueue】這兩種方案。
又因在全鏈路的多版本環(huán)境隔離的需求中,業(yè)務(wù)使用的版本環(huán)境明確提出不做固定,故而我們最終選擇【基于消息維度的user-property】來作為我們多版本環(huán)境隔離的方案。
3.4 項目多版本的落地
基于消息維度的user-property來實(shí)現(xiàn)項目多版本的隔離。
1. 鏈路分析
在多版本環(huán)境中,真實(shí)的業(yè)務(wù)鏈路可能如下,服務(wù)調(diào)用可能走同步RPC或異步MQ。
注釋:
1、業(yè)務(wù)請求中帶有流量標(biāo)識,經(jīng)過網(wǎng)關(guān)時,根據(jù)流量路由規(guī)則將流量染色為全鏈路染色標(biāo)識v-traffic-lane。
2、流量標(biāo)識為userId,流量路由規(guī)則為用戶路由到指定版本,圖中的鏈路情況:
3、在后續(xù)的整個鏈路中,都需要將請求按照流量染色標(biāo)識v-traffic-lane正確路由到對應(yīng)版本環(huán)境。
2. 染色標(biāo)識傳遞
為了正確識別當(dāng)前服務(wù)所在版本,以及流量中的染色標(biāo)識進(jìn)行全鏈路傳遞,需要做如下事情:
(1)啟動
其中v-traffic-lane則是服務(wù)被拉起時所在的版本環(huán)境標(biāo)識(由CICD提供),這樣proxy就能知道這個客戶端連接屬于哪個版本。
(2)消息的發(fā)送與接收
消息發(fā)送:mq-proxy將AMQP消息轉(zhuǎn)化為RocketMQ消息時,將染色標(biāo)識添加到RocketMQ消息的user-property中。
消息接收:mq-proxy將RocketMQ消息轉(zhuǎn)化為AMQP消息時,將染色標(biāo)識再添加到AMQP消息屬性中。
注釋:
上述紅色點(diǎn)位,可通過改動SDK進(jìn)行染色標(biāo)識的傳遞,但這樣就需要業(yè)務(wù)升級SDK了。這里我們是借助調(diào)用鏈agent來統(tǒng)一實(shí)現(xiàn)。
3.生產(chǎn)消費(fèi)邏輯
(1)生產(chǎn)
邏輯比較簡單,對于存在版本tag的消息,只需要將版本標(biāo)識作為一個消息屬性,存儲到當(dāng)前topic中即可。
(2)消費(fèi)
這里其實(shí)是有兩個問題:消費(fèi)的多版本隔離、消息回落。
我們先看下消費(fèi)的多版本隔離應(yīng)該如何實(shí)現(xiàn)?
通過使用不同的消費(fèi)group,采用基于user-property的消息過濾機(jī)制來實(shí)現(xiàn)。
① 版本tag傳遞
- 在RabbitMQ-SDK消費(fèi)啟動時,通過全鏈路Agent傳遞到proxy
② 項目環(huán)境消費(fèi)【消費(fèi)屬于自己版本的消息】
- proxy會根據(jù)版本tag在MQ集群自動創(chuàng)建帶版本tag的group,并通過消費(fèi)訂閱的消息屬性過濾機(jī)制,只消費(fèi)自己版本的消息。
- routingKey的過濾則依賴proxy側(cè)的過濾來完成。相對基線版本,多版本的消息量應(yīng)該會比較少,全量拉取到proxy來做過濾,影響可控。
- 消費(fèi)組group_版本tag無需業(yè)務(wù)申請,由客戶端啟動時proxy會自動創(chuàng)建。
③ 基線消費(fèi)【消費(fèi)全部基線版本消息+不在線多版本的消息】
- 啟動時使用原始group,訂閱消費(fèi)時,基于broker的routingKey過濾機(jī)制消費(fèi)topic所有消息。
- 當(dāng)消息被拉取到proxy后,再做一次消息屬性過濾,將多版本進(jìn)行選擇性過濾,讓基線消費(fèi)到正確版本的消息。
我們再來看下消息回落又該如何實(shí)現(xiàn)?
1、消息回落是基線消費(fèi)需要根據(jù)多版本的在線情況,來決定是否需要消費(fèi)多版本的消息。
2、上面已提到基線消費(fèi)從broker是拉取所有消息進(jìn)行消費(fèi)。
3、我們通過在基線消費(fèi)內(nèi)部維護(hù)一個在線多版本tag的集合,然后進(jìn)行多版本消息的選擇性過濾來支持回落。
4、但這個在線多版本tag的集合,需要及時更新,才能更好的保證消息回落的準(zhǔn)確性。
5、起初我們采用定時任務(wù)從broker拉取所有在線多版本tag的集合,每30s拉取一次,這樣消息回落就需要30s才能生效,準(zhǔn)確性差。
6、后面我們想到用廣播通知機(jī)制,在多版本上下線時廣播通知到所有的基線消費(fèi)實(shí)例,保證了消息回落的實(shí)效性與準(zhǔn)確性。
7、完整的基線消費(fèi)實(shí)例在線多版本tag集合更新機(jī)制如下:
(3)broker側(cè)的調(diào)整
這里主要是為了配合消費(fèi)多版本的實(shí)現(xiàn),對broker進(jìn)行了一些擴(kuò)展。
1、提供在線多版本group集合的擴(kuò)展接口。用以返回當(dāng)前group所有在線的多版本group集合。
2、增加broker側(cè)多版本消息過濾機(jī)制。因RocketMQ原生sql92過濾表達(dá)式,無法支持帶點(diǎn)的屬性字段過濾;而我們的版本標(biāo)識(_vh_.v-traffic-lane)是存在的。
注釋:
1、routingKey過濾機(jī)制:為基于broker的消息過濾機(jī)制的擴(kuò)展,可實(shí)現(xiàn)RabbitMQ中的routingKey表達(dá)式相同的消息路由功能。
2、多版本生產(chǎn)消費(fèi)邏輯,都在mq-proxy與RocketMQ-broker側(cè)完成。業(yè)務(wù)也無需升級SDK。
4. 問題定位
在多版本隔離中,平臺對用戶屏蔽了復(fù)雜的實(shí)現(xiàn)細(xì)節(jié),但用戶使用時,也需要能觀測到消息的生產(chǎn)消費(fèi)情況,便于問題跟蹤定位。
這里我們主要提供了如下功能:
① 消息查詢:可觀測消息當(dāng)前的版本標(biāo)識,以及消息軌跡中的生產(chǎn)消費(fèi)情況
② 消費(fèi)group的在線節(jié)點(diǎn):可看到消費(fèi)節(jié)點(diǎn)當(dāng)前的版本標(biāo)識
四、總結(jié)與展望
本文概述了vivo互聯(lián)網(wǎng)中間件團(tuán)隊,在開源RocketMQ基礎(chǔ)之上,如何落地【測試環(huán)境項目多版本隔離】的業(yè)務(wù)訴求。其中涵蓋了vivo消息中間件主體架構(gòu)現(xiàn)狀、業(yè)內(nèi)較流行的幾種方案對比,并對我們最終選擇方案在實(shí)現(xiàn)層面進(jìn)行了細(xì)節(jié)性的分析。希望可以給業(yè)界提供一種基于proxy來實(shí)現(xiàn)多版本隔離特性的案例參考。
在實(shí)現(xiàn)過程中遇到的問題點(diǎn)歸結(jié)下來則是:
1. 流量染色標(biāo)識在整個生產(chǎn)消費(fèi)過程中如何傳遞?
- 在客戶端SDK使用全鏈路agent進(jìn)行流量染色標(biāo)識的添加、拆解、傳遞。
- 在RocketMQ則存儲到消息的user-property當(dāng)中。
2. 消費(fèi)客戶端版本標(biāo)識如何識別?
- 客戶端SDK使用全鏈路agent將版本標(biāo)識添加到連接屬性當(dāng)中。
- proxy則根據(jù)客戶端版本標(biāo)識自動創(chuàng)建多版本消費(fèi)group。
3. 消費(fèi)的多版本隔離如何實(shí)現(xiàn)?
- 項目版本,通過不同的消費(fèi)group,基于broker端消息屬性的版本過濾來實(shí)現(xiàn)隔離。
- 基線版本,則通過proxy側(cè)消費(fèi)過濾來忽略掉不需要消費(fèi)的消息。
4. 消息回落如何實(shí)現(xiàn)?如何保證消息回落的實(shí)效性與準(zhǔn)確性?
- 基線版本內(nèi)部會維護(hù)一個在線多版本消費(fèi)group的集合,根據(jù)這個集合來決定消息是否需要回落到基線進(jìn)行消費(fèi)。
- 消息回落的實(shí)效性與準(zhǔn)確性則通過定時+廣播消息的機(jī)制保證。
最后,我們實(shí)現(xiàn)的多版本隔離特性如下:
- 多版本環(huán)境隔離。在proxy層面基于消息維度user-property來實(shí)現(xiàn)版本隔離,業(yè)務(wù)不需要升級SDK,業(yè)務(wù)使用層面仍然為同一套配置資源。
- 支持消息回落。
- 消費(fèi)失敗產(chǎn)生的重試消息也能被重投遞到對應(yīng)版本。
但仍存在如下不足:
多版本消費(fèi)客戶端全部下線場景:若topic中仍存在一些已下線版本的消息沒有消費(fèi),則這部分消息不保證一定能被基線版本全部消費(fèi)到。因基線版本與項目版本實(shí)際上采用的是不同的消費(fèi)group,在broker的消費(fèi)進(jìn)度是不一致的,消息回落到基線消費(fèi)之后,其消費(fèi)位點(diǎn)可能已經(jīng)超過項目版本消費(fèi)group下線時的位點(diǎn),中間存在偏差,會導(dǎo)致這部分消息再無法被基線版本消費(fèi)到。
建議用于開發(fā)測試環(huán)境,因其無法保證多版本消息至少會被消費(fèi)一次。
未來,消息中間件也會考慮線上環(huán)境全鏈路灰度場景的支持。