MSE服务治理最佳实践:基于Ingress-nginx网关实现全链路灰度

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


1. 概述

1.1 方案介绍

本文主要介绍通过 Ingress-nginx 来实现全链路灰度功能。我们假设应用的架构由 Ingress-nginx 以及后端的微服务架构(Spring Cloud)来组成,其中后端调用链路有3跳,分别是购物车(a)、交易中心(b)以及库存中心(c),假设客户端是通过客户端或者是 H5 页面来访问后端服务,微服务之间都是通过 Nacos 注册中心做服务发现。

1.2 目标读者

希望通过极简的配置与无代码侵入的方式,使用 Ingress-nginx 网关来实现全链路灰度功能。

1.3 适用场景

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

有时候,我们可以通过不同的域名来区分线上基线环境和灰度环境,灰度环境有单独的域名可以配置,假设我们通过访问www.gray.com 来请求灰度环境,访问 www.base.com 走基线环境。image.png

调用链路 Ingress-nginx -> A -> B -> C ,其中 A 可以是一个 spring-boot 的应用。

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

有些客户端没法改写域名,希望能访问 www.base.com 通过传入不同的 header 来路由到灰度环境。例如下图中,通过添加 x-mse-tag:gray 这个 header,来访问灰度环境。

image.png

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

有时候我们不想要自动透传且自动路由,而是希望微服务调用链上下游上的每个应用能自定义灰度规则,例如 B 应用希望控制只有满足自定义规则的请求才会路由到 B 应用这里,而 C 应用有可能希望定义和 B 不同的灰度规则,这时应该如何配置呢,场景参见如下图:

image.png

相关概念

2. 方案实施

2.1 前提条件

  • 安装 Ingress-nginx 组件

访问容器服务控制台,打开应用目录,搜索 ack-ingress-nginx ,选择命名空间 kube-system,点击创建,安装完成后,在 kube-system 命名空间中会看到一个 deployment ack-ingress-nginx-default-controller ,表明安装成功。

$ kubectl get deployment -n kube-system
NAME                                      READY   UP-TO-DATE   AVAILABLE   AGE
ack-ingress-nginx-default-controller      2/2     2            2           18h
  • 开启 MSE 微服务治理

  1. 点击 开通MSE微服务治理专业版 以使用全链路灰度能力。

  2. 访问容器服务控制台,打开应用目录,搜索 ack-mse-pilot ,点击创建。

  3. 在MSE服务治理控制台,打开K8s集群列表,选择对应集群,对应命名空间,并打开微服务治理。

  • 部署 Demo 应用程序

将下面的文件保存到 ingress-gray-demo-deployment-set.yaml 中,并执行 kubectl apply -f ingress-gray-demo-deployment-set.yaml 以部署应用,这里我们将要部署 A, B, C 三个应用,每个应用分别部署一个基线版本和一个灰度版本。

# A 应用 base 版本
---
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
      
# A 应用 gray 版本
---            
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 版本
---
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
            
# B 应用 gray 版本  
---
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 版本
---
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
            
# C 应用 gray 版本
---
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
---
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 走基线环境。

image.png

调用链路 Ingress-nginx -> A -> B -> C ,其中 A 可以是一个 spring-boot 的应用。

重要

入口应用 A 的 gray 和 A 的 base 环境,需要增加profiler.micro.service.tag.trace.enable=true 这个环境变量,表示开启向后透传当前环境的标签的功能。这样, 当 Ingress-nginx 路由 A 的 gray 之后,即使请求中没有携带任何header,因为开启了此开关,所以往后调用的时候会自动添加 x-mse-tag:gray这个header,其中的 header 的值 gray 来自于 A 应用配置的标签信息。如果原来的请求中带有 x-mse-tag:gray则会以原来请求中的标签优先。

  1. 针对入口应用 A ,配置两个 k8s service。spring-cloud-a-base 对应 A 的 base 版本,spring-cloud-a-gray 对应 A 的 gray 版本。

    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
    
    ---
    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 规则,访问 www.base.com 路由到 a 应用的 base 版本,访问 www.gray.com 路由到 a 应用的 gray 版本。

    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
    spec:
      rules:
      - host: www.gray.com
        http:
          paths:
          - backend:
              serviceName: spring-cloud-a-gray
              servicePort: 20001
            path: /

结果验证

  • 访问 www.base.com 路由到基线环境

curl -H"Host:www.base.com" http://106.14.155.223/a
A[172.18.144.155] -> B[172.18.144.120] -> C[172.18.144.79]%
  • 访问 www.gray.com 路由到灰度环境

curl -H"Host:www.gray.com" http://106.14.155.223/a
Agray[172.18.144.160] -> Bgray[172.18.144.57] -> Cgray[172.18.144.157]% 
  • 如果入口应用 A 没有灰度环境,访问到 A 的 base 环境,又需要在 A -> B 的时候进入灰度环境,则可以,通过增加一个特殊的 header x-mse-tag来实现,header 的值是想要去的环境的标签,例如 gray

