在分布式應用的發布實踐中,全鏈路灰階發布可以通過嚴格泳道和寬鬆泳道的方式滿足絕大部分情境的需求。但是,在一些情境中,需要對多個應用進行獨立的灰階發布,同時,灰階的比例希望以使用者維度進行(而非請求維度,即同一個使用者的請求始終穩定地根據灰階比例命中到對應版本)。本文講述如何通過ASM實現按使用者比例進行多個應用的獨立灰階發布。
背景資訊
流量泳道本質上是對分布式系統中的一個版本由哪些工作群組成進行了定義,例如:

上圖中,一個分布式系統由應用A、應用B、應用C組成,其中:
由A(V1)+B(V1)+C(V1)構成的v1版本。
由A(V2)+B(V1)+C(V2)構成的v2版本。
藉助雜湊染色能力,用戶端發出的請求到達網關後,ASM網關通過雜湊染色外掛程式,對請求以使用者維度進行染色,ASM Sidecar將在整條鏈路透傳染色。這使得在任意應用向上遊發起請求時,始終按照鏈路染色進行路由,從而實現任意的流量固定進入某一泳道。對於由營運人員對整個應用進行統一發布的情境來說,ASM泳道是應對問題的最佳實務。
然而,在特定情境下,您可能希望對分布式應用系統同時對多組應用進行多個灰階發布,同時,每個或每組應用的Team Dev自行決定灰階比例,而不是由一個營運團隊或營運人員來統一操作,例如:
應用A和應用B當前穩定版本為V1,為了發布某新功能,需要對應用A、應用B發布V2版本,由於該功能改動較大,負責該新功能的專案組希望先將10%的使用者流量打到A、B的V2版本。
應用C當前穩定版本為V2,為了修正V2版本中存在的BUG,上線了V3,由於BUG改動小,且希望儘快修複,因此負責該功能的專案組希望直接將50%的使用者流量打到V3版本。

要實現以上需求,就需要讓同一調用鏈路上請求不同服務時以不同的策略進行路由,這依靠單一染色是無法做到的。ASM的雜湊打標外掛程式支援同時為請求打上多種標記,同時藉助ASMHeaderPropagation能力對指定Prefix的透傳,可以輕鬆做到對請求打上多個標記並將它們在整條調用鏈路上透傳,再利用ASM的虛擬服務對這些標記進行匹配,從而實現靈活的灰階發布。

前提條件
已建立並添加叢集到ASM執行個體,執行個體版本為1.18及以上。具體操作,請參見添加叢集到ASM執行個體。
已部署入口網關。具體操作,請參見建立入口網關。
操作步驟
步驟一:部署執行個體應用
本例的示範應用分為app-a、app-b、app-c三個應用,完整的業務調用鏈路為app-a -> app-b -> app-c。其中,app-a和app-b處於v1版本,app-c則已經發布到了v2版本。

使用以下內容建立app-init.yaml。
使用資料面叢集的kubeconfig執行以下命令,部署執行個體應用的Deployment和Service。此處, 使用命名空間default為例, 可以切換使用其他已開啟Sidecar代理注入的命名空間。
$ kubectl apply -f app-init.yaml -n default使用以下內容建立app-init-mesh.yaml。
使用控制面的kubeconfig執行以下命令,為應用和ASM網關配置虛擬服務和目標規則。
$ kubectl apply -f app-init-mesh.yaml執行以下命令,通過ASM入口網關地址,攜帶
x-user-id: 0001要求標頭對應用發起請求。請將${入口網關ip}替換為實際網關IP。關於如何擷取網關IP,請參見擷取入口網關地址。curl -H 'x-user-id: 0001' ${入口網關ip}預期輸出:
-> app-a(version: v1, ip: 10.0.250.27)-> app-b(version: v1, ip: 10.0.250.6)-> app-c(version: v2, ip: 10.0.250.11)可以看到,應用的調用鏈路為app-a v1 -> app-b v1 -> app-c v2,符合預期。
步驟二:灰階發布app-a和app-b的v2版本
為了完成使用者層級的灰階發布,我們需要:
對app-a和app-b發布v2版本,並修改app-a和app-b對應的目標規則,為v2版本建立子集
修改網關和app-b的虛擬服務,添加在請求時攜帶特定標籤的路由到v2版本的規則
對網關應用繼續雜湊值打標的外掛程式,以x-user-id要求標頭的value作為輸入進行雜湊運算,並按照比例打標。
配置ASMHeaderPropagation CRD,使得ASM Sidecar透傳所有外掛程式為請求打上的標識
實際操作順序並不是嚴格按照上述描述的順序進行,此順序只是便於理解,而實際操作時需要根據依賴關係決定操作順序。

