跳至主要内容

Deployment Strategies - Canary Strategy

Canary 部署(金絲雀部署)是一種漸進式部署策略,用於將新版本的應用程式逐步推出到生產環境中,並在此過程中觀察其行為和性能表現。 這種策略的靈感來自於金絲雀在礦井中的應用,即在全面實施之前先進行小範圍測試,以降低風險。


Nginx Ingress 金絲雀部署功能介紹

在 NGINX Ingress Controller 中,Canary Strategy 是一種基於第七層協議(HTTP/HTTPS)的流量分流功能,用於將部分用戶流量分配到新的應用版本上進行測試。

這種策略通過分析 HTTP 標頭、Cookie 或路由規則等應用層特徵,靈活控制流量的分配。開發者可以根據業務需求將一定比例的流量、特定用戶群組或特定請求導向 Canary 版本,而其他流量則繼續訪問穩定版本。

這樣的方式允許在不影響大多數用戶的情況下,驗證新版本的穩定性和性能,確保應用的高可用性與安全性。

註解說明

註解功能描述範例值
nginx.ingress.kubernetes.io/canary啟用 Canary 模式,當設為 "true" 時,Ingress 資源會作為 Canary 配置運行。"true"
nginx.ingress.kubernetes.io/canary-weight定義導向 Canary 版本的流量百分比,範圍為 0-100,用於基於比例的流量分配。"10" (10% 流量導向 Canary 版本)
nginx.ingress.kubernetes.io/canary-by-header根據特定的 HTTP 標頭(Header)來進行 Canary 流量分配,需搭配以下標頭值或正則表達式使用。"x-canary"
nginx.ingress.kubernetes.io/canary-by-header-value定義當標頭的值符合此值時,流量將被導向 Canary 版本,適合精確匹配。"1""true"
nginx.ingress.kubernetes.io/canary-by-header-pattern使用正則表達式匹配 HTTP 標頭的值,當匹配成功時,將流量導向 Canary 版本,適合更靈活的條件判斷。^(canary-user)$
nginx.ingress.kubernetes.io/canary-by-cookie根據指定的 Cookie 名稱進行 Canary 流量分配,當用戶的 Cookie 符合指定名稱時,流量將被導向 Canary 版本。"canary_cookie"

以上設定的優先級別由高到低分別為:canary-by-header > canary-by-cookie > canary-weight


Canary Strategy 更新服務的步驟

Canary Strategy 更新服務的步驟通常包括以下幾個階段,這些步驟能幫助您平穩地將流量切換到新版本並進行測試:

1. 設定 Canary 版本

在 Kubernetes 中,您需要將應用程式部署為 Canary 版本。這通常會涉及以下配置:

  • 創建新的 Deployment 或更新現有 Deployment:部署新版本的應用程式,這通常是以新版本的 Docker 映像檔來更新。
  • 配置 Ingress 控制器:使用 NGINX Ingress 或其他支持 Canary 部署的控制器,通過設置 nginx.ingress.kubernetes.io/canary 註解來標記 Canary 版本。您還可以設置流量分配比例(canary-weight)來控制將多少流量導向新版本。

2. 流量分配

您可以根據配置的流量分配策略(例如設置 nginx.ingress.kubernetes.io/canary-weight)將流量分配給 Canary 版本。這樣您可以控制有多少比例的用戶會訪問 Canary 版本。

  • 如果您設置了 canary-weight 為 10%,則 10% 的流量會被導向 Canary 版本,而 90% 的流量仍然會被導向穩定版本。

此外,您可以通過 HTTP Header、Cookie 或其他標識符來進行流量的有條件分配。例如,某些用戶可能基於其 Cookie 或 Header 被導向 Canary 版本,這有助於進行 A/B 測試。

3. 監控新版本

隨著流量逐步切換到 Canary 版本,您需要密切監控新版本的運行狀況。關鍵指標包括:

  • 應用程式的性能:CPU 和內存的使用情況。
  • 錯誤率:API 或應用程式的錯誤率。
  • 回應時間:用戶的回應時間是否在可接受的範圍內。

這些指標能幫助您及早發現新版本的問題,從而決定是否繼續推進流量切換。

4. 調整流量比例

根據 Canary 版本的表現,您可以逐步增加其流量配額。如果 Canary 版本表現穩定且無錯誤,則可以逐步提高 canary-weight 的比例,直到最終將所有流量切換到新版本。

  • 例如,從最初的 10% 開始,然後增加到 20%、50%、最終 100%。

