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