文档

使用ASM进行用户级混合灰度发布

更新时间:

在分布式应用的发布实践中,全链路灰度发布可以通过严格泳道和宽松泳道的方式满足绝大部分场景的需求。在泳道场景中,通过入口流量进行染色,网格代理(ASM Sidecar)持续在请求链路中透传染色,并在虚拟服务中依据染色路由到对应颜色的子集,从而实现将调用链路始终维持在一个环境中。本文介绍如何使用ASM进行用户级混合灰度发布。

背景信息

流量泳道本质上是对分布式系统中的一个版本由哪些工作组成进行了定义,例如:

yuque_diagram

上图中,一个分布式系统由应用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,为了修正V3版本中存在的BUG,上线了V3,由于BUG改动小,且希望尽快修复,因此负责该功能的项目组希望直接将50%用户的流量打到V3版本。

image

要实现以上需求,就需要让同一调用链路上请求不同服务时以不同的策略进行路由,这依靠单一染色是无法做到的。ASM的哈希打标插件支持同时为请求打上多种标记,同时借助ASMHeaderPropagation能力对指定Prefix的透传,可以轻松做到对请求打上多个标记并将它们在整条调用链路上透传,再利用ASM的虚拟服务对这些标记进行匹配,从而实现灵活的灰度发布。

image

前提条件

操作步骤

步骤一:部署实例应用

本例的演示应用分为app-a、app-b、app-c三个应用,完整的业务调用链路为app-a -> app-b -> app-c。其中,app-a和app-b处于v1版本,app-c则已经发布到了v2版本。

image

  1. 使用以下内容创建app-init.yaml。

    展开查看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
  2. 使用数据面集群的kubeconfig执行以下命令,部署实例应用的Deployment和Service。

    $ kubectl apply -f app-init.yaml
  3. 使用以下内容创建app-init-mesh.yaml。

    展开查看YAML内容

    apiVersion: networking.istio.io/v1
    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/v1
    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/v1
    kind: VirtualService
    metadata:
      name: ingressgateway
      namespace: istio-system
    spec:
      gateways:
      - istio-system/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
    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
    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
    spec:
      host: app-c.default.svc.cluster.local
      subsets:
        - labels:
            version: v2
          name: v2
  4. 使用控制面的kubeconfig执行以下命令,为应用和ASM网关配置虚拟服务和目标规则。

    $ kubectl apply -f app-init-mesh.yaml
  5. 执行以下命令,通过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透传所有插件为请求打上的标识

说明

实际操作顺序并不是严格按照上述描述的顺序进行,此顺序只是便于理解,而实际操作时需要根据依赖关系决定操作顺序。

image

  1. 使用以下内容创建app-ab-v2.yaml。

    展开查看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
  2. 使用数据面集群的kubeconfig执行以下命令,部署app-a和app-b的v2版本。

    $ kubectl apply -f app-ab-v2.yaml
  3. 使用以下内容创建app-ab-v2-mesh.yaml。

    展开查看YAML内容

    apiVersion: networking.istio.io/v1
    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/v1
    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/v1
    kind: VirtualService
    metadata:
      name: ingressgateway
      namespace: istio-system
    spec:
      gateways:
      - istio-system/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
    
  4. 使用控制面的kubeconfig执行以下命令,为app-a和app-b应用对应的目标规则新增v2子集,以及为虚拟服务新增匹配打标规则的路由规则。

    $ kubectl apply -f app-ab-v2-mesh.yaml
  5. 使用以下内容创建header-propagation.yaml。

    apiVersion: istio.alibabacloud.com/v1beta1
    kind: ASMHeaderPropagation
    metadata:
      name: tag-propagation
    spec:
      headerPrefixes:
        - appver
  6. 执行以下命令,使得Sidecar对前缀为appver的请求header进行透传。

    $ kubectl apply -f header-propagation.yaml
  7. 使用以下内容创建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。

  8. 执行以下命令,分别使用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版本。

  1. 使用以下内容创建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
  2. 使用数据面集群的kubeconfig执行以下命令,部署app-c。

    $ kubectl apply -f app-c-v3.yaml
  3. 使用以下内容创建app-c-v3-mesh.yaml。

    apiVersion: networking.istio.io/v1
    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
  4. 使用控制面的kubeconfig执行以下命令,为新增的v3版本配置对应的目标规则和虚拟服务路由规则。

    $ kubectl apply -f app-c-v3-mesh.yaml
  5. 使用以下内容创建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
  6. 由于app-c的开发团队认为bug修复的风险较低,且希望尽快完成灰度,因此app-c团队决定灰度比例从50%开始。执行以下命令,修改哈希打标插件的配置,新增针对app-c的灰度打标规则。

    $ kubectl apply -f wasm-plugin-ab-v2-c-v3.yaml
  7. 再次执行以下命令,分别使用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。

  1. 使用控制面的kubeconfig将下面的YAML内容应用到ASM实例,以更新app-c的虚拟服务中的路由规则。

    apiVersion: networking.istio.io/v1
    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
  2. 由于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
  3. 再次执行以下命令,分别使用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),由于这不是本文讨论的主题,且不影响本文的实验效果,因此不再赘述。