昨天的章節,我們實作了 Recreate,Rolling update 兩個部署策略。今天來繼續實作剩下的策略。
在 Kubernetes 中,可以使用兩個具有相同 Pod 標籤的 Deployment 來實現 Canary (金絲雀)部署。新版本的副本和舊版本的一起發佈。在一段時間後如果沒有檢測到錯誤,則可以擴展新版本的副本數量並刪除舊版本的應用。
如果想要透過 Kubernetes 原生方法實作金絲雀部署,只能透過兩個版本副本比例來調整流量版分比,例如當 v1 與 v2 副本數都是 10 個時,流量比例即 1:1。如果你需要更精確的控制策略,建議使用 Service Mesh 服務 (如 Istio),或是透過 Ingress Controller 服務 (如 nginx)。
下面我們會透過原生 Kubernetes 實作 Canary 部署策略 。
組態檔案: my-app-svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: my-app-svc
  labels:
    app: my-app-svc
spec:
  type: NodePort
  selector:
    app: my-app
  ports:
  - name: http
    port: 8080
    targetPort: 8080
    nodePort: 30000
組態檔案: my-app-v1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-v1
spec:
  replicas: 10
  selector:
    matchLabels:
      app: my-app
      version: v1
  strategy:
    type: Recreate
  template:
    metadata:
      name: my-app
      labels:
        app: my-app
        version: v1
    spec:
      containers:
      - name: my-app
        image: lofairy/foo
        ports:
        - name: http
          containerPort: 8080
        livenessProbe:
          httpGet:
            path: /
            port: http
          initialDelaySeconds: 5
          periodSeconds: 5
        readinessProbe:
          httpGet:
            path: /
            port: http
          periodSeconds: 5
組態檔案: my-app-v2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-v2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
      version: v2
  strategy:
    type: Recreate
  template:
    metadata:
      name: my-app
      labels:
        app: my-app
        version: v2
    spec:
      containers:
      - name: my-app
        image: lofairy/bar
        ports:
        - name: http
          containerPort: 8080
        livenessProbe:
          httpGet:
            path: /
            port: http
          initialDelaySeconds: 5
          periodSeconds: 5
        readinessProbe:
          httpGet:
            path: /
            port: http
          periodSeconds: 5
