Istio 結合 Flagger 進行灰度發布
灰度發布也叫金絲雀部署 ,是指通過控制流量的比例,實現新老版本的逐步替換。比如對于服務 A 有兩個版本(藍和綠兩個版本),當前兩個版本同時部署,但是 version1 比例 90% ,version2 比例 10% ,然后我們可以觀察 version2 的實際運行效果,如果符合預期,則可以逐步調整流量占比,比如調整為 80:20 -> 70:30 -> 10:90 -> 0:100 ,最終 version1 版本下線,全部替換成 version2 版本。如果驗證失敗,切換 100%流量回 v1 版本(回滾)。灰度發布的特點是:
flagger
在 Istio 中要實現灰度發布有多種方案,比如 Flagger、Argo Rollouts 等。
Flagger
Flagger 是一個漸進式交付的 Kubernetes Operator,它可以自動執行 Kubernetes 上運行的應用程序的發布過程。它通過在測量指標和運行一致性測試的同時逐漸將流量轉移到新版本,降低了在生產中引入新軟件版本的風險。
Flagger 通過使用服務網格(App Mesh、Istio、Linkerd、Kuma、Open Service Mesh)或 Ingress 控制器(Contour、Gloo、NGINX、Skipper、 Traefik、APISIX)用于流量路由。對于發布分析,Flagger 可以查詢 Prometheus、InfluxDB、Datadog、New Relic、CloudWatch、Stackdriver 或 Graphite,并使用 Slack、MS Teams、Discord 和 Rocket 來發出警報。
Flagger
Flagger 可以使用 Kubernetes CRD 進行配置,并且與任何為 Kubernetes 制作的 CI/CD 解決方案兼容。由于 Flagger 是聲明性的對 Kubernetes 事件做出反應,因此它可以與諸如此類的工具一起在 GitOps 管道中使用。
安裝 Flagger
要使用 Flagger,需要先選擇一個受支持的路由提供商(比如我們這里使用 Istio),然后使用 Helm 或 Kustomize 安裝 Flagger。
Flagger 需要 Kubernetes 集群 v1.16 或更高版本以及 Istio v1.5 或更高版本。
首先當然需要安裝 Istio,并開啟 Prometheus 插件:
# demo 或者 default 都可以
istioctl manifest install --set profile=demo -y
# istio 根目錄
kubectl apply -f samples/addons/prometheus.yaml
然后在 istio-system 命名空間安裝 Flagger:
$ git clone https://github.com/fluxcd/flagger && cd flagger
$ kubectl apply -k kustomize/istio
$ kubectl get pods -n istio-system -l app=flagger
NAME READY STATUS RESTARTS AGE
flagger-ff76bfdff-kkcmz 1/1 Running 0 17m
測試應用
下面我們創建一個名為 test 的命名空間,并為其啟用 Istio sidecar 自動注入:
kubectl create ns test
kubectl label namespace test istio-injectinotallow=enabled
接下來我們使用 flagger 官方提供的 podinfo 應用來進行測試:
kubectl apply -k kustomize/podinfo
該命令會為 podinfo 應用創建對應的 Deployment 和一個 HPA 對象。
$ kubectl get deployment -n test
NAME READY UP-TO-DATE AVAILABLE AGE
podinfo 2/2 2 0 96s
$ kubectl get hpa -n test
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
podinfo Deployment/podinfo <unknown>/99% 2 4 2 60s
部署后,我們可以看到 podinfo 應用的容器數量已經變成了 2 個(自動注入了 istio sidecar),而且 HPA 也已經生效。
```bash
$ kubectl get pods -n test
NAME READY STATUS RESTARTS AGE
podinfo-584c4546df-fzw4d 0/2 PodInitializing 0 5m26s
podinfo-584c4546df-n28cf 0/2 PodInitializing 0 5m11s
接著我們再部署一個負載測試服務用于在金絲雀分析期間生成流量:
kubectl apply -k kustomize/tester
創建金絲雀
接下來我們就可以創建一個 Canary 自定義資源來實現我們的金絲雀發布了。Canary 對象是 Flagger 的核心,它描述了金絲雀發布的目標。如下所示,我們為 podinfo 應用創建一個 Canary 對象:
# podinfo-canary.yaml
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: podinfo
namespace: test
spec:
targetRef: # deployment 引用
apiVersion: apps/v1
kind: Deployment
name: podinfo
progressDeadlineSeconds: 60 # 金絲雀部署升級最大處理時間(以秒為單位)(默認600秒)
autoscalerRef: # HPA 引用(可選)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
name: podinfo
service:
port: 9898
targetPort: 9898
gateways: # Istio 網關(可選)
- istio-system/public-gateway
hosts: # VirtualService 主機名 (optional)
- podinfo.k8s.local
trafficPolicy: # Istio 流量策略(可選)
tls:
# use ISTIO_MUTUAL when mTLS is enabled
mode: DISABLE
retries: # Istio 重試策略(可選)
attempts: 3
perTryTimeout: 1s
retryOn: "gateway-error,connect-failure,refused-stream"
analysis: # 金絲雀分析
interval: 1m # 金絲雀分析間隔時間(默認 60s)
threshold: 5 # 金絲雀分析失敗閾值(默認 5)
maxWeight: 50 # 金絲雀最大流量權重(默認 50)
stepWeight: 10 # 金絲雀流量權重步長(默認 10)
metrics:
- name: request-success-rate
# minimum req success rate (non 5xx responses)
# percentage (0-100)
thresholdRange:
min: 99
interval: 1m
- name: request-duration
# maximum req duration P99
# milliseconds
thresholdRange:
max: 500
interval: 30s
# testing (optional)
webhooks:
- name: acceptance-test
type: pre-rollout
url: http://flagger-loadtester.test/
timeout: 30s
metadata:
type: bash
cmd: "curl -sd 'test' http://podinfo-canary:9898/token | grep token"
- name: load-test
url: http://flagger-loadtester.test/
timeout: 5s
metadata:
# 使用 hey 工具對 podinfo-canary 進行為期1分鐘的負載測試,每秒發送10個請求,且測試過程中會維持2個并發連接。
cmd: "hey -z 1m -q 10 -c 2 http://podinfo-canary.test:9898/"
上面的配置文件中,我們定義了 podinfo 應用的金絲雀發布策略,其中 targetRef 指定了要進行金絲雀發布的 Deployment 對象,service 指定了金絲雀發布的服務,analysis 指定了金絲雀分析策略,這里我們指定了兩個內置的指標檢查:request-success-rate 和 request-duration,其中 request-success-rate 指定了 HTTP 請求成功率,request-duration 指定了請求持續時間。對于每個指標,你可以使用 thresholdRange 和窗口大小或時間序列指定可接受的值范圍和時間間隔。內置檢查適用于每個服務網格/Ingress 控制器,并通過 Prometheus 查詢實現。
在 service 中我們指定了 Istio 的 Gateway(istio-system/public-gateway)以及 VirtualService 要使用的主機名,首先我們可以為該應用創建一個 Gateway 對象:
# public-gateway.yaml
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: public-gateway
namespace: istio-system
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "*"
另外我們在上面的對象中通過 webhooks 字段指定了金絲雀分析期間要執行的測試,其中 acceptance-test 用于在金絲雀分析開始之前執行,load-test 用于在金絲雀分析期間執行。
金絲雀部署
接下來我們可以直接創建 Canary 對象了:
kubectl apply -f podinfo-canary.yaml
當創建了 Canary 對象后,Flagger 會自動創建一個名為 pod-info-primary 的 Deployment 以及兩個版本的 Service 對象:
$ kubectl get deploy -n test
NAME READY UP-TO-DATE AVAILABLE AGE
flagger-loadtester 1/1 1 1 42m
podinfo 0/0 0 0 46m
podinfo-primary 2/2 2 2 7m3s
$ kubectl get svc -ntest
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
flagger-loadtester ClusterIP 10.106.172.190 <none> 80/TCP 35m
podinfo-canary ClusterIP 10.101.184.213 <none> 9898/TCP 39s
podinfo-primary ClusterIP 10.110.105.36 <none> 9898/TCP 39s
可以看到我們原本的 podinfo 應用已經從 podinfo 這個 Deployment 遷移到了 podinfo-primary 這個 Deployment 之上。
此外還有 Istio 相關的對象:
$ kubectl get vs -ntest
NAME GATEWAYS HOSTS AGE
podinfo ["istio-system/public-gateway"] ["podinfo.k8s.local","podinfo"] 91s
$ kubectl get dr -ntest
NAME HOST AGE
podinfo-canary podinfo-canary 95s
podinfo-primary podinfo-primary 95s
我們可以查看下自動生成的 VirtualService 對象:
$ kubectl get vs -ntest podinfo -oyaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: podinfo
namespace: test
spec:
gateways:
- istio-system/public-gateway
hosts:
- podinfo.k8s.local
- podinfo
http:
- retries:
attempts: 3
perTryTimeout: 1s
retryOn: gateway-error,connect-failure,refused-stream
route:
- destination:
host: podinfo-primary
weight: 100
- destination:
host: podinfo-canary
weight: 0
從上面的配置中我們可以看到當前 podinfo 應用的流量全部被路由到了 podinfo-primary 對象上,而 podinfo-canary 對象的流量權重為 0,當然同樣可以查看 DestinationRule 對象:
$ kubectl get dr podinfo-primary -ntest -oyaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: podinfo-primary
namespace: test
spec:
host: podinfo-primary
trafficPolicy:
tls:
mode: DISABLE
$ kubectl get dr podinfo-canary -ntest -oyaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: podinfo-canary
namespace: test
spec:
host: podinfo-canary
trafficPolicy:
tls:
mode: DISABLE
所以默認情況下現在我們訪問到的就是 podinfo-primary 這個 Deployment 對象,也就是目前的默認版本。我們可以在瀏覽器中訪問 podinfo 來查看當前的版本:
podinfo
自動金絲雀發布
我們可以看到現在的版本是 podinfo v6.0.0,接下來我們來升級應用觸發金絲雀發布。要觸發金絲雀發布,可以由以下任何對象的更改來觸發:
- Deployment PodSpec(容器鏡像、命令、端口、環境變量、資源等)
- 作為卷掛載或映射到環境變量的 ConfigMaps
- 作為卷掛載或映射到環境變量的 Secrets
比如我們可以直接修改 Deployment 對象的鏡像版本來觸發自動化的金絲雀發布:
kubectl -n test set image deployment/podinfo podinfod=ghcr.io/stefanprodan/podinfo:6.0.1
Flagger 檢測到 Deployment 更改后就會開始新的部署:
$ kubectl describe canaries podinfo -ntest
# ......
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning Synced 15m flagger podinfo-primary.test not ready: waiting for rollout to finish: observed deployment generation less than desired generation
Normal Synced 14m (x2 over 15m) flagger all the metrics providers are available!
Normal Synced 14m flagger Initialization done! podinfo.test
Normal Synced 56s flagger New revision detected! Scaling up podinfo.test
需要注意在金絲雀分析期間對 Deployment 應用新的更改,Flagger 將重新啟動分析。第一步是先會去擴容 podinfo 應用:
$ kubectl get pods -ntest
NAME READY STATUS RESTARTS AGE
flagger-loadtester-78dd9787d4-dq5fc 2/2 Running 0 67m
podinfo-5d5dbc4d84-f2mp6 2/2 Running 0 31s
podinfo-5d5dbc4d84-gd8ln 2/2 Running 0 31s
podinfo-primary-64f865cf4-bhr79 2/2 Running 0 3m31s
podinfo-primary-64f865cf4-tgsdj 2/2 Running 0 3m31s
然后就會根據我們在 Canary 對象中定義的金絲雀分析策略來進行分析,并一步步將金絲雀版本的權重提高。
Warning Synced 4m4s flagger podinfo-primary.test not ready: waiting for rollout to finish: observed deployment generation less than desired generation
Normal Synced 3m4s (x2 over 4m4s) flagger all the metrics providers are available!
Normal Synced 3m4s flagger Initialization done! podinfo.test
Normal Synced 64s flagger New revision detected! Scaling up podinfo.test
Normal Synced 4s flagger Starting canary analysis for podinfo.test
Normal Synced 4s flagger Pre-rollout check acceptance-test passed
Normal Synced 4s flagger Advance podinfo.test canary weight 10
最后會自動將流量全部切換到金絲雀版本上。
金絲雀版本
整個過程就是通過控制 VirtualService 的權重來實現的金絲雀發布。
自動回滾
在金絲雀分析期間,我們可以生成 HTTP 500 錯誤和高延遲來測試 Flagger 是否暫停發布。
比如我們觸發另一個金絲雀發布:
kubectl -n test set image deployment/podinfo podinfod=ghcr.io/stefanprodan/podinfo:6.0.2
然后進入 loadtester 容器:
kubectl -n test exec -it flagger-loadtester-xx-xx sh
使用下面的命令來生成 HTTP 500 錯誤:
watch curl http://podinfo-canary:9898/status/500
也可以添加延遲:
watch curl http://podinfo-canary:9898/delay/1
當失敗檢查的次數達到金絲雀分析配置的閾值時,流量將路由回主節點,金絲雀將縮放為零,并將部署標記為失敗。
Normal Synced 8m10s (x3 over 45m) flagger New revision detected! Scaling up podinfo.test
Normal Synced 7m10s (x2 over 44m) flagger Pre-rollout check acceptance-test passed
Normal Synced 7m10s (x2 over 44m) flagger Advance podinfo.test canary weight 10
Normal Synced 7m10s (x2 over 44m) flagger Starting canary analysis for podinfo.test
Warning Synced 6m10s flagger Halt podinfo.test advancement success rate 55.86% < 99%
Warning Synced 5m10s flagger Halt podinfo.test advancement success rate 97.61% < 99%
Warning Synced 4m10s flagger Halt podinfo.test advancement success rate 8.00% < 99%
Warning Synced 3m10s flagger Halt podinfo.test advancement success rate 98.13% < 99%
Warning Synced 2m10s flagger Halt podinfo.test advancement success rate 7.69% < 99%
Warning Synced 70s (x2 over 14m) flagger Canary failed! Scaling down podinfo.test
Warning Synced 70s flagger Rolling back podinfo.test failed checks threshold reached 5
關于 Flagger 的更多使用請關注后續文章。