ingress 灰度发版 ¶
在日常工作中我们经常需要对服务进行版本更新升级,所以我们经常会使用到滚动升级、蓝绿发布、灰度发布等不同的发布操作。而 ingress-nginx 支持通过 Annotations 配置来实现不同场景下的灰度发布和测试,可以满足金丝雀发布、蓝绿部署与 A/B 测试等业务场景。
ingress-nginx 的 Annotations 支持以下 4 种 Canary 规则 | 解释 |
---|---|
nginx.ingress.kubernetes.io/canary-by-header: |
基于 Request Header 的流量切分,适用于灰度发布以及 A/B 测试。当 Request Header 设置为 always 时,请求将会被一直发送到 Canary 版本;当 Request Header 设置为 never时,请求不会被发送到 Canary 入口;对于任何其他 Header 值,将忽略 Header,并通过优先级将请求与其他金丝雀规则进行优先级的比较。 |
nginx.ingress.kubernetes.io/canary-by-header-value: |
要匹配的 Request Header 的值,用于通知 Ingress 将请求路由到 Canary Ingress 中指定的服务。当 Request Header 设置为此值时,它将被路由到 Canary 入口。该规则允许用户自定义 Request Header 的值,必须与上一个 annotation (即:canary-by-header) 一起使用。 |
nginx.ingress.kubernetes.io/canary-weight: |
基于服务权重的流量切分,适用于蓝绿部署,权重范围 0 - 100 按百分比将请求路由到 Canary Ingress 中指定的服务。权重为 0 意味着该金丝雀规则不会向 Canary 入口的服务发送任何请求,权重为 100 意味着所有请求都将被发送到 Canary 入口。 |
nginx.ingress.kubernetes.io/canary-by-cookie: |
基于 cookie 的流量切分,适用于灰度发布与 A/B 测试。用于通知 Ingress 将请求路由到 Canary Ingress 中指定的服务的cookie。当 cookie 值设置为 always 时,它将被路由到 Canary 入口;当 cookie 值设置为 never 时,请求不会被发送到 Canary 入口;对于任何其他值,将忽略 cookie 并将请求与其他金丝雀规则进行优先级的比较。 |
金丝雀规则按优先顺序进行排序:canary-by-header - > canary-by-cookie - > canary-weight
以上的 四 个 annotation 规则划分为以下两类:
-
基于权重的 Canary (金丝雀) 规则
-
基于用户请求的 Canary (金丝雀) 规则
部署 Production (生产环境) 和 Canary (金丝雀) 版本应用 ¶
apiVersion: apps/v1
kind: Deployment
metadata:
name: production
labels:
app: production
spec:
replicas: 1
selector:
matchLabels:
app: production
template:
metadata:
labels:
app: production
spec:
containers:
- name: production
image: mirrorgooglecontainers/echoserver:1.10
ports:
- containerPort: 8080
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
---
apiVersion: v1
kind: Service
metadata:
name: production
labels:
app: production
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: production
[root@k8smaster002 ingress]# kubectl apply -f production-deployment.yml
deployment.apps/production created
service/production created
[root@k8smaster002 ingress]# kubectl get pod -l app=production
NAME READY STATUS RESTARTS AGE
production-64cfc46b65-2459s 1/1 Running 0 87s
[root@k8smaster002 ingress]# kubectl get svc -l app=production
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
production ClusterIP 10.97.100.168 <none> 80/TCP 3m37s
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: production
spec:
ingressClassName: nginx
rules:
- host: production.k8s.huichengcheng.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: production
port:
number: 80
[root@k8smaster002 ingress]# kubectl apply -f production.ingress.yml
ingress.networking.k8s.io/production created
[root@k8smaster002 ingress]# kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
production nginx production.k8s.huichengcheng.com 192.168.3.204 80 75s
[root@k8smaster002 ingress]# kubectl describe ingress
Name: production
Labels: <none>
Namespace: default
Address: 192.168.3.204
Ingress Class: nginx
Default backend: <default>
Rules:
Host Path Backends
---- ---- --------
production.k8s.huichengcheng.com
/ production:80 (10.244.35.58:8080)
Annotations: <none>
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Sync 2m5s (x2 over 2m25s) nginx-ingress-controller Scheduled for sync
❯ curl https://production.k8s.huichengcheng.com:60000/
Hostname: production-64cfc46b65-2459s
Pod Information:
node name: k8sworker002
pod name: production-64cfc46b65-2459s
pod namespace: default
pod IP: 10.244.35.58
Server values:
server_version=nginx: 1.13.3 - lua: 10008
Request Information:
client_address=10.244.87.252
method=GET
real path=/
query=
request_version=1.1
request_scheme=http
request_uri=http://production.k8s.huichengcheng.com:8080/
Request Headers:
accept=*/*
accept-encoding=gzip
host=production.k8s.huichengcheng.com:60000
user-agent=curl/7.81.0
x-forwarded-for=192.168.3.202
x-forwarded-host=production.k8s.huichengcheng.com:60000
x-forwarded-port=80
x-forwarded-proto=http
x-forwarded-scheme=http
x-original-forwarded-for=192.168.1.99
x-real-ip=192.168.3.202
x-request-id=14976def2dea3555cbf3a3c2060ef6ba
x-scheme=http
Request Body:
-no body in request-
apiVersion: apps/v1
kind: Deployment
metadata:
name: canary
labels:
app: canary
spec:
replicas: 1
selector:
matchLabels:
app: canary
template:
metadata:
labels:
app: canary
spec:
containers:
- name: canary
image: mirrorgooglecontainers/echoserver:1.10
ports:
- containerPort: 8080
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
---
apiVersion: v1
kind: Service
metadata:
name: canary
labels:
app: canary
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: http
selector:
app: canary
[root@k8smaster002 ingress]# kubectl apply -f Canary-deployment.yml
deployment.apps/canary created
service/canary created
[root@k8smaster002 ingress]# kubectl get pod -l app=canary
NAME READY STATUS RESTARTS AGE
canary-7d97679b67-9dn5c 1/1 Running 0 18s
[root@k8smaster002 ingress]# kubectl get svc -l app=canary
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
canary ClusterIP 10.110.80.240 <none> 80/TCP 37s
配置 Annotation 规则进行流量切分 ¶
基于权重 ¶
基于权重的流量切分的典型应用场景就是蓝绿部署,可通过将权重设置为 0 或 100 来实现。例如,可将 Green 版本设置为主要部分,并将 Blue 版本的入口配置为 Canary。最初,将权重设置为 0,因此不会将流量代理到 Blue 版本。一旦新版本测试和验证都成功后,即可将 Blue 版本的权重设置为 100,即所有流量从 Green 版本转向 Blue。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: canary
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "30"
spec:
ingressClassName: nginx
rules:
- host: production.k8s.huichengcheng.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: canary
port:
number: 80
说明: 应用的 Canary 版本基于权重 (30%) 进行流量切分后,访问到 Canary 版本的概率接近 30%,流量比例可能会有小范围的浮动。
❯ for i in $(seq 1 10); do curl -s https://production.k8s.huichengcheng.com:60000 | grep "Hostname"; done
Hostname: production-64cfc46b65-2459s
Hostname: production-64cfc46b65-2459s
Hostname: production-64cfc46b65-2459s
Hostname: production-64cfc46b65-2459s
Hostname: canary-7d97679b67-9dn5c
Hostname: canary-7d97679b67-9dn5c
Hostname: canary-7d97679b67-9dn5c
Hostname: production-64cfc46b65-2459s
Hostname: production-64cfc46b65-2459s
Hostname: production-64cfc46b65-2459s
基于 Request Header ¶
基于 Request Header 进行流量切分的典型应用场景即灰度发布或 A/B 测试场景。
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/canary: "true" # 要开启灰度发布机制,首先需要启用 Canary
nginx.ingress.kubernetes.io/canary-by-header: canary # 基于header的流量切分
nginx.ingress.kubernetes.io/canary-weight: "30" # 会被忽略,因为配置了 canary-by-headerCanary版本
更新上面的 Ingress 资源对象后,我们在请求中加入不同的 Header 值,再次访问应用的域名。
注意:当 Request Header 设置为 never 或 always 时,请求将不会或一直被发送到 Canary 版本,对于任何其他 Header 值,将忽略 Header,并通过优先级将请求与其他 Canary 规则进行优先级的比较。
❯ for i in $(seq 1 10); do curl -s -H "canary: never" https://production.k8s.huichengcheng.com:60000 | grep "Hostname"; done
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
# 请求的时候设置了 canary: never 这个 Header 值,所以请求没有发送到 Canary 应用中去
❯ for i in $(seq 1 10); do curl -s -H "canary: never" https://production.k8s.huichengcheng.com:60000 | grep "Hostname"; done
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
# 请求的时候设置了 canary: never 这个 Header 值,所以请求没有发送到 Canary 应用中去
❯ for i in $(seq 1 10); do curl -s -H "canary: user-value" https://production.k8s.huichengcheng.com:60000 | grep "Hostname"; done
Hostname: production-64cfc46b65-6wkqx
Hostname: canary-7d97679b67-klfq7
Hostname: production-64cfc46b65-6wkqx
Hostname: canary-7d97679b67-klfq7
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: canary-7d97679b67-klfq7
# ingress-nginx 会通过优先级将请求与其他 Canary 规则进行优先级的比较,我们这里也就会进入 canary-weight: "30" 这个规则去
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/canary: "true" # 要开启灰度发布机制,首先需要启用 Canary
nginx.ingress.kubernetes.io/canary-by-header-value: user-value
nginx.ingress.kubernetes.io/canary-by-header: canary # 基于header的流量切分
nginx.ingress.kubernetes.io/canary-weight: "30" # 分配30%流量到当前Canary版本
# ingress-nginx 会通过优先级将请求与其他 Canary 规则进行优先级的比较,我们这里也就会进入 canary-weight: "30" 这个规则去
❯ for i in $(seq 1 10); do curl -s -H "canary: user-value" https://production.k8s.huichengcheng.com:60000 | grep "Hostname"; done
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
基于 Cookie ¶
与基于 Request Header 的 annotation 用法规则类似。例如在 A/B 测试场景下,需要让地域为北京的用户访问 Canary 版本。那么当 cookie 的 annotation 设置为 nginx.ingress.kubernetes.io/canary-by-cookie: "users_from_Beijing",此时后台可对登录的用户请求进行检查,如果该用户访问源来自北京则设置 cookie users_from_Beijing 的值为 always,这样就可以确保北京的用户仅访问 Canary 版本。
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/canary: "true" # 要开启灰度发布机制,首先需要启用 Canary
nginx.ingress.kubernetes.io/canary-by-cookie: "users_from_Beijing" # 基于 cookie
nginx.ingress.kubernetes.io/canary-weight: "30" # 会被忽略,因为配置了 canary-by-cookie
❯ for i in $(seq 1 10); do curl -s -b "users_from_Beijing=always" https://production.k8s.huichengcheng.com:60000 | grep "Hostname"; done
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
Hostname: canary-7d97679b67-klfq7
# 应用都被路由到了 Canary 版本的应用中去了
❯ for i in $(seq 1 10); do curl -s -b "users_from_Beijing=never" https://production.k8s.huichengcheng.com:60000 | grep "Hostname"; done
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
Hostname: production-64cfc46b65-6wkqx
# 如果我们将这个 Cookie 值设置为 never,则不会路由到 Canary 应用中