組態檔案大致上與 Blue/Green 練習使用的一致,唯一的差別是 my-app-svc.yaml 中的 label select 去掉了標籤 version,因此 Service 會同時適用 v1 和 v2 的 Pod 應用。
接下來我們按照下面的步驟來驗證 Canary 策略:
kubectl apply -f my-app-v1.yaml my-app-svc.yaml
my-app 是否部署成功`curl localhost:30000
---
{
    "message": "foo"
}
可以看到 v1 的應用正常運行了
為了查看接下來流量切換的變化,我們進行以下動作:
t1 ,運行以下命令來持續訪問 Servicewhile sleep 0.1; do curl http://0.0.0.0:30000; echo ""; done
kubectl apply -f my-app-v2.yaml
現在,叢集中同時有 my-app-v1, my-app-v2 兩個版本的應用
t1 ,查看結果{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"bar"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"bar"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"bar"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
可以看到大致上有不到 10% 的流量會訪問到 v2 應用,代表金絲雀部署成功了
kubectl scale --replicas=10 deploy my-app-v2
t1 ,查看結果{"message":"bar"}
{"message":"bar"}
{"message":"bar"}
{"message":"bar"}
{"message":"foo"}
{"message":"bar"}
{"message":"bar"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"bar"}
{"message":"foo"}
{"message":"bar"}
{"message":"bar"}
{"message":"foo"}
{"message":"bar"}
{"message":"foo"}
{"message":"foo"}
{"message":"bar"}
{"message":"bar"}
{"message":"bar"}
{"message":"foo"}
{"message":"bar"}
{"message":"bar"}
{"message":"bar"}
{"message":"foo"}
{"message":"bar"}
{"message":"foo"}
{"message":"bar"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"bar"}
{"message":"bar"}
可以看到 v1 與 v2 版本的流量大致是 1:1。
最後,如果 v2 版本沒有問題,就可以刪除舊版的 Deployment:
kubectl delete -f my-app-v1.yaml
從上面的過程可以理解 Canary 部署有以下特性:
如果對新功能的發佈沒有信心,使用金絲雀部署是不錯的選擇。
在 Kubernetes 中,我們可以用兩種方法來實現 Blue/Green (藍綠)部署,通過單個 Service 對象或者 Ingress 控製器來實現藍綠部署,實際操作都是類似的,都是通過 label 標籤去控制。
下面我們會透過原生 Kubernetes Service 實作 Blue/Green 部署策略 。
組態檔案: my-app-svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: my-app-svc
  labels:
    app: my-app-svc
spec:
  type: NodePort
  selector:
    app: my-app
    version: v1
  ports:
  - name: http
    port: 8080
    targetPort: 8080
    nodePort: 30000
組態檔案: my-app-v1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-v1
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
      version: v1
  template:
    metadata:
      name: my-app
      labels:
        app: my-app
        version: v1
    spec:
      containers:
      - name: my-app
        image: lofairy/foo
        ports:
        - name: http
          containerPort: 8080
        livenessProbe:
          httpGet:
            path: /
            port: http
          initialDelaySeconds: 5
          periodSeconds: 5
        readinessProbe:
          httpGet:
            path: /
            port: http
          periodSeconds: 5
組態檔案: my-app-v2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-v2
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
      version: v2
  template:
    metadata:
      name: my-app
      labels:
        app: my-app
        version: v2
    spec:
      containers:
      - name: my-app
        image: lofairy/bar
        ports:
        - name: http
          containerPort: 8080
        livenessProbe:
          httpGet:
            path: /
            port: http
          initialDelaySeconds: 5
          periodSeconds: 5
        readinessProbe:
          httpGet:
            path: /
            port: http
          periodSeconds: 5
組態檔案大致上與 Recreate 練習使用的一致,唯一的差別是 v1 (藍), v2 (綠) Pod 增加了新的標籤 version,用以區別不同版本的應用。因為 Blue/Green 部署的過程不改變流量切換,我們依然使用 sepc.strategy.type=Recreate 策略加快副本 scale up/down 的速度。
接下來我們按下面的步驟來驗證 Blue/Green 策略:
kubectl apply -f my-app-v1.yaml my-app-svc.yaml
my-app 是否部署成功,訪問 localhost:30000,得到以下回應curl localhost:30000
---
{
    "message": "foo"
}
可以看到 v1 的應用正常運行了
kubectl apply -f my-app-v2.yaml
現在,叢集中同時有 my-app-v1, my-app-v2 兩個版本的應用,不過 Service 目前將流量都指向 my-app-v1 裡。
為了查看接下來流量切換的變化,我們進行以下動作:
t1 ,運行以下命令來持續訪問 Servicewhile sleep 0.1; do curl http://0.0.0.0:30000; echo ""; done
kubectl patch service my-app-svc -p '{"spec":{"selector":{"version":"v2"}}}'
t1 ,查看結果{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"foo"}
{"message":"bar"}
{"message":"bar"}
{"message":"bar"}
{"message":"bar"}
{"message":"bar"}
{"message":"bar"}
{"message":"bar"}
{"message":"bar"}
可以看到流量在一瞬間切換成 v2 版本,並沒有中斷服務。
如果 v2 版本發生問題,可以再次修改 Service 回滾:
kubectl patch service my-app-svc -p '{"spec":{"selector":{"version":"v1"}}}'
最後,如果 v2 版本沒有問題,就可以刪除舊版的 Deployment:
kubectl delete -f my-app-v1.yaml
從上面的過程可以理解 Blue/Green 部署有以下特性:
在生產環境執行s無法向下相容的更新部署,Blue/Green 是很好的部署策略。
在原生 Kubernetes 中,並無實作 A/B testing,不過可以透過 Service Mesh 服務 (如 Istio) 或其他 Ingress Controller 服務 (如 nginx) 實現。
下面我們會透過 Nginx Ingress Controller 實作 A/B Testing 部署策略 。
我們需要在叢集中安裝 Nginx Ingress Controller。
之前的章節中已經帶領過大家如何安裝和使用 Nginx Ingress Controller,這邊就不再贅述。
組態檔案: my-app-v1.yaml
---
apiVersion: v1
kind: Service
metadata:
  name: my-app-v1-svc
  labels:
    app: my-app-v1-svc
spec:
  selector:
    app: my-app
    version: v1
  ports:
  - name: http
    port: 8080
    targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-v1
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
      version: v1
  strategy:
    type: Recreate
  template:
    metadata:
      name: my-app
      labels:
        app: my-app
        version: v1
    spec:
      containers:
      - name: my-app
        image: lofairy/foo
        ports:
        - name: http
          containerPort: 8080
        livenessProbe:
          httpGet:
            path: /
            port: http
          initialDelaySeconds: 5
          periodSeconds: 5
        readinessProbe:
          httpGet:
            path: /
            port: http
          periodSeconds: 5
組態檔案: my-app-v2.yaml
---
apiVersion: v1
kind: Service
metadata:
  name: my-app-v2-svc
  labels:
    app: my-app-v2-svc
spec:
  selector:
    app: my-app
    version: v2
  ports:
  - name: http
    port: 8080
    targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-v2
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
      version: v2
  strategy:
    type: Recreate
  template:
    metadata:
      name: my-app
      labels:
        app: my-app
        version: v2
    spec:
      containers:
      - name: my-app
        image: lofairy/bar
        ports:
        - name: http
          containerPort: 8080
        livenessProbe:
          httpGet:
            path: /
            port: http
          initialDelaySeconds: 5
          periodSeconds: 5
        readinessProbe:
          httpGet:
            path: /
            port: http
          periodSeconds: 5
組態檔案: ingress.yaml
apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx
  namespace: ingress-nginx
spec:
  type: NodePort
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30000
  selector:
    app.kubernetes.io/name: ingress-nginx
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: main-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/canary: "false"
spec:
  ingressClassName: nginx
  rules:
  - host: "localhost"
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-app-v1-svc
            port:
              number: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: canary-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-by-header: "version"
    nginx.ingress.kubernetes.io/canary-by-header-value: "v2"
spec:
  ingressClassName: nginx
  rules:
  - host: "localhost"
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-app-v2-svc
            port:
              number: 8080
大致說明一下:
Canary 練習使用的一致,不過這次為兩個版本的應用都定義了一組 Service 供 Ingress 使用。Nginx Ingress Controller 的 Canary 拓展實現依據 header 分流。KinD 設定對本機暴露的 30000 port,我們在 Ingress 的外邊再加上一組 Service。接下來我們按照下面的步驟來驗證 A/B Testing 策略:
kubectl apply -f my-app-v1.yaml -f my-app-v2.yaml
kubectl apply -f ingress.yaml
my-app 和 ingress 是否部署成功curl localhost:30000
---
{"message": "foo"}
得到回應就代表部署成功。
不過訪問 ingress 只能得到 v1 版本的回應,是因為我們在組態檔案的定義是:
version: v2 時,導流到 v2 應用的 Service現在我們來試著在 Header 加入參數,再訪問 ingress:
curl -H "version: v2" http://localhost:30000
---
bar
可以看到,我們成功的以 Header 為條件將訪問者分流到不同版本的應用。
從上面的過程可以理解 A/B Testing 部署有以下特性:
在 Deployment strategy 三個章節中,我們已經對大部分的部署策略有了蠻深的了解。落實在實際應用中,我們需要分析產品和應用的需求,選擇合適的部署策略。沒有哪個部署策略是完美的。
Kubernetes 部署策略详解-阳明的博客|Kubernetes|Istio|Prometheus|Python|Golang|云原生 (qikqiak.com)