基于 Istio 的全鏈路灰度方案探索和實(shí)踐
背景
微服務(wù)軟件架構(gòu)下,業(yè)務(wù)新功能上線前搭建完整的一套測試系統(tǒng)進(jìn)行驗(yàn)證是相當(dāng)費(fèi)人費(fèi)時(shí)的事,隨著所拆分出微服務(wù)數(shù)量的不斷增大其難度也愈大。這一整套測試系統(tǒng)所需付出的機(jī)器成本往往也不低,為了保證應(yīng)用新版本上線前的功能正確性驗(yàn)證效率,這套系統(tǒng)還必須一直單獨(dú)維護(hù)好。當(dāng)業(yè)務(wù)變得龐大且復(fù)雜時(shí),往往還得準(zhǔn)備多套,這是整個(gè)行業(yè)共同面臨且難解的成本和效率挑戰(zhàn)。如果能在同一套生產(chǎn)系統(tǒng)中完成新版本上線前的功能驗(yàn)證的話,所節(jié)約的人力和財(cái)力是相當(dāng)可觀的。
除了開發(fā)階段的功能驗(yàn)證,生產(chǎn)環(huán)境中引入灰度發(fā)布才能更好地控制新版本軟件上線的風(fēng)險(xiǎn)和爆炸半徑。灰度發(fā)布是將具有一定特征或者比例的生產(chǎn)流量分配到需要被驗(yàn)證的服務(wù)版本中,以觀察新版本上線后的運(yùn)行狀態(tài)是否符合預(yù)期。
阿里云 ASM Pro(相關(guān)鏈接請(qǐng)見文末)基于 Service Mesh 所構(gòu)建的全鏈路灰度方案,能很好幫助解決以上兩個(gè)場景的問題。
ASM Pro 產(chǎn)品功能架構(gòu)圖:
核心能力使用的就是上圖擴(kuò)展的流量打標(biāo)和按標(biāo)路由以及流量 Fallback 的能力,下面詳細(xì)介紹說明。
場景說明
全鏈路灰度發(fā)布的常見場景如下:
以 Bookinfo 為例,入口流量會(huì)帶上期望的 tag 分組,sidecar 通過獲取請(qǐng)求上下文(Header 或 Context) 中的期望 tag,將流量路由分發(fā)到對(duì)應(yīng) tag 分組,若對(duì)應(yīng) tag 分組不存在,默認(rèn)會(huì) fallback 路由到 base 分組,具體 fallback 策略可配置。接下來詳細(xì)描述具體的實(shí)現(xiàn)細(xì)節(jié)。
入口流量的 tag 標(biāo)簽,一般是在網(wǎng)關(guān)層面基于類似 tag 插件的方式,將請(qǐng)求流量進(jìn)行打標(biāo)。 比如將 userid 處于一定范圍的打上代表灰度的 tag,考慮到實(shí)際環(huán)境網(wǎng)關(guān)的選擇和實(shí)現(xiàn)的多樣性,網(wǎng)關(guān)這塊實(shí)現(xiàn)不在本文討論的范圍內(nèi)。
下面我們著重討論基于 ASM Pro 如何做到全鏈路流量打標(biāo)和實(shí)現(xiàn)全鏈路灰度。
實(shí)現(xiàn)原理
Inbound 是指請(qǐng)求發(fā)到 App 的入口流量,Outbond 是指 App 向外發(fā)起請(qǐng)求的出口流量。
上圖是一個(gè)業(yè)務(wù)應(yīng)用在開啟 mesh 后典型流量路徑:業(yè)務(wù) App 接收到一個(gè)外部請(qǐng)求 p1,接著調(diào)用背后所依賴的另一個(gè)服務(wù)的接口。此時(shí),請(qǐng)求的流量路徑是 p1->p2->p3->p4,其中 p2 是 Sidecar 對(duì) p1 的轉(zhuǎn)發(fā),p4 是 Sidecar 對(duì) p3 的轉(zhuǎn)發(fā)。為了實(shí)現(xiàn)全鏈路灰度,p3 和 p4 都需要獲取到 p1 進(jìn)來的流量標(biāo)簽,才能將請(qǐng)求路由到標(biāo)簽所對(duì)應(yīng)的后端服務(wù)實(shí)例,且 p3 和 p4 也要帶上同樣的標(biāo)簽。關(guān)鍵在于,如何讓標(biāo)簽的傳遞對(duì)于應(yīng)用完全無感,從而實(shí)現(xiàn)全鏈路的標(biāo)簽透傳,這是全鏈路灰度的關(guān)鍵技術(shù)。ASM Pro 的實(shí)現(xiàn)是基于分布式鏈路追蹤技術(shù)(比如,OpenTracing、OpenTelemetry 等)中的 traceId 來實(shí)現(xiàn)這一功能。
在分布式鏈路追蹤技術(shù)中,traceId 被用于唯一地標(biāo)識(shí)一個(gè)完整的調(diào)用鏈,鏈路上的每一個(gè)應(yīng)用所發(fā)出的扇出(fanout)調(diào)用,都會(huì)通過分布式鏈路追蹤的 SDK 將源頭的 traceId 給帶上。ASM Pro 全鏈路灰度解決方案的實(shí)現(xiàn)正是建立在這一分布式應(yīng)用架構(gòu)所廣泛采納的實(shí)踐之上的。
上圖中,Sidecar 本來所看到的 inbound 和 outbound 流量是完全獨(dú)立的,無法感知兩者的對(duì)應(yīng)關(guān)系,也不清楚一個(gè) inbound 請(qǐng)求是否導(dǎo)致了多個(gè) outbound 請(qǐng)求的發(fā)生。換句話說,圖中 p1 和 p3 兩個(gè)請(qǐng)求之間是否有對(duì)應(yīng)關(guān)系 Sidecar 并不知情。
在 ASM Pro 全鏈路灰度解決方案中,通過 traceId 將 p1 和 p3 兩個(gè)請(qǐng)求做關(guān)聯(lián),具體說來依賴了 Sidecar 中的 x-request-id 這個(gè) trace header。Sidecar 內(nèi)部維護(hù)了一張映射表,其中記錄了 traceId 和標(biāo)簽的對(duì)應(yīng)關(guān)系。當(dāng) Sidecar 收到 p1 請(qǐng)求時(shí),將請(qǐng)求中的 traceId 和標(biāo)簽存儲(chǔ)到這張表中。當(dāng)收到 p3 請(qǐng)求時(shí),從映射表中查詢獲得 traceId 所對(duì)應(yīng)的標(biāo)簽并將這一標(biāo)簽加入到 p4 請(qǐng)求中,從而實(shí)現(xiàn)全鏈路的打標(biāo)和按標(biāo)路由。下圖大致示例了這一實(shí)現(xiàn)原理。
換句話說,ASM Pro 的全鏈路灰度功能需要應(yīng)用使用分布式鏈路追蹤技術(shù)。如果想運(yùn)用這一技術(shù)的應(yīng)用沒有使用分布式鏈路追蹤技術(shù)的話不可避免地涉及到一定的改造工作。對(duì)于 Java 應(yīng)用來說,仍可以考慮采用 Java Agent 以 AOP 的方式讓業(yè)務(wù)無需改造地實(shí)現(xiàn) traceId 在 inbound 和 outbound 之間透傳。
實(shí)現(xiàn)流量打標(biāo)
ASM Pro 中引入了全新的 TrafficLabel CRD 用于定義 Sidecar 所需透傳的流量標(biāo)簽從哪里獲取。下面所例舉的 YAML 文件中,定義了流量標(biāo)簽來源和需要將標(biāo)簽存儲(chǔ) OpenTracing 中(具體是 x-trace 頭)。其中流量標(biāo)的名為 trafficLabel,取值依次從 $getContext(x-request-id) 到最后從本地環(huán)境的$(localLabel)中獲取。
- apiVersion: istio.alibabacloud.com/v1beta1kind: TrafficLabelmetadata: name: defaultspec: rules: - labels: - name: trafficLabel valueFrom: - $getContext(x-request-id) //若使用aliyun arms,對(duì)應(yīng)為x-b3-traceid - $(localLabel) attachTo: - opentracing # 表示生效的協(xié)議,空為都不生效,*為都生效 protocols: "*"
CR 定義包含兩塊,即標(biāo)簽的獲取和存儲(chǔ)。
獲取邏輯:先根據(jù)協(xié)議上下文或者頭(Header 部分)中的定義的字段獲取流量標(biāo)簽,如果沒有,會(huì)根據(jù) traceId 通過 Sidecar 本地記錄的 map 獲取, 該 map 表中保存了 traceId 對(duì)應(yīng)流量標(biāo)識(shí)的映射。若 map 表中找到對(duì)應(yīng)映射,會(huì)將該流量打上對(duì)應(yīng)的流量標(biāo),若獲取不到,會(huì)將流量標(biāo)取值為本地部署對(duì)應(yīng)環(huán)境的 localLabel。localLabel 對(duì)應(yīng)本地部署的關(guān)聯(lián) label,label 名為 ASM_TRAFFIC_TAG。
本地部署對(duì)應(yīng)環(huán)境的標(biāo)簽名為"ASM_TRAFFIC_TAG",實(shí)際部署可以結(jié)合 CI/CD 系統(tǒng)來關(guān)聯(lián)。
存儲(chǔ)邏輯:attachTo 指定存儲(chǔ)在協(xié)議上下文的對(duì)應(yīng)字段,比如 HTTP 對(duì)應(yīng) Header 字段,Dubbo 對(duì)應(yīng) rpc context 部分,具體存儲(chǔ)到哪一個(gè)字段中可配置。
有了TrafficLabel 的定義,我們知道如何將流量打標(biāo)和傳遞標(biāo)簽,但光有這個(gè)還不足以做到全鏈路灰度,我們還需要一個(gè)可以基于 trafficLabel 流量標(biāo)識(shí)來做路由的功能,也就是“按標(biāo)路由”,以及路由 fallback 等邏輯,以便當(dāng)路由的目的地不存在時(shí),可以實(shí)現(xiàn)降級(jí)的功能。
按流量標(biāo)簽路由
這一功能的實(shí)現(xiàn)擴(kuò)展了 Istio 的 VirtualService 和 DestinationRule。
在 DestinationRule 中定義 Subset
自定義分組 subset 對(duì)應(yīng)的是 trafficLabel 的 value
- apiVersion: networking.istio.io/v1alpha3kind: DestinationRulemetadata: name: myappspec: host: myapp/* subsets: - name: myproject # 項(xiàng)目環(huán)境 labels: env: abc - name: isolation # 隔離環(huán)境 labels: env: xxx # 機(jī)器分組 - name: testing-trunk # 主干環(huán)境 labels: env: yyy - name: testing # 日常環(huán)境 labels: env: zzz---apiVersion: networking.istio.io/v1alpha3kind: ServiceEntrymetadata: name: myappspec: hosts: - myapp/* ports: - number: 12200 name: http protocol: HTTP endpoints: - address: 0.0.0.0 labels: env: abc - address: 1.1.1.1 labels: env: xxx - address: 2.2.2.2 labels: env: zzz - address: 3.3.3.3 labels: env: yyy
Subset 支持兩種指定形式:
labels 用于匹配應(yīng)用中帶特定標(biāo)記的節(jié)點(diǎn)(endpoint);
通過 ServiceEntry 用于指定屬于特定 subset 的 IP 地址,注意這種方式與labels指定邏輯不同,它們可以不是從注冊中心(K8s 或者其他)拿到的地址,直接通過配置的方式指定。適用于 Mock 環(huán)境,這個(gè)環(huán)境下的節(jié)點(diǎn)并沒有向服務(wù)注冊中心注冊。
在 VirtualService 中基于 subset
1)全局默認(rèn)配置
route 部分可以按順序指定多個(gè) destination,多個(gè) destination 之間按照 weight 值的比例來分配流量。
每個(gè) destination 下可以指定 fallback 策略,case 標(biāo)識(shí)在什么情況下執(zhí)行 fallback,取值:noinstances(無服務(wù)資源)、noavailabled(有服務(wù)資源但是服務(wù)不可用),target 指定 fallback 的目標(biāo)環(huán)境。如果不指定 fallback,則強(qiáng)制在該 destination 的環(huán)境下執(zhí)行。
按標(biāo)路由邏輯,我們通過改造 VirtualService,讓 subset 支持占位符 $trafficLabel, 該占位符 $trafficLabel 表示從請(qǐng)求流量標(biāo)中獲取目標(biāo)環(huán)境, 對(duì)應(yīng) TrafficLabel CR 中的定義。
全局默認(rèn)模式對(duì)應(yīng)泳道,也就是單個(gè)環(huán)境內(nèi)封閉,同時(shí)指定了環(huán)境級(jí)別的 fallback 策略。自定義分組 subset 對(duì)應(yīng)的是 trafficLabel 的 value
配置樣例如下:
- apiVersion: networking.istio.io/v1alpha3kind: VirtualServicemetadata: name: default-routespec: hosts: # 對(duì)所有應(yīng)用生效 - */* http: - name: default-route route: - destination: subset: $trafficLabel weight: 100 fallback: case: noinstances target: testing-trunk - destination: host: */* subset: testing-trunk # 主干環(huán)境 weight: 0 fallback: case: noavailabled target: testing - destination: subset: testing # 日常環(huán)境 weight: 0 fallback: case: noavailabled target: mock - destination: host: */* subset: mock # Mock中心 weight: 0
2)個(gè)人開發(fā)環(huán)境定制
先打到日常環(huán)境,當(dāng)日常環(huán)境沒有服務(wù)資源時(shí),再打到主干環(huán)境。
- apiVersion: networking.istio.io/v1alpha3kind: VirtualServicemetadata: name: projectx-routespec: hosts: # 只對(duì)myapp生效 - myapp/* http: - name: dev-x-route match: trafficLabel: - exact: dev-x # dev環(huán)境: x route: - destination: host: myapp/* subset: testing # 日常環(huán)境 weight: 100 fallback: case: noinstances target: testing-trunk - destination: host: myapp/* subset: testing-trunk # 主干環(huán)境 weight: 0
3) 支持權(quán)重配置
將打了主干環(huán)境標(biāo)并且本機(jī)環(huán)境是 dev-x 的流量,80% 打到主干環(huán)境,20% 打到日常環(huán)境。當(dāng)主干環(huán)境沒有可用的服務(wù)資源時(shí),流量打到日常。
sourceLabels 為本地 workload 對(duì)應(yīng)的 label
- apiVersion: networking.istio.io/v1alpha3kind: VirtualServicemetadata: name: dev-x-routespec: hosts: # 對(duì)哪些應(yīng)用生效(不支持多應(yīng)用配置) - myapp/* http: - name: dev-x-route match: trafficLabel: - exact: testing-trunk # 主干環(huán)境標(biāo) sourceLabels: - exact: dev-x # 流量來自某個(gè)項(xiàng)目環(huán)境 route: - destination: host: myapp/* subset: testing-trunk # 80%流量打向主干環(huán)境 weight: 80 fallback: case: noavailabled target: testing - destination: host: myapp/* subset: testing # 20%流量打向日常環(huán)境 weight: 20
按(環(huán)境)標(biāo)路由
該方案依賴業(yè)務(wù)部署應(yīng)用時(shí)帶上相關(guān)標(biāo)識(shí)(例子中對(duì)應(yīng) label 為 ASM_TRAFFIC_TAG: xxx),常見為環(huán)境標(biāo)識(shí),標(biāo)識(shí)可以理解是服務(wù)部署的相關(guān)元信息,這個(gè)依賴上游部署系統(tǒng) CI/CD 系統(tǒng)的串聯(lián),大概示意圖如下:
K8s 場景,通過業(yè)務(wù)部署時(shí)自動(dòng)帶上對(duì)應(yīng)環(huán)境/分組 label 標(biāo)識(shí)即可,也就是采用K8s 本身作為元數(shù)據(jù)管理中心。
非 K8s 場景,可以通過微服務(wù)已集成的服務(wù)注冊中心或者元數(shù)據(jù)配置管理服務(wù)(metadata server)來集成實(shí)現(xiàn)。
注:ASM Pro 自研開發(fā)了ServiceDiretory 組件(可以參看 ASM Pro 產(chǎn)品功能架構(gòu)圖),實(shí)現(xiàn)了多注冊中心對(duì)接以及部署元信息的動(dòng)態(tài)獲??;
應(yīng)用場景延伸
下面是典型的一個(gè)基于流量打標(biāo)和按標(biāo)路由實(shí)現(xiàn)的多套開發(fā)環(huán)境治理功能;每個(gè)開發(fā)者對(duì)應(yīng)的 Dev X 環(huán)境只需部署有版本更新的服務(wù)即可;如果需要和其他開發(fā)者聯(lián)調(diào),可以通過配置 fallback 將服務(wù)請(qǐng)求 fallback 流轉(zhuǎn)到對(duì)應(yīng)開發(fā)環(huán)境即可。如下圖的 Dev Y 環(huán)境的B -> Dev X 環(huán)境的 C。
同理,將 Dev X 環(huán)境等同于線上灰度版本環(huán)境也是可以的,對(duì)應(yīng)可以解決線上環(huán)境的全鏈路灰度發(fā)布問題。
總結(jié)
本文介紹的基于“流量打標(biāo)”和“按標(biāo)路由” 能力是一個(gè)通用方案,基于此可以較好地解決測試環(huán)境治理、線上全鏈路灰度發(fā)布等相關(guān)問題,基于服務(wù)網(wǎng)格技術(shù)做到與開發(fā)語言無關(guān)。同時(shí),該方案適應(yīng)于不同的7層協(xié)議,當(dāng)前已支持 HTTP/gRpc 和 Dubbo 協(xié)議。
對(duì)應(yīng)全鏈路灰度,其他廠商也有一些方案,對(duì)比其他方案 ASM Pro 的解決方案的優(yōu)點(diǎn)是:
- 支持多語言、多協(xié)議。
- 統(tǒng)一配置模板 TrafficLabel, 配置簡單且靈活,支持多級(jí)別的配置(全局、namespace 、pod 級(jí)別)。
- 支持路由 fallback 實(shí)現(xiàn)降級(jí)。
基于“流量打標(biāo)” 和 “按標(biāo)路由”能力還可以用于其他相關(guān)場景:
- 大促前的性能壓測。在線上壓測的場景中,為了讓壓測數(shù)據(jù)和正式的線上數(shù)據(jù)實(shí)現(xiàn)隔離,常用的方法是對(duì)于消息隊(duì)列,緩存,數(shù)據(jù)庫使用影子的方式。這就需要流量打標(biāo)的技術(shù),通過 tag 區(qū)分請(qǐng)求是測試流量還是生產(chǎn)流量。當(dāng)然,這需要 Sidecar 對(duì)中間件比如 Redis、RocketMQ 等進(jìn)行支持。
- 單元化路由。常見的單元化路由場景,可能是需要根據(jù)請(qǐng)求流量中的某些元信息比如 uid,然后通過配置得出對(duì)應(yīng)所屬的單元。在這個(gè)場景中,我們可以通過擴(kuò)展 TrafficLabel 定義獲取“單元標(biāo)”的函數(shù)來給流量打上“單元標(biāo)”,然后基于“單元標(biāo)”將流量路由到對(duì)應(yīng)的服務(wù)單元。