通过Ingress-nginx提供的全链路灰度能力,可以在不需要修改任何您的业务代码的情况下,实现全链路流量控制。本文介绍通过Ingress-nginx实现全链路灰度功能。

前提条件

背景信息

微服务架构下,有些开发需求,微服务调用链路上的多个微服务同时发生了改动,通常每个微服务都会有灰度环境或分组来接收灰度流量。此时希望通过进入上游灰度环境的流量,也能进入下游灰度的环境中,确保1个请求始终在灰度环境中传递,即使这个调用链路上有一些微服务没有灰度环境,这些应用请求在下游的时候依然能够回到灰度环境中。通过MSE提供的全链路灰度能力,可以在不需要修改任何您的业务代码的情况下,能够轻松实现上述能力。

本文通过示例为您演示MSE云原生网关全链路灰度功能。假设应用的架构由MSE云原生网关以及后端的微服务架构(Spring Cloud)组成,后端调用链路有3个:购物车(A),交易中心(B),库存中心(C),可以通过客户端或者是HTML来访问后端服务,这些服务之间通过Nacos注册中心实现服务发现。

准备工作

安装Ingress-nginx组件

  1. 登录容器服务控制台
  2. 在左侧导航栏选择市场 > 应用市场
  3. 应用市场页面单击应用目录页签,然后搜索并单击ack-ingress-nginx组件。
  4. ack-ingress-nginx页面右上方单击一键部署,在创建面板中选择集群命名空间,设置组件发布名称,然后单击下一步
    说明 推荐使用默认的命名空间kube-system
  5. 参数配置向导中确认组件参数信息,然后单击确定
    安装完成后,在命名空间kube-system中出现ack-ingress-nginx-default应用,表示安装成功。

开启MSE微服务治理

  1. 开通微服务治理专业版:

    1. 单击开通MSE微服务治理
    2. 微服务治理版本选择专业版,选中服务协议,然后单击立即开通

      关于微服务治理的计费详情,请参见价格说明

  2. 安装MSE微服务治理组件:

    1. 容器服务控制台左侧导航栏中,选择市场 > 应用市场
    2. 应用市场页面单击应用目录页签,然后搜索并单击ack-onepilot组件。
    3. ack-onepilot页面右上方单击一键部署,在创建面板中选择集群命名空间,设置组件发布名称,然后单击下一步
      说明 推荐使用默认的命名空间ack-onepilot
    4. 参数配置向导中确认组件参数信息,然后单击确定

      安装完成后,在命名空间ack-onepilot中出现ack-onepilot应用,表示安装成功。

  3. 为应用开启微服务治理:

    1. 登录MSE治理中心控制台
    2. 在左侧导航栏选择微服务治理中心 > K8s集群列表
    3. K8s集群列表页面搜索目标集群,单击搜索图标图标,然后单击目标集群操作列下方的管理
    4. 集群详情页面命名空间列表区域,单击目标命名空间操作列下方的开启微服务治理
    5. 开启微服务治理对话框中单击确认

