應用流量無損切換技術測驗

上篇相關應用流量引流相關的技術探討,相信大家已經對 Kubernetes 的服務引流架構有了更深入的瞭解。常言道好記性不如爛筆頭,筆者在反覆練習這些參數的過程中,也是費勁了很大的一段時間纔對 Kubernetes 的集羣引流技術有了一些運用。以下的練習案例都是筆者認爲可以加固自身知識體系的必要練習,還請大家跟隨我的記錄一起練習吧。

練習 1:Deployment 下實現無損流量應用更新

我們在更新應用的時候,往往會發現即使發佈應用的時候 Kubernetes 採用了滾動更新的策略,應用流量還是會秒斷一下。這個困惑在於官方文檔資料的介紹中這裏都是重點說可以平滑更新的。注意這裏,它是平滑更新,並不是無損流量的更新。所以到底問題出在哪裏呢。筆者查閱了資料,發現核心問題是 Pod 生命週期中應用的版本更新如下圖,關聯對象資源如 Pod、Endpoint、IPVS、Ingress/SLB 等資源的更新操作都是異步執行的。往往流量還在處理中,Pod 容器就有可能給如下圖:

依據 Pod 容器進程生命週期流程圖中,容器進程的狀態變更都是異步的,如果應用部署對象 Deployment 不增加 lifecycle 參數 preStop 的配置,即使南北向流量關閉了,進程仍然還需要幾秒鐘處理正在執行中的會話數據,纔可以優雅退出。以下爲應用部署 Deployment 對象的聲明式配置:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      component: nginx
  progressDeadlineSeconds: 120
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
  template:
    metadata:
      labels:
        component: nginx
    spec:
      terminationGracePeriodSeconds: 60
      containers:
      - name: nginx
        image: xds2000/nginx-hostname
        ports:
        - name: http
          containerPort: 80
          protocol: TCP
        readinessProbe:
          httpGet:
            path: /
            port: 80
            httpHeaders:
            - name: X-Custom-Header
              value: Awesome
          initialDelaySeconds: 15
          periodSeconds: 3
          timeoutSeconds: 1
        lifecycle:
          preStop:
            exec:
              command: ["/bin/bash""-c""sleep 10"]

就緒探測器(readinessProbe)可以知道容器什麼時候準備好了並可以開始接受請求流量, 當一個 Pod 內的所有容器都準備好了,才能把這個 Pod 看作就緒。這種信號的一個用途就是控制哪個 Pod 作爲 Service 的後端。在 Pod 還沒有準備好的時候,會從 Service 的負載均衡器中剔除 Pod。periodSeconds 字段指定了 kubelet 每隔 3 秒執行一次存活探測。initialDelaySeconds 字段告訴 kubelet 在執行第一次探測前應該等待 15 秒。

kubectl 工具手動刪除某個特定的 Pod,而該 Pod 的優雅終止限期默認值是 30 秒。preStop 回調所需要的時間長於默認的優雅終止限期,你必須修改 terminationGracePeriodSeconds 屬性值來使其正常工作。

如果 Pod 中的容器之一定義了 preStop 回調,kubelet 開始在容器內運行該回調邏輯。如果超出體面終止限期時,preStop 回調邏輯 仍在運行,kubelet 會請求給予該 Pod 的寬限期一次性增加 2 秒鐘。

在熟練掌握這些生命週期的配置屬性後,單個 Pod 的流量就可以優雅處理,這個原子能力的處理讓上層高級對象的處理也可以原生支持無損流量的切換。

練習 2:Ingress-nginx 流量無損切換更新應用

Ingress 對象是 Kubernetes 設計的引流對象,它直接監聽 Service 的 Endpoints 接口列表的變化來更新負載均衡的接口列表,當前 ingress-nginx 的負載均衡算法已經採用 Lua 編寫的數加權移動平均(EWMA)算法來實現流量的平滑處理。以下例子以 Nginx OSS 版本的 Ingress 來作爲範例幫助大家理解。案例如下:

# 安裝 Ingress
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-0.32.0/deploy/static/provider/cloud/deploy.yaml
# 驗證
❯ kubectl get pods --all-namespaces -l app.kubernetes.io/name=ingress-nginx
NAMESPACE       NAME                                        READY   STATUS      RESTARTS   AGE
ingress-nginx   ingress-nginx-admission-create-j5f8z        0/1     Completed   0          11m
ingress-nginx   ingress-nginx-admission-patch-btfd4         0/1     Completed   1          11m
ingress-nginx   ingress-nginx-controller-866488c6d4-snp4s   1/1     Running     0          11m

加載應用:

kubectl create -f sample/apple.yaml

# sample/apple.yaml
kind: Pod
apiVersion: v1
metadata:
  name: apple-app
  labels:
    app: apple
    version: apple-v1
spec:
  containers:
    - name: apple-app
      image: hashicorp/http-echo
      args:
        - "-text=apple"

---

kind: Service
apiVersion: v1
metadata:
  name: apple-service
spec:
  selector:
    version: apple-v1
  ports:
    - port: 5678 # Default port for image

加載應用:

kubectl create -f sample/banana.yaml

# sample/banana.yaml
kind: Pod
apiVersion: v1
metadata:
  name: banana-app
  labels:
    app: banana
    version: banana-v1
spec:
  containers:
    - name: banana-app
      image: hashicorp/http-echo
      args:
        - "-text=banana"

---

kind: Service
apiVersion: v1
metadata:
  name: banana-service
spec:
  selector:
    version: banana-v1
  ports:
    - port: 5678 # Default port for image

加載 Ingress 規則:

# sample/ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: example-ingress
  annotations:
    ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - http:
      paths:
        - path: /apple
          backend:
            serviceName: apple-service
            servicePort: 5678
        - path: /banana
          backend:
            serviceName: banana-service
            servicePort: 5678

當你切換 ingress.yaml 裏面的路由規則的時候,反向代理就會 reload 更新一次,連接就會中斷。爲了解決這個問題,我們必須防止 Ingress 發生變更,只在 Service 對象通過更新 selector 的方式變更 Endpoints 對象集合,因爲 Ingress 的更新機制就是監聽 Endpoints,會自動熱加載更改代理配置,實現無重啓的流量切換。更新例子如下:

# 這裏 Service 的更新會觸發 IPVS 更新 endpoints 的 Pod 地址,
export RELEASE_VERSION=banana-v2
kubectl patch svc default-www -p '{"spec":{"selector":{"version":"'${RELEASE_VERSION}'"}}}'

Ingress 監聽 Endpoints 的 lua 函數如下:

# https://github.com/kubernetes/ingress-nginx/blob/8f413c4231a727b28c4c137a0fb3f7a790f156c4/rootfs/etc/nginx/lua/util.lua#L100

-- diff_endpoints compares old and new
-- and as a first argument returns what endpoints are in new
-- but are not in old, and as a second argument it returns
-- what endpoints are in old but are in new.
-- Both return values are normalized (ip:port).
function _M.diff_endpoints(old, new)
  local endpoints_added, endpoints_removed = {}{}
  local normalized_old = normalize_endpoints(old)
  local normalized_new = normalize_endpoints(new)

  for endpoint_string, _ in pairs(normalized_old) do
    if not normalized_new[endpoint_string] then
      table.insert(endpoints_removed, endpoint_string)
    end
  end

  for endpoint_string, _ in pairs(normalized_new) do
    if not normalized_old[endpoint_string] then
      table.insert(endpoints_added, endpoint_string)
    end
  end

  return endpoints_added, endpoints_removed
end

練習 3:通過 Traefik 實現零停機時間部署

因爲 Traefik 可以直接和 Kubernetes Apiserver 進行交互,所以對於流量的切換和部署會比 ingress-nginx 更加便捷。Traefik 在 Kubernetes 中也是一個 Ingress 對象,在第二個練習中我們已經介紹了通過 Service 的 selector 切換實現無損流量的部署方法,第三個例子我們介紹另外三種比較流行的方法,藍綠部署、金絲雀發佈和 A/B 測試。雖然這三種方式都有關聯,但也各有不同。

通過 Kubernetes 不可變基礎設施的支持,我們可以讓同一軟件的多個版本實例在同一集羣內服務於請求,這種模式會讓試驗變得非常有趣。像這樣混合使用新舊版本,就可以配置路由規則來測試生產環境的最新版本。更重要的是,新版本可以逐步發佈——如果出現問題,甚至可以撤回——所有這一切幾乎都沒有停機時間。

藍綠髮布模式下,"綠色" 指的是應用的當前穩定版本,而 “藍色” 指的是引入新功能和修復的即將發佈的版本。兩個版本的實例同時在同一生產環境中運行。同時,代理服務(如 Traefik)確保只有發送到私有地址的請求才能到達藍色實例。例子如下圖:

金絲雀發佈模式將藍綠測試又向前推進了一步,用一種謹慎的方式將新功能和補丁部署到活躍的生產環境中。路由配置讓當前的穩定版本處理大多數請求,但有限比例的請求會被路由到新的 “金絲雀” 版本的實例。例子如下:

A/B 測試技術有時會與前兩種技術混淆,但它有自己的目的,即評估即將發佈的版本的兩個不同的版本,看看哪個版本會更成功。這種策略在 UI 開發中很常見。例如,假設一個新功能很快就會推出到應用程序中,但不清楚如何最好地將其暴露給用戶。爲了找出答案,包括該功能在內的兩個版本的 UI,同時運行 A 版本和 B 版本,代理路由器向每個版本發送有限數量的請求。例子如下:

這些技術對於測試現代的雲原生軟件架構是非常寶貴的,尤其是與傳統的瀑布式部署模型相比。如果使用得當,它們可以幫助發現生產環境中不可預見的迴歸、集成失敗、性能瓶頸和可用性問題,但要在新代碼進入穩定的生產版本之前。

這三種方法的共同點是,它們依靠容器和 Kubernetes 提供的部署便利性,加上雲原生網絡技術,將請求路由到可測試的部署,同時最大限度地減少對生產代碼的干擾。這是一個強大的組合——這正是 Traefik 的優勢所在——如果明智地使用,它可以有效地將整體應用程序的停機時間降至零。

總結

應用的流量更新複雜程度涉及到應用狀態的變更,以上的例子只是在有限的環境中驗證無損流量的切換思路。在真實場景中,我們還需要考慮數據庫,業務系統等關聯應用的影響,很難像習題中一樣直接用無狀態應用來隨意切換。但是這些因素都不妨礙我們確認一個事實,Kubernetes 確實可以通過參數實現無損流量的切換,它是可行的一套基礎設施,你需要深入理解並掌握這些基本對象的實現細節,通過合理的配置就可以實現你需要的不可變基礎設施。

參考資料

https://kubernetes.io/zh/docs/concepts/workloads/pods/pod-lifecycle/

https://itnext.io/improving-zero-downtime-on-kubernetes-95e3a2f623ba

https://github.com/kubernetes/ingress-nginx/blob/0549d9b132f54d0ac8d956bc78427cd2734ffdcd/rootfs/etc/nginx/lua/balancer/ewma.lua

https://traefik.io/blog/achieve-zero-downtime-deployments-with-traefik-and-kubernetes/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/QuP95-bNtBglaZwT3wP94A