5. 完全切換至新版本

當 Canary 版本完全穩定,並且經過充分測試後,您可以將所有流量切換到新版本。這時,您可以更新 Deployment 或 Ingress 配置,將所有流量導向新版本,並停用舊版本。

  • 通常,您會進行一個 rollback 機制,確保在發生意外情況時,可以將流量快速回退到穩定版本。

6. 清理舊版本

在 Canary 部署成功後,您可以選擇刪除或更新舊版本的 Pod,以減少資源消耗並保持集群的乾淨與高效。


實作金絲雀部屬

  1. 首先我們先建立一個基本的 deployement 和 service,與我們前幾章做的一樣,讓服務可以順利跑起來。
app-v1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: v1-deployment
labels:
app: v1-app
spec:
replicas: 1
selector:
matchLabels:
app: v1-app
version: v1
template:
metadata:
labels:
app: v1-app
version: v1
spec:
containers:
- name: v1-container
image: hello-world:v1.0.0
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: v1-service
spec:
selector:
app: v1-app
version: v1
type: NodePort
ports:
- protocol: TCP
port: 8080
targetPort: 8080
  1. 建立基礎的 ingress 讓流量可以導到這個 service 上,如果不知道如何設定可以參考Advanced - Ingress
ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
spec:
ingressClassName: nginx
defaultBackend:
service:
name: v1-service
port:
number: 8080
  1. 接著我們準備 v2 版本的 deployment 和 service 檔案,service name 也要記得改為 v2,select 的 label 也是 v2 的。
app-v2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: v2-deployment
labels:
app: v2-app
spec:
replicas: 1
selector:
matchLabels:
app: v2-app
version: v2
template:
metadata:
labels:
app: v2-app
version: v2
spec:
containers:
- name: v2-container
image: hello-world:v2.0.0
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: v2-service
spec:
selector:
app: v2-app
version: v2
type: NodePort
ports:
- protocol: TCP
port: 8080
targetPort: 8080
  1. 接著我們將 v1 和 v2 的服務都啟動起來,ingress 也跑起來,這樣流量首先會全部都到 v1 的 pod 中。
kubectl.exe apply -f app-v1.yaml,app-v2.yaml,ingress.yaml
---

deployment.apps/v1-deployment created
service/v1-service created
deployment.apps/v2-deployment created
service/v2-service created
ingress.networking.k8s.io/my-ingress created
kubectl.exe get all
---

NAME READY STATUS RESTARTS AGE
pod/v1-deployment-6c8656f965-pcljn 1/1 Running 0 100s
pod/v2-deployment-7dccc856d9-cc5q6 1/1 Running 0 100s

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 98d
service/v1-service NodePort 10.108.96.177 <none> 8080:30972/TCP 100s
service/v2-service NodePort 10.99.180.3 <none> 8080:30855/TCP 100s

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/v1-deployment 1/1 1 1 101s
deployment.apps/v2-deployment 1/1 1 1 100s

NAME DESIRED CURRENT READY AGE
replicaset.apps/v1-deployment-6c8656f965 1 1 1 100s
replicaset.apps/v2-deployment-7dccc856d9 1 1 1 100s
kubectl.exe get ingress
---

NAME CLASS HOSTS ADDRESS PORTS AGE
my-ingress nginx * localhost 80 52s
  1. 我們通過 for 迴圈將請求包起來,這樣就會呼叫 10 次,可以看到全部都在 v1 中。
for ($i = 1; $i -le 10; $i++) {
$response = Invoke-WebRequest -Uri http://localhost -UseBasicParsing
$response.Content
}
---

{"data":"Hello world v1!"}
{"data":"Hello world v1!"}
{"data":"Hello world v1!"}
{"data":"Hello world v1!"}
{"data":"Hello world v1!"}
{"data":"Hello world v1!"}
{"data":"Hello world v1!"}
{"data":"Hello world v1!"}
{"data":"Hello world v1!"}
{"data":"Hello world v1!"}
  1. 接著是重頭戲,我們撰寫 canary-ingress,我想要讓 10% 的流量導入到 v2 中,在 annotations 中增加 canary,並將 weight 設為 10,最下面的 service 要記得改成 v2。