部署Demo应用程序

  1. 容器服务控制台左侧导航栏中,单击集群
  2. 集群列表页面中,单击目标集群名称或者目标集群右侧操作列下的详情
  3. 在集群管理页左侧导航栏中,选择工作负载 > 无状态
  4. 无状态页面选择命名空间,然后单击使用YAML创建资源
  5. 对模板进行相关配置,完成配置后单击创建
    本文示例中部署A、B、C三个应用,每个应用分别部署一个基线版本和一个灰度版本;并部署一个Nacos server应用,用于实现服务发现。
    • A应用
      • 基线(base)版本YAML:
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: spring-cloud-a
        spec:
          replicas: 2
          selector:
            matchLabels:
              app: spring-cloud-a
          template:
            metadata:
              annotations:
                msePilotCreateAppName: spring-cloud-a
              labels:
                app: spring-cloud-a
            spec:
              containers:
              - env:
                - name: JAVA_HOME
                  value: /usr/lib/jvm/java-1.8-openjdk/jre
                image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-a:0.1-SNAPSHOT
                imagePullPolicy: Always
                name: spring-cloud-a
                ports:
                - containerPort: 20001
                livenessProbe:
                  tcpSocket:
                    port: 20001
                  initialDelaySeconds: 10
                  periodSeconds: 30
      • 灰度(gray)版本YAML:
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: spring-cloud-a-new
        spec:
          replicas: 2
          selector:
            matchLabels:
              app: spring-cloud-a-new
          strategy:
          template:
            metadata:
              annotations:
                alicloud.service.tag: gray
                msePilotCreateAppName: spring-cloud-a
              labels:
                app: spring-cloud-a-new
            spec:
              containers:
              - env:
                - name: JAVA_HOME
                  value: /usr/lib/jvm/java-1.8-openjdk/jre
                - name: profiler.micro.service.tag.trace.enable
                  value: "true"
                image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-a:0.1-SNAPSHOT
                imagePullPolicy: Always
                name: spring-cloud-a-new
                ports:
                - containerPort: 20001
                livenessProbe:
                  tcpSocket:
                    port: 20001
                  initialDelaySeconds: 10
                  periodSeconds: 30
    • B应用
      • 基线(base)版本YAML:
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: spring-cloud-b
        spec:
          replicas: 2
          selector:
            matchLabels:
              app: spring-cloud-b
          strategy:
          template:
            metadata:
              annotations:
                msePilotCreateAppName: spring-cloud-b
              labels:
                app: spring-cloud-b
            spec:
              containers:
              - env:
                - name: JAVA_HOME
                  value: /usr/lib/jvm/java-1.8-openjdk/jre
                image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-b:0.1-SNAPSHOT
                imagePullPolicy: Always
                name: spring-cloud-b
                ports:
                - containerPort: 8080
                livenessProbe:
                  tcpSocket:
                    port: 20002
                  initialDelaySeconds: 10
                  periodSeconds: 30
      • 灰度(gray)版本YAML:
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: spring-cloud-b-new
        spec:
          replicas: 2
          selector:
            matchLabels:
              app: spring-cloud-b-new
          template:
            metadata:
              annotations:
                alicloud.service.tag: gray
                msePilotCreateAppName: spring-cloud-b
              labels:
                app: spring-cloud-b-new
            spec:
              containers:
              - env:
                - name: JAVA_HOME
                  value: /usr/lib/jvm/java-1.8-openjdk/jre
                image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-b:0.1-SNAPSHOT
                imagePullPolicy: Always
                name: spring-cloud-b-new
                ports:
                - containerPort: 8080
                livenessProbe:
                  tcpSocket:
                    port: 20002
                  initialDelaySeconds: 10
                  periodSeconds: 30
    • C应用
      • 基线(base)版本YAML:
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: spring-cloud-c
        spec:
          replicas: 2
          selector:
            matchLabels:
              app: spring-cloud-c
          template:
            metadata:
              annotations:
                msePilotCreateAppName: spring-cloud-c
              labels:
                app: spring-cloud-c
            spec:
              containers:
              - env:
                - name: JAVA_HOME
                  value: /usr/lib/jvm/java-1.8-openjdk/jre
                image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-c:0.1-SNAPSHOT
                imagePullPolicy: Always
                name: spring-cloud-c
                ports:
                - containerPort: 8080
                livenessProbe:
                  tcpSocket:
                    port: 20003
                  initialDelaySeconds: 10
                  periodSeconds: 30
      • 灰度(gray)版本YAML:
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: spring-cloud-c-new
        spec:
          replicas: 2
          selector:
            matchLabels:
              app: spring-cloud-c-new
          template:
            metadata:
              annotations:
                alicloud.service.tag: gray
                msePilotCreateAppName: spring-cloud-c
              labels:
                app: spring-cloud-c-new
            spec:
              containers:
              - env:
                - name: JAVA_HOME
                  value: /usr/lib/jvm/java-1.8-openjdk/jre
                image: registry.cn-shanghai.aliyuncs.com/yizhan/spring-cloud-c:0.1-SNAPSHOT
                imagePullPolicy: IfNotPresent
                name: spring-cloud-c-new
                ports:
                - containerPort: 8080
                livenessProbe:
                  tcpSocket:
                    port: 20003
                  initialDelaySeconds: 10
                  periodSeconds: 30
    • Nacos Server应用YAML:
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: nacos-server
      spec:
        replicas: 1
        selector:
          matchLabels:
            app: nacos-server
        template:
          metadata:
            labels:
              app: nacos-server
          spec:
            containers:
            - env:
              - name: MODE
                value: standalone
              image: nacos/nacos-server:latest
              imagePullPolicy: Always
              name: nacos-server
            dnsPolicy: ClusterFirst
            restartPolicy: Always
      
      # Nacos Server Service 配置
      ---
      apiVersion: v1
      kind: Service
      metadata:
        name: nacos-server
      spec:
        ports:
        - port: 8848
          protocol: TCP
          targetPort: 8848
        selector:
          app: nacos-server
        type: ClusterIP
                                      

