K8S Ingress 实现金丝雀(灰度)发布
假设有如下三个节点的 K8S 集群:
k8s31master 是控制节点
k8s31node1、k8s31node2 是工作节点
容器运行时是 containerd
一、场景分析
阅读本文,默认您已经安装了 Ingress Nginx。
1)A/B 测试
A/B 测试基于用户请求的元信息将流量路由到新版本,这是一种基于请求内容匹配的灰度发布策略。只有匹配特定规则的请求才会被引流到新版本,常见的做法包括基于 HTTP Header 和Cookie。基于 HTTP Header 方式,例如 User-Agent 的值为 Android 的请求(来自安卓系统的请求)可以访问新版本,其他系统仍然访问旧版本。基于 Cookie 方式,Cookie 中通常包含具有业务语义的用户信息,例如普通用户可以访问新版本,VIP 用户仍然访问旧版本。
如下图所示,某服务当前版本为v1,现在新版本v2要上线。希望安卓用户可以尝鲜新功能,其他系统用户保持不变。
通过在监控平台观察旧版本与新版本的成功率、RT对比,当新版本整体服务符合预期后,即可将所有请求切换到新版本v2,最后为了节省资源,可以逐步下线到旧版本v1。
在 K8S 中,可以利用 Ingress Nginx 基于 Header 或 Cookie 进行流量切分的策略来实现 A/B 测试发布。业务使用 Header 或 Cookie 来标识不同类型的用户,我们通过配置 Ingress 来实现让带有指定 Header 或 Cookie 的请求被转发到新版本,其它的仍然转发到旧版本,从而实现将新版本灰度给部分用户。
2)金丝雀发布
金丝雀发布是将少量的请求引流到新版本上,因此部署新版本服务只需极小数的实例。验证新版本符合预期后,逐步调整流量权重比例,使得流量慢慢从老版本迁移至新版本,期间可以根据设置的流量比例,对新版本服务进行扩容,同时对老版本服务进行缩容,使得底层资源得到最大化利用。
如下图所示,某服务当前版本为 v1,现在新版本 v2 要上线。为确保流量在服务升级过程中平稳无损,采用金丝雀发布方案,逐步将流量从老版本迁移至新版本。
在 K8S 中,可以利用 Ingress Nginx 基于权重进行流量切分的策略来实现金丝雀发布。先切一部分的流量到新版本,然后对新版本进行监控,等观察一段时间稳定后再逐渐加大新版本的流量比例直至完全替换旧版本,最后再平滑下线旧版本,从而实现流量的定向分配。
二、注解介绍
Ingress Nginx 是一个 K8S Ingress 工具,支持配置 Ingress Annotations 来实现不同场景下的灰度发布和测试。
- 前提:
# 注解的键和值只能是字符串。其他类型,如布尔值或数值,必须加引号,例如:"true"、"false"、"100"。
# 开启灰度发布
nginx.ingress.kubernetes.io/canary: "true"
- Ingress Nginx Annotations 支持以下几种 Canary 规则:
nginx.ingress.kubernetes.io/canary-by-header:利用请求头,通知 Ingress 将请求路由到 Canary Ingress 中指定的服务。当请求头部设置为 always 时,请求将被路由到金丝雀版本。当头部设置为 never 时,请求永远不会被路由到金丝雀版本。对于任何其他值,头部将被忽略,请求将根据优先级与其他金丝雀规则进行比较。
nginx.ingress.kubernetes.io/canary-by-header-value:利用请求头值,通知 Ingress 将请求路由到 Canary Ingress 中指定的服务。当请求头设置为该值时,请求将被路由到金丝雀版本。对于任何其他头值,将忽略该头,并按照优先级与其他金丝雀规则进行比较。此注解必须配合使用 nginx.ingress.kubernetes.io/canary-by-header。这个注解是nginx.ingress.kubernetes.io/canary-by-header 的扩展,允许自定义请求头值而不是使用硬编码值。如果未定义 nginx.ingress.kubernetes.io/canary-by-header 注解,则它没有任何效果。
nginx.ingress.kubernetes.io/canary-by-header-pattern: 这个注解的作用与 canary-by-header-value 相同,但它使用的是 PCRE 正则表达式匹配。注意,当设置了 canary-by-header-value 时,这个注解将被忽略。如果给定的正则表达式在请求处理过程中导致错误,该请求将被认为不匹配。
nginx.ingress.kubernetes.io/canary-by-cookie:利用 cookie,通知 Ingress 将请求路由到 Canary Ingress 中指定的服务。当 cookie 值设置为 always 时,请求将始终路由到金丝雀版本。当 cookie 设置为 never 时,请求永远不会路由到金丝雀版本。对于任何其他值,将忽略 cookie,并根据优先级将请求与其他金丝雀规则进行比较。
nginx.ingress.kubernetes.io/canary-weight:整数(0-)百分比的随机请求将会被路由到金丝雀 Ingress 中指定的服务。权重为 0 表示该金丝雀规则不会将任何请求发送到金丝雀 Ingress 中的服务。权重为 <weight-total> 表示所有请求都将发送到 Ingress 中指定的备用服务。 <weight-total> 默认为 100,可以通过 nginx.ingress.kubernetes.io/canary-weight-total 进行增加。
nginx.ingress.kubernetes.io/canary-weight-total:流量的总权重。如果未指定,默认为 100。
金丝雀规则的评估顺序遵循优先级。
优先级顺序如下:按头部信息金丝雀 -> 按Cookie金丝雀 -> 权重金丝雀
三、实验准备
-
镜像下载
[root@k8s31node1 ~]# ctr -n=k8s.io images pull swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/openresty/openresty:latest
[root@k8s31node1 ~]# ctr -n=k8s.io images tag swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/openresty/openresty:latest docker.io/openresty/openresty:latest[root@k8s31node2 ~]# ctr -n=k8s.io images pull swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/openresty/openresty:latest
[root@k8s31node2 ~]# ctr -n=k8s.io images tag swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/openresty/openresty:latest docker.io/openresty/openresty:latest
-
部署 v1
apiVersion: apps/v1
kind: Deployment
metadata:name: nginx-v1
spec:replicas: 1selector:matchLabels:app: nginxversion: v1template:metadata:labels:app: nginxversion: v1spec:containers:- name: nginximage: "openresty/openresty:latest"imagePullPolicy: IfNotPresentports:- name: httpprotocol: TCPcontainerPort: 80volumeMounts:- mountPath: /usr/local/openresty/nginx/conf/nginx.confname: configsubPath: nginx.confvolumes:- name: configconfigMap:name: nginx-v1
---
apiVersion: v1
kind: ConfigMap
metadata:labels:app: nginxversion: v1name: nginx-v1
data:nginx.conf: |-worker_processes 1;events {accept_mutex on;multi_accept on;use epoll;worker_connections 1024;}http {ignore_invalid_headers off;server {listen 80;location / {access_by_lua 'local header_str = ngx.say("nginx-v1")';}}}
---
apiVersion: v1
kind: Service
metadata:name: nginx-v1
spec:type: ClusterIPports:- port: 80protocol: TCPname: httpselector:app: nginxversion: v1
该 yml 定义了三个资源 ConfigMap、Deployment、Service。
- ConfigMap 定义了一个 nginx.conf 配置文件,使用 lua 脚本输出 nginx-v1。
- Deployment 定义了一个 Pod,里面运行 openresty 它是一个封装了 nginx+lua 的 web 服务器。Pod 有两个标签 app: nginx、version: v1。
- Service 代理了 Deployment 运行的 Pod。
部署 v2
apiVersion: apps/v1
kind: Deployment
metadata:name: nginx-v2
spec:replicas: 1selector:matchLabels:app: nginxversion: v2template:metadata:labels:app: nginxversion: v2spec:containers:- name: nginximage: "openresty/openresty:latest"imagePullPolicy: IfNotPresentports:- name: httpprotocol: TCPcontainerPort: 80volumeMounts:- mountPath: /usr/local/openresty/nginx/conf/nginx.confname: configsubPath: nginx.confvolumes:- name: configconfigMap:name: nginx-v2
---
apiVersion: v1
kind: ConfigMap
metadata:labels:app: nginxversion: v2name: nginx-v2
data:nginx.conf: |-worker_processes 1;events {accept_mutex on;multi_accept on;use epoll;worker_connections 1024;}http {ignore_invalid_headers off;server {listen 80;location / {access_by_lua 'local header_str = ngx.say("nginx-v2")';}}}
---
apiVersion: v1
kind: Service
metadata:name: nginx-v2
spec:type: ClusterIPports:- port: 80protocol: TCPname: httpselector:app: nginxversion: v2
创建 v1 ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:name: nginx
spec:ingressClassName: nginxrules:- host: canary.example.comhttp:paths:- path: / pathType: Prefixbackend: #配置后端服务service:name: nginx-v1port:number: 80
对外暴露域名 canary.example.com 访问。
修改本机 hosts
192.168.40.20 canary.example.com
浏览器访问
四、实战
1)nginx.ingress.kubernetes.io/canary-by-header
- 创建 v2 ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:annotations:nginx.ingress.kubernetes.io/canary: "true" # 开启金丝雀nginx.ingress.kubernetes.io/canary-by-header: "Canary"name: nginx-canary
spec:ingressClassName: nginxrules:- host: canary.example.comhttp:paths:- path: /pathType: Prefixbackend: #配置后端服务service:name: nginx-v2port:number: 80
现在系统里面有两个 ingress,一个 v1 版本,一个 v2 金丝雀版本。
注意:
ingress 要 ADDRESS 那一栏出来才能访问。
curl -H "Host: canary.example.com" -H "Canary: always" 192.168.40.20
请求头参数 Canary 匹配 always,走金丝雀版本服务。
请求头参数 Canary 不匹配 always,走 v1 服务。
2) nginx.ingress.kubernetes.io/canary-by-header-value
删掉上一个 ingress,以免干扰下面实验。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:annotations:nginx.ingress.kubernetes.io/canary: "true"nginx.ingress.kubernetes.io/canary-by-header: "Canary"nginx.ingress.kubernetes.io/canary-by-header-value: "v2"name: nginx-canary
spec:ingressClassName: nginxrules:- host: canary.example.comhttp:paths:- path: /pathType: Prefixbackend: #配置后端服务service:name: nginx-v2port:number: 80
请求头参数 Canary 匹配 v2,走金丝雀版本服务。
curl -H "Host: canary.example.com" -H "Canary: v1" 192.168.40.20
3) nginx.ingress.kubernetes.io/canary-by-header-pattern
删掉上一个 ingress,以免干扰下面实验。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:annotations:nginx.ingress.kubernetes.io/canary: "true"nginx.ingress.kubernetes.io/canary-by-header: "Canary"nginx.ingress.kubernetes.io/canary-by-header-pattern: "v2|v3" # 匹配v2或v3name: nginx-canary
spec:ingressClassName: nginxrules:- host: canary.example.comhttp:paths:- path: /pathType: Prefixbackend: #配置后端服务service:name: nginx-v2port:number: 80
请求头参数 Canary 匹配 v2 或 v3,走金丝雀版本服务。
curl -H "Host: canary.example.com" -H "Canary: v1" 192.168.40.20
4)nginx.ingress.kubernetes.io/canary-by-cookie
删掉上一个 ingress,以免干扰下面实验。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:annotations:nginx.ingress.kubernetes.io/canary: "true"nginx.ingress.kubernetes.io/canary-by-cookie: "Canary"name: nginx-canary
spec:ingressClassName: nginxrules:- host: canary.example.comhttp:paths:- path: /pathType: Prefixbackend: #配置后端服务service:name: nginx-v2port:number: 80
cookie 参数 Canary 匹配 always,走金丝雀版本服务。
cookie 参数 Canary 不匹配 always,走 v1 服务。
curl -H "Host: canary.example.com" -H "Cookie: Canary=always" 192.168.40.20
5)nginx.ingress.kubernetes.io/canary-weight
删掉上一个 ingress,以免干扰下面实验。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:annotations:nginx.ingress.kubernetes.io/canary: "true"nginx.ingress.kubernetes.io/canary-weight: "10"name: nginx-canary
spec:ingressClassName: nginxrules:- host: canary.example.comhttp:paths:- path: /pathType: Prefixbackend: #配置后端服务service:name: nginx-v2port:number: 80
10%的流量打到金丝雀服务。
for i in {1..10}; do curl -H "Host: canary.example.com" 192.168.40.20; done;
五、金丝雀比较
实现金丝雀发布的方式有很多,从Java程序员的角度来看,就有:
基于 Spring Cloud Gateway 路由断言工厂、基于 Nginx、基于 K8S Deployment 伪金丝雀、基于 Ingress Nginx 注解、基于 Istio 流量切分。
基于 Spring Cloud Gateway 路由断言工厂:路由规则变化很难做到实时响应,要实现实时响应代码实现复杂。
基于 Nginx:要有很多的配置。
基于 K8S Deployment 伪金丝雀:没有实现流量的切分。
基于 Istio 流量切分:技术栈门槛高。需要对于服务网格的一整套有所了解。
综上来看,基于 Ingress Nginx 注解 是配置最简单的方式了。