canary-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/canary: 'true'
nginx.ingress.kubernetes.io/canary-weight: '10'
labels:
app: my-app
name: canary-ingress
spec:
ingressClassName: nginx
defaultBackend:
service:
name: v2-service
port:
number: 8080
  1. 透過指令執行 canary-ingress,他會新創立一個 canary-ingress,並且透過 for 迴圈觀察是否有導向 v2 的回應。
kubectl.exe apply -f canary-ingress.yaml
---

ingress.networking.k8s.io/canary-ingress created
for ($i = 1; $i -le 10; $i++) {
$response = Invoke-WebRequest -Uri http://localhost -UseBasicParsing
$response.Content
}
---

{"data":"Hello world v1!"}
{"data":"Hello world v1!"}
{"data":"Hello world v1!"}
{"data":"Hello world v1!"}
{"data":"Hello world v1!"}
{"data":"Hello world v2!"}
{"data":"Hello world v1!"}
{"data":"Hello world v1!"}
{"data":"Hello world v2!"}
{"data":"Hello world v1!"}
  1. 接著將 canary-ingress 設定為 50%,執行他之後查看流量是不是一半都導到 v2。
canary-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/canary: 'true'
nginx.ingress.kubernetes.io/canary-weight: '50'
labels:
app: my-app
name: canary-ingress
spec:
ingressClassName: nginx
defaultBackend:
service:
name: v2-service
port:
number: 8080
kubectl.exe apply -f canary-ingress.yaml
---

ingress.networking.k8s.io/canary-ingress configured
for ($i = 1; $i -le 10; $i++) {
$response = Invoke-WebRequest -Uri http://localhost -UseBasicParsing
$response.Content
}
---

{"data":"Hello world v1!"}
{"data":"Hello world v1!"}
{"data":"Hello world v2!"}
{"data":"Hello world v1!"}
{"data":"Hello world v2!"}
{"data":"Hello world v2!"}
{"data":"Hello world v1!"}
{"data":"Hello world v1!"}
{"data":"Hello world v1!"}
{"data":"Hello world v2!"}
  1. 最後我們將 100%流量導到 v2,查看是否全部流量都到 v2。
canary-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/canary: 'true'
nginx.ingress.kubernetes.io/canary-weight: '100'
labels:
app: my-app
name: canary-ingress
spec:
ingressClassName: nginx
defaultBackend:
service:
name: v2-service
port:
number: 8080
kubectl.exe apply -f canary-ingress.yaml
---

ingress.networking.k8s.io/canary-ingress configured
for ($i = 1; $i -le 10; $i++) {
$response = Invoke-WebRequest -Uri http://localhost -UseBasicParsing
$response.Content
}
---

{"data":"Hello world v2!"}
{"data":"Hello world v2!"}
{"data":"Hello world v2!"}
{"data":"Hello world v2!"}
{"data":"Hello world v2!"}
{"data":"Hello world v2!"}
{"data":"Hello world v2!"}
{"data":"Hello world v2!"}
{"data":"Hello world v2!"}
{"data":"Hello world v2!"}
  1. 最後我們可以將原本的 ingress 修改成 v2,並且執行他,最後再將 canary-ingress 刪除,就能完成平滑且可控流量的升版作業。
ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
spec:
ingressClassName: nginx
defaultBackend:
service:
name: v2-service
port:
number: 8080
kubectl.exe apply -f ingress.yaml
---

ingress.networking.k8s.io/my-ingress configured
kubectl.exe describe ingress
---

...canary-ingerss 省略...

Name: my-ingress
Labels: <none>
Namespace: default
Address: localhost
Ingress Class: nginx
Default backend: v2-service:8080 (10.1.1.31:8080)
Rules:
Host Path Backends
---- ---- --------
* * v2-service:8080 (10.1.1.31:8080)
Annotations: <none>
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Sync 31s (x4 over 26m) nginx-ingress-controller Scheduled for sync
kubectl.exe delete -f canary-ingress.yaml
---

ingress.networking.k8s.io "canary-ingress" deleted
for ($i = 1; $i -le 10; $i++) {
$response = Invoke-WebRequest -Uri http://localhost -UseBasicParsing
$response.Content
}
---

{"data":"Hello world v2!"}
{"data":"Hello world v2!"}
{"data":"Hello world v2!"}
{"data":"Hello world v2!"}
{"data":"Hello world v2!"}
{"data":"Hello world v2!"}
{"data":"Hello world v2!"}
{"data":"Hello world v2!"}
{"data":"Hello world v2!"}
{"data":"Hello world v2!"}