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 走基线环境。
调用链路 Ingress-nginx -> A -> B -> C ,其中 A 可以是一个 spring-boot 的应用。
场景二:通过给流量带上特定的header实现全链路灰度
有些客户端没法改写域名,希望能访问 www.base.com 通过传入不同的 header 来路由到灰度环境。例如下图中,通过添加 x-mse-tag:gray
这个 header,来访问灰度环境。
场景三:通过自定义路由规则来进行全链路灰度
有时候我们不想要自动透传且自动路由,而是希望微服务调用链上下游上的每个应用能自定义灰度规则,例如 B 应用希望控制只有满足自定义规则的请求才会路由到 B 应用这里,而 C 应用有可能希望定义和 B 不同的灰度规则,这时应该如何配置呢,场景参见如下图:
相关概念
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 微服务治理
点击 开通MSE微服务治理专业版 以使用全链路灰度能力。
访问容器服务控制台,打开应用目录,搜索
ack-mse-pilot
,点击创建。在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 走基线环境。
调用链路 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
则会以原来请求中的标签优先。
针对入口应用 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
配置入口的 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 处配置好规则,某个应用需要灰度发布的时候,只需要在灰度环境中部署好应用,灰度流量自然会进入好灰度机器中,如果验证没问题,则将灰度的镜像发布到基线环境中;如果一次变更有多个应用需要灰度发布,则把他们都加入到灰度环境中即可。
最佳实践
给所有灰度环境的应用打上 gray 标,基线环境的应用默认不打标。
线上常态化引流2%的流量进去灰度环境中。
场景二:通过给流量带上特定的header实现全链路灰度
有些客户端没法改写域名,希望能访问 www.base.com 通过传入不同的 header 来路由到灰度环境。例如下图中,通过添加 x-mse-tag:gray
这个 header,来访问灰度环境。
重要
这个时候 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 的请求进入灰度环境。
只需要增加下面这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 不同的灰度规则,这时应该如何配置呢,场景参见如下图:
说明
注意,最好把场景1和2中配置的参数清除掉。
需要在入口应用 A 处(最好是所有的入口应用都增加该环境变量,包括gray和base) 增加一个环境变量:
alicloud.service.header=x-user-id
,x-user-id
是需要透传的 header,它的作用是识别该 header 并做自动透传。说明
注意这里不要使用 x-mse-tag, 它是系统默认的一个 header,有特殊的逻辑。
在中间的 B 应用处,在 MSE 控制台配置标签路由规则(C应用需要配置规则同理)。
在 Ingress 处配置路由规则,这一步参考场景二,并采用如下配置:
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 对象。