应用场景一:对经过节点的流量进行自动染色,实现全链路灰度

可以通过不同的域名来区分线上基线环境和灰度环境,灰度环境有单独的域名可以配置,例如通过访问www.gray.com请求灰度环境,访问www.base.com请求基线环境。应用场景一

调用链路为:Ingress-nginx->A->B->C,其中A可以是一个Spring Boot的应用。

说明 入口应用A的基线(base)和灰度(gray)环境,需要增加profiler.micro.service.tag.trace.enable=true环境变量,表示开启向后透传当前环境的标签功能。这样,当Ingress-nginx路由A的灰度版本后,即使请求中没有携带任何Header,往后调用时会自动添加x-mse-tag:gray这个Header,其中的Header的值gray来自于A应用配置的标签信息。如果原来的请求中带有x-mse-tag:gray则会以原来请求中的标签优先。
  1. 针对入口应用A,配置两个K8s service。
    • 配置spring-cloud-a-base对应A的基线(base)版本。
      apiVersion: v1
      kind: Service
      metadata:
        name: spring-cloud-a-base
      spec:
        ports:
          - name: http
            port: 20001
            protocol: TCP
            targetPort: 20001
        selector:
          app: spring-cloud-a
    • 配置spring-cloud-a-gray对应A的灰度(gray)版本
      apiVersion: v1
      kind: Service
      metadata:
        name: spring-cloud-a-gray
      spec:
        ports:
          - name: http
            port: 20001
            protocol: TCP
            targetPort: 20001
        selector:
          app: spring-cloud-a-new
  2. 配置入口的Ingress-nginx规则。
    • 访问www.base.com路由到A应用的基线(base)版本。
      apiVersion: networking.k8s.io/v1beta1
      kind: Ingress
      metadata:
        name: spring-cloud-a-base
      spec:
        rules:
        - host: www.base.com
          http:
            paths:
            - backend:
                serviceName: spring-cloud-a-base
                servicePort: 20001
              path: /
    • 访问www.gray.com路由到A应用的灰度(gray)版本。
      apiVersion: networking.k8s.io/v1beta1
      kind: Ingress
      metadata:
        name: spring-cloud-a-gray
      spec:
        rules:
        - host: www.gray.com
          http:
            paths:
            - backend:
                serviceName: spring-cloud-a-gray
                servicePort: 20001
              path: /
