K8S釋出策略,無損釋出

黃青石發表於2021-11-10

  大家好,相信大部分公司都已經使用K8S進行容器管理和編排了,但是關於K8S的釋出策略,還有很多同學不太清楚,通過這篇文章的介紹,相信大家對目前K8S的釋出情況有一個概括的認識。總結下來,共有如下幾種:

  1. 重建(recreate) :即停止一個原有的容器,然後進行容器的新建。
  2. 滾動更新(rollingUpdate):停掉一個容器,然後更新一個容器。 
  3. 藍綠佈署(blue/green ):準備一套藍色的容器和一套綠色的容器,進行流量切換。
  4. 金絲雀釋出(canary):更新部分容器,沒有問題後進行逐步替換,直到切完。
  5. A/B測試釋出:即將釋出的結果面向部分使用者,這塊沒有現成的元件,需要進行自行處理,比如使用Istio、Linkerd、Traefik等。這種方式採用在Http的Header上進行處理。
  6. 無損釋出:現在很多釋出都是將容器停掉,當沒有請求的時候這個時候釋出會實現無損釋出。

  接下來,我們將挨個展示來講。

  一、重新(reCreate)

    就是停掉原來的容器,然後再啟動容器,這種方式對於開發環境和測試環境使用還可以,但是對於正式環境就不適用了。相當於本地的服務重啟一下,這樣會直接影響服務的使用。

spec:
  replicas: 2
  strategy:
    type: Recreate

  這個就是同時啟動2個服務,如下圖,當我們要新發布服務的時候,需要將這兩個都停掉之後才啟動新服務。

 

  這種情況缺點是十分明顯的,當服務停止之後再建立新的服務。服務什麼時候啟動,也是根據服務啟動時間決定的。

  二、滾動更新(rollingUpdate)

  滾動更新步驟:

  1. 準備一個新版本的POD,比如新版本為V2,舊版本為V1。 

  2. 當V2版本的POD準備好後,在負載均衡中的例項池中將V2的版本加入。

  3. 在例項池中剔除一個V1版本的POD。

  4. 逐個例項替換,直到每個版本都是V2版本。

  如下圖所示:

 

 

   滾動更新使用的YAML方式如下:

spec:
  replicas: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2        # 一次可以新增多少個Pod
      maxUnavailable: 1  # 滾動更新期間最大多少個Pod不可用

  我們準備兩個版本, my-app-v1.yaml檔案。

apiVersion: v1
kind: Service
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  type: NodePort
  ports:
  - name: http
    port: 80
    targetPort: http
  selector:
    app: my-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
        version: v1.0.0
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9101"
    spec:
      containers:
      - name: my-app
        image: containersol/k8s-deployment-strategies
        ports:
        - name: http
          containerPort: 8080
        - name: probe
          containerPort: 8086
        env:
        - name: VERSION
          value: v1.0.0
        livenessProbe:
          httpGet:
            path: /live
            port: probe
          initialDelaySeconds: 5
          periodSeconds: 5
        readinessProbe:
          httpGet:
            path: /ready
            port: probe
          periodSeconds: 5

  然後我們再準備一下滾動更新的v2版本, my-app-rolling-update.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  replicas: 10
  # maxUnavailable設定為0可以完全確保在滾動更新期間服務不受影響,還可以使用百分比的值來進行設定。
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
        version: v2.0.0
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9101"
    spec:
      containers:
      - name: my-app
        image: containersol/k8s-deployment-strategies
        ports:
        - name: http
          containerPort: 8080
        - name: probe
          containerPort: 8086
        env:
        - name: VERSION
          value: v2.0.0
        livenessProbe:
          httpGet:
            path: /live
            port: probe
          initialDelaySeconds: 5
          periodSeconds: 5
        readinessProbe:
          httpGet:
            path: /ready
            port: probe
          # 初始延遲設定高點可以更好地觀察滾動更新過程
          initialDelaySeconds: 15
          periodSeconds: 5

  接下來,我們按如下步驟進行操作:

  1. 啟動my-app-v1應用

  2. 啟動my-app-rollingupdate應用

  3. 觀察所有容器版本變為V2版本

  

  啟動my-app-v1應用並觀察,都已經啟動,我們進行介面呼叫。

apply -f my-app-v1.yaml
kubectl get pods -l app=my-app

NAME                      READY     STATUS    RESTARTS   AGE
my-app-847874cd75-h3d2e   1/1       Running   0          47s
my-app-847874cd75-p4l8f   1/1       Running   0          47s
my-app-847874cd75-qnt7p   1/1       Running   0          47s
$ kubectl get svc my-app
NAME      TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
my-app    NodePort   10.109.99.184   <none>        80:30486/TCP   1m
$ curl http://127.0.0.1:30486
Host: my-app-847874cd75-qnt7p, Version: v1.0.0

   在當前視窗監控容器的情況,執行命令

watch kubectl get pods -l app=my-app

  開啟一個新的視窗,然後執行滾動更新命令