使用以下內容建立app-ab-v2.yaml。
使用資料面叢集的kubeconfig執行以下命令,部署app-a和app-b的v2版本。
$ kubectl apply -f app-ab-v2.yaml使用以下內容建立app-ab-v2-mesh.yaml。
使用控制面的kubeconfig執行以下命令,為app-a和app-b應用對應的目標規則新增v2子集,以及為虛擬服務新增匹配打標規則的路由規則。
$ kubectl apply -f app-ab-v2-mesh.yaml使用以下內容建立header-propagation.yaml。
apiVersion: istio.alibabacloud.com/v1beta1 kind: ASMHeaderPropagation metadata: name: tag-propagation spec: headerPrefixes: - appver執行以下命令,使得Sidecar對首碼為appver的請求header進行透傳。
$ kubectl apply -f header-propagation.yaml -n default使用以下內容建立hash-tagging-plugin.yaml。
apiVersion: extensions.istio.io/v1alpha1 kind: WasmPlugin metadata: name: hash-tagging namespace: istio-system spec: imagePullPolicy: IfNotPresent selector: matchLabels: istio: ingressgateway url: registry-cn-hangzhou.ack.aliyuncs.com/acs/asm-wasm-hash-tagging:v1.22.6.2-g72656ba-aliyun phase: AUTHN pluginConfig: rules: - header: x-user-id modulo: 100 tagHeader: appver-a policies: - range: 10 tagValue: v2 - header: x-user-id modulo: 100 tagHeader: appver-b policies: - range: 100 tagValue: v2上述雜湊打標外掛程式的配置中,我們配置了兩條打標規則:
使用x-user-id做雜湊,以100為模,當餘數範圍為10以內時,為請求添加Header:appver-a = 2。
使用x-user-id做雜湊,以100為模,當餘數範圍為10以內時,為請求添加Header:appver-b = 2。
執行以下命令,分別使用0001、0002、0003、0004、0005作為
x-user-id要求標頭的值發起請求。curl -H 'x-user-id: 0001' ${入口網關ip} curl -H 'x-user-id: 0002' ${入口網關ip} curl -H 'x-user-id: 0003' ${入口網關ip} curl -H 'x-user-id: 0004' ${入口網關ip} curl -H 'x-user-id: 0005' ${入口網關ip}預期輸出:
-> app-a(version: v1, ip: 10.0.250.27)-> app-b(version: v1, ip: 10.0.250.6)-> app-c(version: v2, ip: 10.0.250.11) -> app-a(version: v1, ip: 10.0.250.27)-> app-b(version: v1, ip: 10.0.250.6)-> app-c(version: v2, ip: 10.0.250.11) -> app-a(version: v1, ip: 10.0.250.27)-> app-b(version: v1, ip: 10.0.250.6)-> app-c(version: v2, ip: 10.0.250.11) -> app-a(version: v1, ip: 10.0.250.27)-> app-b(version: v1, ip: 10.0.250.6)-> app-c(version: v2, ip: 10.0.250.11) -> app-a(version: v2, ip: 10.0.250.14)-> app-b(version: v2, ip: 10.0.250.8)-> app-c(version: v2, ip: 10.0.250.11)可以看到,0005這個使用者雜湊的結果落在了10以內,因此被網關外掛程式成功打標,進而在訪問a、b服務時命中了v2路由規則。
步驟三:部署app-c的v3版本
在本步驟中,我們來類比在app-a和app-b的灰階過程中,負責app-c的團隊為修正app-c v2中存在的一個Bug,希望開始灰階發布app-c的v3版本。要開始app-c的灰階發布,首先需要部署app-c的v3版本。
使用以下內容建立app-c-v3.yaml。
apiVersion: apps/v1 kind: Deployment metadata: name: app-c-v3 labels: app: app-c version: v3 spec: replicas: 1 selector: matchLabels: app: app-c version: v3 ASM_TRAFFIC_TAG: v3 template: metadata: labels: app: app-c version: v3 ASM_TRAFFIC_TAG: v3 annotations: instrumentation.opentelemetry.io/inject-java: "true" instrumentation.opentelemetry.io/container-names: "default" spec: containers: - name: default image: registry-cn-hangzhou.ack.aliyuncs.com/acs/asm-mock:v0.1-java imagePullPolicy: IfNotPresent env: - name: version value: v3 - name: app value: app-c ports: - containerPort: 8000使用資料面叢集的kubeconfig執行以下命令,部署app-c。
$ kubectl apply -f app-c-v3.yaml使用以下內容建立app-c-v3-mesh.yaml。
apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: app-c namespace: default spec: hosts: - app-c.default.svc.cluster.local http: - name: v3 match: - headers: appver-c: exact: v3 route: - destination: host: app-c.default.svc.cluster.local port: number: 8000 subset: v3 - name: default route: - destination: host: app-c.default.svc.cluster.local port: number: 8000 subset: v2 --- apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: app-c spec: host: app-c.default.svc.cluster.local subsets: - labels: version: v2 name: v2 - labels: version: v3 name: v3使用控制面的kubeconfig執行以下命令,為新增的v3版本配置對應的目標規則和虛擬服務路由規則。
$ kubectl apply -f app-c-v3-mesh.yaml使用以下內容建立wasm-plugin-ab-v2-c-v3.yaml。
apiVersion: extensions.istio.io/v1alpha1 kind: WasmPlugin metadata: name: hash-tagging namespace: istio-system spec: imagePullPolicy: IfNotPresent selector: matchLabels: istio: ingressgateway url: registry-cn-hangzhou.ack.aliyuncs.com/acs/asm-wasm-hash-tagging:v1.22.6.2-g72656ba-aliyun phase: AUTHN pluginConfig: rules: - header: x-user-id modulo: 100 tagHeader: appver-a policies: - range: 10 tagValue: v2 - header: x-user-id modulo: 100 tagHeader: appver-b policies: - range: 10 tagValue: v2 - header: x-user-id modulo: 100 tagHeader: appver-c policies: - range: 50 tagValue: v3由於app-c的Team Dev認為bug修複的風險較低,且希望儘快完成灰階,因此app-c團隊決定灰階比例從50%開始。執行以下命令,修改雜湊打標外掛程式的配置,新增針對app-c的灰階打標規則。
$ kubectl apply -f wasm-plugin-ab-v2-c-v3.yaml再次執行以下命令,分別使用0001、0002、0003、0004、0005作為
x-user-id要求標頭的值發起請求。curl -H 'x-user-id: 0001' ${入口網關ip} curl -H 'x-user-id: 0002' ${入口網關ip} curl -H 'x-user-id: 0003' ${入口網關ip} curl -H 'x-user-id: 0004' ${入口網關ip} curl -H 'x-user-id: 0005' ${入口網關ip}預期輸出:
-> app-a(version: v1, ip: 10.0.250.27)-> app-b(version: v1, ip: 10.0.250.6)-> app-c(version: v2, ip: 10.0.250.11) -> app-a(version: v1, ip: 10.0.250.27)-> app-b(version: v1, ip: 10.0.250.6)-> app-c(version: v2, ip: 10.0.250.11) -> app-a(version: v1, ip: 10.0.250.27)-> app-b(version: v1, ip: 10.0.250.6)-> app-c(version: v3, ip: 10.0.250.23) -> app-a(version: v1, ip: 10.0.250.27)-> app-b(version: v1, ip: 10.0.250.6)-> app-c(version: v3, ip: 10.0.250.23) -> app-a(version: v2, ip: 10.0.250.14)-> app-b(version: v2, ip: 10.0.250.8)-> app-c(version: v3, ip: 10.0.250.23)可以看到:
id為0001、0002的使用者的調用鏈路是app-a(v1)->app-b(v1)->app-c(v2)。
id為0003、0004的使用者的調用鏈路app-a(v1)->app-b(v1)->app-c(v3)。
id為0005的使用者的調用鏈路是app-a(v2)->app-b(v2)->app-c(v3)。
步驟四:完成app-c的灰階發布
經過一段時間的灰階驗證,app-c的團隊希望率先完成發布,即將100%的流量全部路由至v3,由於我們不再需要對app-c的流量做區分,因此當需要對應用完成發布時,可以直接將虛擬服務中匹配標籤的路由規則去除,將沒有匹配條件的預設路由規則改為路由至v3。
使用控制面的kubeconfig將下面的YAML內容應用到ASM執行個體,以更新app-c的虛擬服務中的路由規則。
apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: app-c namespace: default spec: hosts: - app-c.default.svc.cluster.local http: - name: default route: - destination: host: app-c.default.svc.cluster.local port: number: 8000 subset: v3由於v3的發布已經結束,打標規則也可以一併移除,從而減少請求鏈路上攜帶的不必要的資訊,使用ASM執行個體的kubeconfig將下面的YAML應用到ASM執行個體,更新網關打標外掛程式的配置,去除app-c應用的灰階打標配置。
apiVersion: extensions.istio.io/v1alpha1 kind: WasmPlugin metadata: name: hash-tagging namespace: istio-system spec: imagePullPolicy: IfNotPresent selector: matchLabels: istio: ingressgateway url: registry-cn-hangzhou.ack.aliyuncs.com/acs/asm-wasm-hash-tagging:v1.22.6.2-g72656ba-aliyun phase: AUTHN pluginConfig: rules: - header: x-user-id modulo: 100 tagHeader: appver-a policies: - range: 10 tagValue: v2 - header: x-user-id modulo: 100 tagHeader: appver-b policies: - range: 10 tagValue: v2再次執行以下命令,分別使用0001、0002、0003、0004、0005作為
x-user-id要求標頭的值發起請求。curl -H 'x-user-id: 0001' ${入口網關ip} curl -H 'x-user-id: 0002' ${入口網關ip} curl -H 'x-user-id: 0003' ${入口網關ip} curl -H 'x-user-id: 0004' ${入口網關ip} curl -H 'x-user-id: 0005' ${入口網關ip}預期輸出:
-> app-a(version: v1, ip: 10.0.250.27)-> app-b(version: v1, ip: 10.0.250.6)-> app-c(version: v3, ip: 10.0.250.23) -> app-a(version: v1, ip: 10.0.250.27)-> app-b(version: v1, ip: 10.0.250.6)-> app-c(version: v3, ip: 10.0.250.23) -> app-a(version: v1, ip: 10.0.250.27)-> app-b(version: v1, ip: 10.0.250.6)-> app-c(version: v3, ip: 10.0.250.23) -> app-a(version: v1, ip: 10.0.250.27)-> app-b(version: v1, ip: 10.0.250.6)-> app-c(version: v3, ip: 10.0.250.23) -> app-a(version: v2, ip: 10.0.250.14)-> app-b(version: v2, ip: 10.0.250.8)-> app-c(version: v3, ip: 10.0.250.23)可以看到,所有使用者針對app-c的訪問都到達了v3版本。
說明在流量完整切換至app-c的v3版本後,還需要根據實際需求將app-c(v2)的副本數設定為0或者刪除app-c(v2),由於這不是本文討論的主題,且不影響本文的實驗效果,因此不再贅述。