假設有如下三個節點的?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 流量切分。基于 K8S Gateway API。
基于 Spring Cloud Gateway 路由斷言工廠:路由規則變化很難做到實時響應,要實現實時響應代碼實現復雜。
基于 Nginx:要有很多的配置。
基于 K8S Deployment 偽金絲雀:沒有實現流量的切分。
基于 Istio 流量切分:技術棧門檻高。需要對于服務網格的一整套有所了解。
綜上來看,基于 K8S Gateway API?是配置最簡單的方式了。
Ingress Nginx 注解,實現金絲雀還需要有兩個 Ingress,一個分發舊版本流量,一個分發灰度版本流量。K8S Gateway API 只需要一個。