curl -H"Host:www.base.com"  -H"x-mse-tag:gray" http://106.14.155.223/a
A[172.18.144.155] -> Bgray[172.18.144.139] -> Cgray[172.18.144.8]% 

可以看到第一跳,进入了 A 的 base环境,但是 A->B 的时候又重新回到了灰度环境。

说明

这种使用方式的好处是,配置简单,只需要在 Ingress 处配置好规则,某个应用需要灰度发布的时候,只需要在灰度环境中部署好应用,灰度流量自然会进入好灰度机器中,如果验证没问题,则将灰度的镜像发布到基线环境中;如果一次变更有多个应用需要灰度发布,则把他们都加入到灰度环境中即可。

最佳实践

  1. 给所有灰度环境的应用打上 gray 标,基线环境的应用默认不打标。

  2. 线上常态化引流2%的流量进去灰度环境中。

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

有些客户端没法改写域名,希望能访问 www.base.com 通过传入不同的 header 来路由到灰度环境。例如下图中,通过添加 x-mse-tag:gray 这个 header,来访问灰度环境。

image.png

重要

这个时候 base 的Ingress 规则如下,注意这里增加了 nginx.ingress.kubernetes.io/canary 相关的多条规则。

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: /
  • 此时,访问 www.base.com 路由到基线环境。

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 -H"Host:www.base.com"  -H"x-mse-tag:gray" http://106.14.155.223/a
Agray[172.18.144.82] -> Bgray[172.18.144.57] -> Cgray[172.18.144.8]%

可以看到 Ingress 根据这个header直接路由到了 A 的 gray 环境中。

更进一步的,还可以借助 Ingress 实现更复杂的路由,比如客户端已经带上了某个header,想要利用现成的 header来实现路由,而不用新增一个 header,例如下图所示,假设我们想要 x-user-id 为 100 的请求进入灰度环境。

image.png

只需要增加下面这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 ,满足条件进入灰度环境:

curl -H"Host:www.base.com"  -H"x-user-id:100" http://106.14.155.223/a
Agray[172.18.144.93] -> Bgray[172.18.144.24] -> Cgray[172.18.144.25]
  • 不满足条件的请求,进入基线环境:

curl -H"Host:www.base.com"  -H"x-user-id:101" http://106.14.155.223/a
A[172.18.144.91] -> B[172.18.144.22] -> C[172.18.144.95]

相比场景一来说这样的好处是,客户端的域名不变,只需要通过请求来区分。

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

有时候我们不想要自动透传且自动路由,而是希望微服务调用链上下游上的每个应用能自定义灰度规则,例如 B 应用希望控制只有满足自定义规则的请求才会路由到 B 应用这里,而 C 应用有可能希望定义和 B 不同的灰度规则,这时应该如何配置呢,场景参见如下图:

image.png

说明

注意,最好把场景1和2中配置的参数清除掉。

  1. 需要在入口应用 A 处(最好是所有的入口应用都增加该环境变量,包括gray和base) 增加一个环境变量:alicloud.service.header=x-user-idx-user-id 是需要透传的 header,它的作用是识别该 header 并做自动透传。

    说明

    注意这里不要使用 x-mse-tag, 它是系统默认的一个 header,有特殊的逻辑。

  2. 在中间的 B 应用处,在 MSE 控制台配置标签路由规则(C应用需要配置规则同理)。

  3. 在 Ingress 处配置路由规则,这一步参考场景二,并采用如下配置:

image.png

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: /

结果验证

  • 访问灰度环境,带上满足条件的 header,路由到 B 的灰度环境中。

curl 120.77.215.62/a -H "Host: www.base.com" -H "x-user-id: 100" 
Agray[192.168.86.42] -> Bgray[192.168.74.4] -> C[192.168.86.33]
  • 访问灰度环境,带上不满足条件的 header,路由到 B 的base环境中。

curl 120.77.215.62/a -H "Host: www.base.com" -H "x-user-id: 101" 
A[192.168.86.35] -> B[192.168.73.249] -> C[192.168.86.33]
  • 如果仅仅需要灰度对应的应用,不需要 Ingress 根据Header路由,那么可以去掉 Ingress Canary配置。

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

curl 120.77.215.62/a -H "Host: www.base.com" -H "x-user-id: 100" 
A[192.168.86.35] -> Bgray[192.168.74.4] -> C[192.168.86.33]
  • 访问基线(base)环境,带上不满足条件的header,路由到 B 的 base 环境中。

curl 120.77.215.62/a -H "Host: www.base.com" -H "x-user-id: 101" 
A[192.168.86.35] -> B[192.168.73.249] -> C[192.168.86.33]
  • MSE :阿里云微服务引擎

  • 全链路:请求从上往下的过程达到超过2个应用。

  • Ingress:对集群中服务的外部访问进行管理的API 对象。

3. 实例:通常对应一个应用进程,是服务的承载实体,通常一个服务集群有多个实例。

作者介绍
目录