在分布式應用的發布實踐中,全鏈路灰度發布可以通過嚴格泳道和寬松泳道的方式滿足絕大部分場景的需求。但是,在一些場景中,需要對多個應用進行獨立的灰度發布,同時,灰度的比例希望以用戶維度進行(而非請求維度,即同一個用戶的請求始終穩定地根據灰度比例命中到對應版本)。本文講述如何通過ASM實現按用戶比例進行多個應用的獨立灰度發布。
背景信息
流量泳道本質上是對分布式系統中的一個版本由哪些工作組成進行了定義,例如:
上圖中,一個分布式系統由應用A、應用B、應用C組成,其中:
由A(V1)+B(V1)+C(V1)構成的v1版本。
由A(V2)+B(V1)+C(V2)構成的v2版本。
借助哈希染色能力,客戶端發出的請求到達網關后,ASM網關通過哈希染色插件,對請求以用戶維度進行染色,ASM Sidecar將在整條鏈路透傳染色。這使得在任意應用向上游發起請求時,始終按照鏈路染色進行路由,從而實現任意的流量固定進入某一泳道。對于由運維人員對整個應用進行統一發布的場景來說,ASM泳道是應對問題的最佳實踐。
然而,在特定場景下,您可能希望對分布式應用系統同時對多組應用進行多個灰度發布,同時,每個或每組應用的開發團隊自行決定灰度比例,而不是由一個運維團隊或運維人員來統一操作,例如:
應用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。
apiVersion: v1 kind: Service metadata: name: app-a labels: app: app-a service: app-a spec: ports: - port: 8000 name: http selector: app: app-a --- apiVersion: apps/v1 kind: Deployment metadata: name: app-a-v1 labels: app: app-a version: v1 spec: replicas: 1 selector: matchLabels: app: app-a version: v1 ASM_TRAFFIC_TAG: v1 template: metadata: labels: app: app-a version: v1 ASM_TRAFFIC_TAG: v1 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: v1 - name: app value: app-a - name: upstream_url value: "http://app-b:8000/" ports: - containerPort: 8000 --- apiVersion: v1 kind: Service metadata: name: app-b labels: app: app-b service: app-b spec: ports: - port: 8000 name: http selector: app: app-b --- apiVersion: apps/v1 kind: Deployment metadata: name: app-b-v1 labels: app: app-b version: v1 spec: replicas: 1 selector: matchLabels: app: app-b version: v1 ASM_TRAFFIC_TAG: v1 template: metadata: labels: app: app-b version: v1 ASM_TRAFFIC_TAG: v1 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: v1 - name: app value: app-b - name: upstream_url value: "http://app-c:8000/" ports: - containerPort: 8000 --- apiVersion: v1 kind: Service metadata: name: app-c labels: app: app-c service: app-c spec: ports: - port: 8000 name: http selector: app: app-c --- apiVersion: apps/v1 kind: Deployment metadata: name: app-c-v2 labels: app: app-c version: v2 spec: replicas: 1 selector: matchLabels: app: app-c version: v2 ASM_TRAFFIC_TAG: v2 template: metadata: labels: app: app-c version: v2 ASM_TRAFFIC_TAG: v2 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: v2 - name: app value: app-c ports: - containerPort: 8000
使用數據面集群的kubeconfig執行以下命令,部署實例應用的Deployment和Service。此處, 使用命名空間default為例, 可以切換使用其他已開啟Sidecar代理注入的命名空間。
$ kubectl apply -f app-init.yaml -n default
使用以下內容創建app-init-mesh.yaml。
apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: app-b namespace: default spec: hosts: - app-b.default.svc.cluster.local http: - name: default route: - destination: host: app-b.default.svc.cluster.local port: number: 8000 subset: v1 --- 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: v2 --- apiVersion: networking.istio.io/v1beta1 kind: Gateway metadata: name: ingressgateway namespace: default spec: selector: istio: ingressgateway servers: - hosts: - '*' port: name: http number: 80 protocol: HTTP --- apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: ingressgateway namespace: istio-system spec: gateways: - default/ingressgateway hosts: - '*' http: - name: default route: - destination: host: app-a.default.svc.cluster.local port: number: 8000 subset: v1 --- apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: app-a namespace: default spec: host: app-a.default.svc.cluster.local subsets: - labels: version: v1 name: v1 --- apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: app-b namespace: default spec: host: app-b.default.svc.cluster.local subsets: - labels: version: v1 name: v1 --- apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: app-c namespace: default spec: host: app-c.default.svc.cluster.local subsets: - labels: version: v2 name: v2
使用控制面的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。
apiVersion: apps/v1 kind: Deployment metadata: name: app-a-v2 labels: app: app-a version: v2 spec: replicas: 1 selector: matchLabels: app: app-a version: v2 ASM_TRAFFIC_TAG: v2 template: metadata: labels: app: app-a version: v2 ASM_TRAFFIC_TAG: v2 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: v2 - name: app value: app-a - name: upstream_url value: "http://app-b:8000/" ports: - containerPort: 8000 --- apiVersion: apps/v1 kind: Deployment metadata: name: app-b-v2 labels: app: app-b version: v2 spec: replicas: 1 selector: matchLabels: app: app-b version: v2 ASM_TRAFFIC_TAG: v2 template: metadata: labels: app: app-b version: v2 ASM_TRAFFIC_TAG: v2 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: v2 - name: app value: app-b - name: upstream_url value: "http://app-c:8000/" ports: - containerPort: 8000
使用數據面集群的kubeconfig執行以下命令,部署app-a和app-b的v2版本。
$ kubectl apply -f app-ab-v2.yaml
使用以下內容創建app-ab-v2-mesh.yaml。
apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: app-b namespace: default spec: hosts: - app-b.default.svc.cluster.local http: - name: v2 match: - headers: appver-b: exact: v2 route: - destination: host: app-b.default.svc.cluster.local port: number: 8000 subset: v2 - name: default route: - destination: host: app-b.default.svc.cluster.local port: number: 8000 subset: v1 --- 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: v2 --- apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: ingressgateway namespace: default spec: gateways: - default/ingressgateway hosts: - '*' http: - name: v2 match: - headers: appver-a: exact: v2 route: - destination: host: app-a.default.svc.cluster.local port: number: 8000 subset: v2 - name: default route: - destination: host: app-a.default.svc.cluster.local port: number: 8000 subset: v1 --- apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: app-a spec: host: app-a.default.svc.cluster.local subsets: - labels: version: v1 name: v1 - labels: version: v2 name: v2 --- apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: app-b spec: host: app-b.default.svc.cluster.local subsets: - labels: version: v1 name: v1 - labels: version: v2 name: 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
使用控制面的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的開發團隊認為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),由于這不是本文討論的主題,且不影響本文的實驗效果,因此不再贅述。