简介
日常工作中我们经常需要对服务进行版本更新升级,为此我们经常使用到的发布方式有滚动升级、分批暂停发布、蓝绿发布以及灰度发布,今天主要跟大家分享下在阿里云容器服务Kubernetes集群中如何通过Ingress Controller来实现应用服务的灰度发布及AB测试。
发布场景
场景一
假设当前线上环境我们已经有一套服务Service A对外提供7层服务,此时我们新开发了一些新的特性,需要灰度发布上线一个新的版本Service A',但是我们暂时又不希望简单地直接替换掉Service A服务,而是希望将请求头中包含foo=bar或者cookie中包含foo=bar的客户端请求转发到Service A'服务中,待运行一段时间稳定,将所有的流量切换到Service A'服务中后,再平滑地下线掉Service A服务:
场景二
假设当前线上环境我们已经有一套服务Service B对外提供7层服务,此时我们修复了一些问题,需要灰度发布上线一个新的版本Service B',但是我们又不希望简单直接地将所有客户端流量切换到新版本Service B'中,而是希望仅仅切换20%的流量到新版本Service B'中,待运行一段时间稳定,将所有的流量切换到Service B'服务中后,再平滑地下线掉Service B服务:
针对以上多种不同的应用发布需求,阿里云容器服务K8S Ingress Controller支持了多种流量切分方式:
- 基于Request Header的流量切分,适用于灰度发布以及AB测试场景
- 基于Cookie的流量切分,适用于灰度发布以及AB测试场景
- 基于Query Param的流量切分,适用于灰度发布以及AB测试场景
- 基于服务权重的流量切分,适用于蓝绿发布场景
注解说明
阿里云容器服务K8S Ingress Controller通过下列Annotation来支持应用服务的灰度发布机制:
1. nginx.ingress.kubernetes.io/service-match
该注解用来配置新版本服务的路由匹配规则,格式如下:
nginx.ingress.kubernetes.io/service-match: |
<service-name>: <match-rule>
# 参数说明:
# service-name:服务名称,满足match-rule的请求会被路由到该服务中
# match-rule:路由匹配规则
#
# 路由匹配规则:
# 1. 支持的匹配类型
# - header:基于请求头,支持正则匹配和完整匹配
# - cookie:基于cookie,支持正则匹配和完整匹配
# - query:基于请求参数,支持正则匹配和完整匹配
#
# 2. 匹配方式
# - 正则匹配格式:/{regular expression}/,//表明采用正则方式匹配
# - 完整匹配格式:"{exact expression}",""表明采用完整方式匹配
路由匹配规则配置示例:
# 请求头中满足foo正则匹配^bar$的请求被转发到新版本服务new-nginx中
new-nginx: header("foo", /^bar$/)
# 请求头中满足foo完整匹配bar的请求被转发到新版本服务new-nginx中
new-nginx: header("foo", "bar")
# cookie中满足foo正则匹配^sticky-.+$的请求被转发到新版本服务new-nginx中
new-nginx: cookie("foo", /^sticky-.+$/)
# query param中满足foo完整匹配bar的请求被转发到新版本服务new-nginx中
new-nginx: query("foo", "bar")
2. nginx.ingress.kubernetes.io/service-weight
该注解用来配置新旧版本服务的流量权重,格式如下:
nginx.ingress.kubernetes.io/service-weight: |
<new-svc-name>:<new-svc-weight>, <old-svc-name>:<old-svc-weight>
参数说明:
new-svc-name:新版本服务名称
new-svc-weight:新版本服务权重
old-svc-name:旧版本服务名称
old-svc-weight:旧版本服务权重
服务权重配置示例:
nginx.ingress.kubernetes.io/service-weight: |
new-nginx: 20, old-nginx: 60
注意
:
- 服务权重采用相对值计算方式。如示例中的服务权重设置,new-nginx服务的权重百分比为25%,old-nginx服务的权重百分比为75%。
- 一个服务组(同一个ingress yaml中具有相同Host和Path的服务)中未明确设置权重的服务默认权重值为100。
版本更新
目前阿里云容器服务K8S Ingress Controller需要0.12.0-5
及其以上版本才支持流量切分特性,可以通过下面命令来查看Ingress Controller的当前版本号:
- 采用Deployment部署的情况
kubectl -n kube-system get deploy nginx-ingress-controller -o yaml | grep -v 'apiVersion' | grep 'aliyun-ingress-controller'
- 采用DaemonSet部署的情况
kubectl -n kube-system get ds nginx-ingress-controller -o yaml | grep -v 'apiVersion' | grep 'aliyun-ingress-controller'
若您的集群内Ingress Controller当前版本号小于0.12.0-5
,那么您可以通过下面方式来升级Ingress Controller到新版本:
- 采用Deployment部署的情况
kubectl -n kube-system set image deploy/nginx-ingress-controller nginx-ingress-controller=registry.cn-hangzhou.aliyuncs.com/acs/aliyun-ingress-controller:0.12.0-5
- 采用DaemonSet部署的情况
kubectl -n kube-system set image ds/nginx-ingress-controller nginx-ingress-controller=registry.cn-hangzhou.aliyuncs.com/acs/aliyun-ingress-controller:0.12.0-5
至此,您集群内的Ingress Controller已经支持灰度发布功能。
部署服务
在应用灰度发布前,我们必须事先已经存在一*有的应用对外提供服务。这里我们部署一套nginx服务并通过Ingress Controller对外提供7层域名访问:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: old-nginx
spec:
replicas: 2
selector:
matchLabels:
run: old-nginx
template:
metadata:
labels:
run: old-nginx
spec:
containers:
- image: registry.cn-hangzhou.aliyuncs.com/xianlu/old-nginx
imagePullPolicy: Always
name: old-nginx
ports:
- containerPort: 80
protocol: TCP
restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
name: old-nginx
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
run: old-nginx
sessionAffinity: None
type: NodePort
部署ingress资源:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: gray-release
spec:
rules:
- host: www.example.com
http:
paths:
# 老版本服务
- path: /
backend:
serviceName: old-nginx
servicePort: 80
部署并测试访问情况:
kubectl get ing
NAME HOSTS ADDRESS PORTS AGE
gray-release www.example.com 47.107.20.35 80 1m
curl -H "Host: www.example.com" http://47.107.20.35
old
curl -H "Host: www.example.com" http://47.107.20.35
old
curl -H "Host: www.example.com" http://47.107.20.35
old
灰度发布新版本服务
此时我们需要发布一个新版本的nginx服务并配置仅允许部分客户端才能访问到这个新版本服务:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: new-nginx
spec:
replicas: 1
selector:
matchLabels:
run: new-nginx
template:
metadata:
labels:
run: new-nginx
spec:
containers:
- image: registry.cn-hangzhou.aliyuncs.com/xianlu/new-nginx
imagePullPolicy: Always
name: new-nginx
ports:
- containerPort: 80
protocol: TCP
restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
name: new-nginx
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
run: new-nginx
sessionAffinity: None
type: NodePort
场景一:设置满足特定规则的客户端才能访问新版本服务
假若我们希望请求头中满足foo=bar的客户端请求才能路由到新版本服务中,那么我们可以如下修改配置ingress规则:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: gray-release
annotations:
# 请求头中满足正则匹配foo=bar的请求才会被路由到新版本服务new-nginx中
nginx.ingress.kubernetes.io/service-match: |
new-nginx: header("foo", /^bar$/)
spec:
rules:
- host: www.example.com
http:
paths:
# 老版本服务
- path: /
backend:
serviceName: old-nginx
servicePort: 80
# 新版本服务
- path: /
backend:
serviceName: new-nginx
servicePort: 80
测试客户端请求:
curl -H "Host: www.example.com" http://47.107.20.35
old
curl -H "Host: www.example.com" http://47.107.20.35
old
curl -H "Host: www.example.com" http://47.107.20.35
old
curl -H "Host: www.example.com" -H "foo: bar" http://47.107.20.35
new
curl -H "Host: www.example.com" -H "foo: bar" http://47.107.20.35
new
curl -H "Host: www.example.com" -H "foo: bar" http://47.107.20.35
new
场景二:在满足特定规则的基础上设置一定比例的请求被路由到新版本服务中
假若我们希望请求头中满足foo=bar的客户端请求仅允许50%的流量被路由到新版本服务中,那么我们可以如下修改配置ingress规则:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: gray-release
annotations:
# 请求头中满足正则匹配foo=bar的请求才会被路由到新版本服务new-nginx中
nginx.ingress.kubernetes.io/service-match: |
new-nginx: header("foo", /^bar$/)
# 在满足上述匹配规则的基础上仅允许50%的流量会被路由到新版本服务new-nginx中
nginx.ingress.kubernetes.io/service-weight: |
new-nginx: 50, old-nginx: 50
spec:
rules:
- host: www.example.com
http:
paths:
# 老版本服务
- path: /
backend:
serviceName: old-nginx
servicePort: 80
# 新版本服务
- path: /
backend:
serviceName: new-nginx
servicePort: 80
测试客户端请求:
curl -H "Host: www.example.com" http://47.107.20.35
old
curl -H "Host: www.example.com" http://47.107.20.35
old
curl -H "Host: www.example.com" http://47.107.20.35
old
curl -H "Host: www.example.com" -H "foo: bar" http://47.107.20.35
new
curl -H "Host: www.example.com" -H "foo: bar" http://47.107.20.35
old
curl -H "Host: www.example.com" -H "foo: bar" http://47.107.20.35
old
curl -H "Host: www.example.com" -H "foo: bar" http://47.107.20.35
new
场景三:仅设置一定比例的请求被路由到新版本服务中
假若我们仅仅希望50%的流量被路由到新版本服务中,那么我们可以如下修改配置ingress规则:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: gray-release
annotations:
# 允许50%的流量被路由到新版本服务new-nginx中
nginx.ingress.kubernetes.io/service-weight: |
new-nginx: 50, old-nginx: 50
spec:
rules:
- host: www.example.com
http:
paths:
# 老版本服务
- path: /
backend:
serviceName: old-nginx
servicePort: 80
# 新版本服务
- path: /
backend:
serviceName: new-nginx
servicePort: 80
测试客户端访问:
curl -H "Host: www.example.com" http://47.107.20.35
new
curl -H "Host: www.example.com" http://47.107.20.35
new
curl -H "Host: www.example.com" http://47.107.20.35
new
curl -H "Host: www.example.com" http://47.107.20.35
old
curl -H "Host: www.example.com" http://47.107.20.35
old
curl -H "Host: www.example.com" http://47.107.20.35
new
curl -H "Host: www.example.com" http://47.107.20.35
old
特别说明
:当我们既没有设置service-match
也没有设置service-weight
注解时,Ingress Controller在转发请求时默认会将请求均衡地随机转发到新老版本服务中。
灰度发布完成
系统运行一段时间后,当新版本服务已经稳定并且符合预期后,我们需要将老版本的服务下线掉,仅仅保留新版本服务在线上运行,此时我们需要修改ingress规则删除相关的annotation以及老版本服务,最终规则如下:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: gray-release
spec:
rules:
- host: www.example.com
http:
paths:
# 新版本服务
- path: /
backend:
serviceName: new-nginx
servicePort: 80
测试客户端访问:
curl -H "Host: www.example.com" http://47.107.20.35
new
curl -H "Host: www.example.com" http://47.107.20.35
new
curl -H "Host: www.example.com" http://47.107.20.35
new
可以看到,现在的请求是全部被路由到了新版本的服务中,这就完成了灰度发布的整个周期。最后,您可以删除老版本的service和deployment。