kubectl apply -f my-app-rolling-update.yaml

  在watch的終端觀察,開始的時候並沒刪除舊的pod,而是建立好新的pod後,然後進行挨個替,我們可以使用如下命令觀察介面請求, 漸漸的有了第二個版本的請求。

$ while sleep 0.1; do curl http://127.0.0.1:30486; done


Host:my-app-847874cd75-h3d2e, Version: v1.0.0 
......
Host: my
-app-847874cd75-h3d2e, Version: v1.0.0
Host: my-app-6b5479d97f-2fk24, Version: v2.0.0

  在這個過程中,我們發現第二個版本有問題,我們需要進行回滾,此時我們需要執行一下如下命令

kubectl rollout undo deploy my-app

  如果想兩個版本都觀察一下,這個時候需要執行命令。

kubectl rollout pause deploy my-app

  如果發現第二個版本沒有問題,那麼我們要恢復執行

kubectl rollout resume deploy my-app

  最後我們清理一下所有的部署

kubectl delete -l app=my-app

  總結一下:

  1. 滾動部署沒有控制流量的情況。

  2. 各個版本部署的時候需要一定的時間。

  三、藍綠部署

  我們需要準備兩個版本,一個藍版本,一個綠藍本,這兩個版本同時存在,且兩個版本是獨立的,這個時候可以瞬間對兩個版本的切換。上圖:

 

 

   接下來,我們採用svc的方式,通過label selector來進行版本之間的選擇。

selector:
  app: my-app
  version: v1.0.0

  我們建立一下 app-v1-svc.yaml檔案,我們首先建立一個svc服務,然後通過label selector來指定一下版本。

apiVersion: v1
kind: Service
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  type: NodePort
  ports:
  - name: http
    port: 80
    targetPort: http
  # 注意這裡我們匹配 app 和 version 標籤,當要切換流量的時候,我們更新 version 標籤的值,比如:v2.0.0
  selector:
    app: my-app
    version: v1.0.0
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-v1
  labels:
    app: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
      version: v1.0.0
  template:
    metadata:
      labels:
        app: my-app
        version: v1.0.0
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9101"
    spec:
      containers:
      - name: my-app
        image: containersol/k8s-deployment-strategies
        ports:
        - name: http
          containerPort: 8080
        - name: probe
          containerPort: 8086
        env:
        - name: VERSION
          value: v1.0.0
        livenessProbe:
          httpGet:
            path: /live
            port: probe
          initialDelaySeconds: 5
          periodSeconds: 5
        readinessProbe:
          httpGet:
            path: /ready
            port: probe
          periodSeconds: 5

  接下來,我們再建立另一個版本 app-v2-svc.yaml檔案

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-v2
  labels:
    app: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
      version: v2.0.0
  template:
    metadata:
      labels:
        app: my-app
        version: v2.0.0
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9101"
    spec:
      containers:
      - name: my-app
        image: containersol/k8s-deployment-strategies
        ports:
        - name: http
          containerPort: 8080
        - name: probe
          containerPort: 8086
        env:
        - name: VERSION
          value: v2.0.0
        livenessProbe:
          httpGet:
            path: /live
            port: probe
          initialDelaySeconds: 5
          periodSeconds: 5
        readinessProbe:
          httpGet:
            path: /ready
            port: probe
          periodSeconds: 5

  接下來,我們執行一下操作步驟:

  1. 啟動版本1服務

  2. 啟動版本2服務

  3. 將版本1服務切換到版本2,觀察服務情況

 

  啟動版本的服務並觀察,沒有問題。

kubectl apply -f app-v1-svc.yaml
kubectl get pods -l app=my-app
watch kubectl get pods -l app=my-app
while sleep 0.1; do curl http://127.0.0.1:31539; done
Host: my-app-v1-7b4874cd75-dmq8f, Version: v1.0.0
Host: my-app-v1-7b4874cd75-dmq8f, Version: v1.0.0

  啟動版本2的服務, 因為版本2沒有掛到SVC,所以沒有辦法觀察,但是我們可以先啟動。

kubectl apply -f app-v2-svc.yaml

  這個時候我們進行版本的切換,通過切換標籤的方式

kubctl patch service my-ap -p '{"spec":{"selector":{"version":"v2.0.0"}}}'
while sleep 0.1; do curl http://127.0.0.1:31539; done
Host: my-app-v2-f885c8d45-r5m6z, Version: v2.0.0
Host: my-app-v2-f885c8d45-r5m6z, Version: v2.0.0  

  同時,當發現問題的時候,我們再把版本切回到v1.0.0即可。

  總結:

  1. 藍綠部署需要準備兩套資源,相對有的時候需要的資源會多。

  2. 這種情況可以快速還原版本,不會出現滾動更新那樣,回滾需要的時間會很久。

 

  四、金絲雀部署

  金絲雀部署就是將部分新版本發在舊的容器池裡邊,然後進行流量觀察,比如10%的流量切到新服務上,90%的流量還在舊服務上。如果你想用更精細粒度的話精使用Istio。上圖

 

   我們這次還是採用svc的方式進行部署的選擇,但是這次我們在選擇的時候我們只使用label,不使用版本號。新建 app-v1-canary.yaml, 裡邊有10個pod支撐這個服務。