结果验证
  • 访问www.base.com路由到基线(base)环境。
    • Curl命令:
      curl -H"Host:www.base.com" http://106.14.XX.XX/a
    • 执行结果:
      A[172.18.XX.XX] -> B[172.18.XX.XX] -> C[172.18.XX.XX]%
  • 访问www.gray.com路由到灰度(gray)环境。
    • Curl命令:
      curl -H"Host:www.gray.com" http://106.14.XX.XX/a
    • 执行结果:
      Agray[172.18.XX.XX] -> Bgray[172.18.XX.XX] -> Cgray[172.18.XX.XX]%
  • 如果入口应用A没有灰度(gray)环境,访问到A的基线(base)环境,又需要在A->B的时候进入灰度环境,则可以通过增加一个特殊的 Header:x-mse-tag来实现,Header的值是流量走向的环境的标签,例如gray
    • Curl命令:
      curl -H"Host:www.base.com"  -H"x-mse-tag:gray" http://106.14.XX.XX/a
    • 执行结果:
      A[172.18.XX.XX] -> Bgray[172.18.XX.XX] -> Cgray[172.18.XX.XX]%

      可以看到,首先进入了A的基线(base)环境,但是A->B的时候又重新回到了灰度(gray)环境。

      说明 您在MSE云原生网关中配置好规则,当某个应用需要灰度发布时,只需要在灰度环境中部署好应用,灰度流量会进入到灰度节点中。如果验证没问题,则将灰度的镜像发布到基线环境中;如果一次变更中有多个应用需要灰度发布,则把他们都加入到灰度环境中即可。

场景二:通过给流量带上特定的Header,实现全链路灰度

有些客户端没法改写域名,希望能访问www.base.com通过传入不同的Header来路由到灰度环境。例如下图中,通过添加x-mse-tag:gray这个Header,来访问灰度(gray)环境。应用场景二
基线(base)环境的Ingress-nginx配置如下:
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: spring-cloud-a-base
spec:
  rules:
  - host: www.base.com
    http:
      paths:
      - backend:
          serviceName: spring-cloud-a-base
          servicePort: 20001
        path: /
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: spring-cloud-a-gray
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-by-header: "x-mse-tag"
    nginx.ingress.kubernetes.io/canary-by-header-value: "gray"
    nginx.ingress.kubernetes.io/canary-weight: "0"
spec:
  rules:
  - host: www.base.com
    http:
      paths:
      - backend:
          serviceName: spring-cloud-a-gray
          servicePort: 20001
        path: /
注意 该配置中增加了nginx.ingress.kubernetes.io/canary相关的多条规则。
结果验证
  • 访问www.base.com路由到基线(base)环境。
    • Curl命令:
      curl -H"Host:www.base.com" http://106.14.155.223/a
    • 执行结果:
      A[172.18.144.155] -> B[172.18.144.56] -> C[172.18.144.156]%
  • 如果想访问灰度环境,只需要在请求中增加一个Header:x-mse-tag:gray
    • Curl命令:
      curl -H"Host:www.base.com"  -H"x-mse-tag:gray" http://106.14.XX.XX/a
    • 执行结果:
      Agray[172.18.XX.XX] -> Bgray[172.18.XX.XX] -> Cgray[172.18.XX.XX]%

      可以看到Ingress-nginx根据这个Header直接路由到了A的灰度(gray)环境中。

更进一步,还可以借助Ingress-nginx实现更复杂的路由,例如客户端已经带上了某个Header,想要利用现成的Header来实现路由,而不用新增一个Header,如下图所示,假设我们想要x-user-id为100的请求进入灰度环境。应用场景2-1
只需要增加下面这4条规则:
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: spring-cloud-a-base
spec:
  rules:
  - host: www.base.com
    http:
      paths:
      - backend:
          serviceName: spring-cloud-a-base
          servicePort: 20001
        path: /
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: spring-cloud-a-base-gray
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-by-header: "x-user-id"
    nginx.ingress.kubernetes.io/canary-by-header-value: "100"
    nginx.ingress.kubernetes.io/canary-weight: "0"
spec:
  rules:
  - host: www.base.com
    http:
      paths:
      - backend:
          serviceName: spring-cloud-a-gray
          servicePort: 20001
        path: /