apiVersion: v1
kind: Service
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  type: NodePort
  ports:
  - name: http
    port: 80
    targetPort: http
  selector:
    app: my-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-v1
  labels:
    app: my-app
spec:
  replicas: 10
  selector:
    matchLabels:
      app: my-app
      version: v1.0.0
  template:
    metadata:
      labels:
        app: my-app
        version: v1.0.0
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9101"
    spec:
      containers:
      - name: my-app
        image: containersol/k8s-deployment-strategies
        ports:
        - name: http
          containerPort: 8080
        - name: probe
          containerPort: 8086
        env:
        - name: VERSION
          value: v1.0.0
        livenessProbe:
          httpGet:
            path: /live
            port: probe
          initialDelaySeconds: 5
          periodSeconds: 5
        readinessProbe:
          httpGet:
            path: /ready
            port: probe
          periodSeconds: 5

  接下來,我們建立v2版本 app-v2-canary.yaml檔案 

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-v2
  labels:
    app: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
      version: v2.0.0
  template:
    metadata:
      labels:
        app: my-app
        version: v2.0.0
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "9101"
    spec:
      containers:
      - name: my-app
        image: containersol/k8s-deployment-strategies
        ports:
        - name: http
          containerPort: 8080
        - name: probe
          containerPort: 8086
        env:
        - name: VERSION
          value: v2.0.0
        livenessProbe:
          httpGet:
            path: /live
            port: probe
          initialDelaySeconds: 5
          periodSeconds: 5
        readinessProbe:
          httpGet:
            path: /ready
            port: probe
          periodSeconds: 5

  我們說一下要執行的步驟:

  1. 啟動V1的服務版本:10個複本。

  2. 啟動V2的服務版本:1個複本。

  3. 觀察V2流量正常的情況的話,那麼啟動V2的10個複本。

  4. 刪除V1的10個複本,流量全部到V2上。

  

  啟動V1服務,檢視服務是否正確,然後觀察一下服務。

kubectl apply -f app-v1-canary.yaml
kubectl get svc -l app=my-app
curl http://127.0.0.1:30760 watch kubectl get pod

  新開啟容器,執行命令,啟動V2服務。 上邊的watch將觀察到新增了1個pod, 此時共有11個pod, 2.0.0的版本已經上來了。

kubectl apply -f app-v2-canary.yaml
hile sleep 0.1; do curl http://127.0.0.1:30760; done
Host: my-app-v1-7b4874cd75-bhxbp, Version: v1.0.0
Host: my-app-v1-7b4874cd75-wmcqc, Version: v1.0.0
Host: my-app-v1-7b4874cd75-tsh2s, Version: v1.0.0
Host: my-app-v1-7b4874cd75-ml58j, Version: v1.0.0
Host: my-app-v1-7b4874cd75-spsdv, Version: v1.0.0
Host: my-app-v2-f885c8d45-mc2fx, Version: v2.0.0

  此時我們觀察版本2的服務是否正確,如果正確,那麼我們將版本2擴充套件到10個複本。

kubectl scale --replicas=10 deploy my-app-v2

  這個時候版本1和版本2一樣了。我們再將版本1的pod給清除掉

kubectl delete deploy my-app-v1

  如果沒有問題可以清理所有服務

kubectl delete all -l app=my-app

  結論:

  1. 因為有部分版本線上上執行,我們能夠對其日誌進行觀察和追蹤、定位問題。

  2. 如果有問題也能快速將新版本清理掉

  3. 如果沒有問題,相比於藍綠的話,釋出較慢。

  4. 對程式碼信心不足的情況可以採用此方法,影響範圍較小。

 

  五、A/B測試

  這種方法一般適合於業務場景測試,比如場景A的情況下帶來的交易額是否會增大,也就是針對不同的人展示不同的介面,然後來觀察效益。

  這個時候,我們需要在需要對流量進行按權重分發,或者是在Header, cookie 中做文章。比如,我們可以採用Istio進行許可權的分發。

route:
- tags:
  version: v1.0.0
  weight: 90
- tags:
  version: v2.0.0
  weight: 10

  結論:

  1. 這種方法可以將流量進行分發,我們需要做一個全域性的鏈路跟蹤

  2. 這種其實不屬於部署範圍,是流量分發的一種機制。

  

  六、無損釋出

  我們經常會遇到的情況是,程式正在執行的時候,pod突然停止了,這個時候,執行的一半的程式很可能會對資料的完整性造成影響。這個時候就需要我們精巧的設計,流量的切換,優雅的停服務。

  有開源的OpenKruise v0.5.0支援無損釋出

  基於service mesh網路服務,無損釋出的方法是採用K8S+Istio邊車模式+envoy實現無損釋出。

  阿里雲也有企業級的EDAS,支援無損釋出,有興趣的同學也可以去學習一下。、

  

  好了。有問題的同學歡迎留言。

相關文章