结果验证
  • 访问的时候带上特殊的Header,满足条件进入灰度(gray)环境。
    • Curl命令:
      curl -H"Host:www.base.com"  -H"x-user-id:100" http://106.14.XX.XX/a
    • 执行结果:
      Agray[172.18.XX.XX] -> Bgray[172.18.XX.XX] -> Cgray[172.18.XX.XX]
  • 不满足条件的请求,进入基线(base)环境:
    • Curl命令:
      curl -H"Host:www.base.com"  -H"x-user-id:101" http://106.14.XX.XX/a
    • 执行结果:
      A[172.18.XX.XX] -> B[172.18.XX.XX] -> C[172.18.XX.XX]

    这样的好处是,客户端的域名不变,只需要通过请求来进行区分。

场景三:通过自定义路由规则来进行全链路灰度

有时候,我们不想要自动透传且自动路由,而是希望微服务调用链上下游上的每个应用能自定义灰度规则,例如B应用希望控制只有满足自定义规则的请求才会路由到B应用这里,而C应用有可能希望定义和B应用不同的灰度规则,如下图所示:应用场景三
  1. 在入口应用A处(最好是所有的入口应用都增加该环境变量,包括基线环境和灰度环境) 增加一个环境变量:alicloud.service.header=x-user-id,其中x-user-id是需要透传的Header,它的作用是识别该Header并做自动透传。
    说明
    • 建议把场景一和场景二中配置的环境变量profiler.micro.service.tag.trace.enable=true删除。
    • 不要使用x-mse-tag,它是系统默认的一Header,有特殊的逻辑。
  2. MSE治理中心控制台给B应用和C应用配置标签路由规则。具体操作,请参见配置标签路由
    配置路由规则
  3. 配置Ingress-nginx路由规则。
    apiVersion: networking.k8s.io/v1beta1
    kind: Ingress
    metadata:
      name: spring-cloud-a-base
    spec:
      rules:
      - host: www.base.com
        http:
          paths:
          - backend:
              serviceName: spring-cloud-a-base
              servicePort: 20001
            path: /
    ---
    apiVersion: networking.k8s.io/v1beta1
    kind: Ingress
    metadata:
      annotations:
        nginx.ingress.kubernetes.io/canary: 'true'
        nginx.ingress.kubernetes.io/canary-by-header: x-user-id
        nginx.ingress.kubernetes.io/canary-by-header-value: '100'
        nginx.ingress.kubernetes.io/canary-weight: '0'
      name: spring-cloud-a-gray
    spec:
      rules:
        - host: www.base.com
          http:
            paths:
              - backend:
                  serviceName: spring-cloud-a-gray
                  servicePort: 20001
                path: /
结果验证
  • 访问灰度(gray)环境,带上满足条件的Header,路由到B应用的灰度(gray)环境中。
    • Curl命令:
      curl 120.77.215.62/a -H "Host: www.base.com" -H "x-user-id: 100"
    • 执行结果:
      Agray[192.168.XX.XX] -> Bgray[192.168.XX.XX] -> C[192.168.XX.XX]
  • 访问灰度(gray)环境,带上不满足条件的Header,路由到B应用的基线(base)环境中。
    • Curl命令:
      curl 120.77.215.62/a -H "Host: www.base.com" -H "x-user-id: 101"
    • 执行结果:
      A[192.168.XX.XX] -> B[192.168.XX.XX] -> C[192.168.XX.XX]
  • 如果仅仅需要灰度对应的应用,不需要通过Ingress-nginx实现Header路由,那么可以去掉Ingress Canary配置。

    访问基线(base)环境A应用(基线环境入口应用需要加上alicloud.service.header环境变量),带上满足条件的Header,路由到B应用的灰度(gray)环境中。

    • Curl命令:
      curl 120.77.215.62/a -H "Host: www.base.com" -H "x-user-id: 100"
    • 执行结果:
      A[192.168.XX.XX] -> Bgray[192.168.XX.XX] -> C[192.168.XX.XX]
  • 访问基线(base)环境,带上不满足条件的Header,路由到B应用的基线(base)环境中。
    • Curl命令:
      curl 120.77.215.62/a -H "Host: www.base.com" -H "x-user-id: 101"
    • 执行结果:
      A[192.168.XX.XX] -> B[192.168.XX.XX] -> C[192.168.XX.XX]