Kubernetes 零宕機滾動更新

Kubernetes 零宕機滾動更新

圖片來源: https://unsplash.com/photos/_ZxC9GRHk1k

軟件世界的發展比以往任何時候都快,為了保持競爭力需要儘快推出新的軟件版本,而又不影響在線得用戶。許多企業已將工作負載遷移到了 Kubernetes 集群,Kubernetes 集群本身就考慮到了一些生產環境的實踐,但是要讓 Kubernetes 實現真正的零停機不中斷或丟失請求,我們還需要做一些額外的操作才行。

滾動更新

默認情況下,Kubernetes 的 Deployment 是具有滾動更新的策略來進行 Pod 更新的,該策略可以在任何時間點更新應用的時候保證某些實例依然可以正常運行來防止應用 down 掉,當新部署的 Pod 啟動並可以處理流量之後,才會去殺掉舊的 Pod。

在使用過程中我們還可以指定 Kubernetes 在更新期間如何處理多個副本的切換方式,比如我們有一個3副本的應用,在更新的過程中是否應該立即創建這3個新的 Pod 並等待他們全部啟動,或者殺掉一個之外的所有舊的 Pod,或者還是要一個一個的 Pod 進行替換?下面示例是使用默認的滾動更新升級策略的一個 Deployment 定義,在更新過程中最多可以有一個超過副本數的容器(maxSurge),並且在更新過程中沒有不可用的容器。

<code>apiVersion: apps/v1kind: Deploymentmetadata:  name: zero-downtime  labels:    app: zero-downtimespec:  replicas: 3  selector:    matchLabels:      app: zero-downtime  strategy:    type: RollingUpdate    rollingUpdate:      maxSurge: 1      maxUnavailable: 0  template:    # with image nginx    # .../<code> 

上面的 zero-downtime 這個應用使用 nginx 這個鏡像創建3個副本,該 Deployment 執行滾動更新的方式:首先創建一個新版本的 Pod,等待 Pod 啟動並準備就緒,然後刪除一箇舊的 Pod,然後繼續下一個新的 Pod,直到所有副本都已替換完成。為了讓 Kubernetes 知道我們的 Pod 何時可以準備處理流量請求了,我們還需要配置上 liveness 和 readiness 探針。下面展示的就是新舊 Pod 替換的輸出信息:

<code>$ kubectl get podsNAME                            READY   STATUS              RESTARTS   AGEzero-downtime-d449b5cc4-k8b27   1/1     Running             0          3m9szero-downtime-d449b5cc4-n2lc4   1/1     Running             0          3m9szero-downtime-d449b5cc4-sdw8b   1/1     Running             0          3m9s...zero-downtime-d449b5cc4-k8b27   1/1     Running             0          3m9szero-downtime-d449b5cc4-n2lc4   1/1     Running             0          3m9szero-downtime-d449b5cc4-sdw8b   1/1     Running             0          3m9szero-downtime-d569474d4-q9khv   0/1     ContainerCreating   0          12s...zero-downtime-d449b5cc4-n2lc4   1/1     Running             0          3m9szero-downtime-d449b5cc4-sdw8b   1/1     Running             0          3m9szero-downtime-d449b5cc4-k8b27   1/1     Terminating         0          3m29szero-downtime-d569474d4-q9khv   1/1     Running             0          1m...zero-downtime-d449b5cc4-n2lc4   1/1     Running             0          5mzero-downtime-d449b5cc4-sdw8b   1/1     Running             0          5mzero-downtime-d569474d4-q9khv   1/1     Running             0          1mzero-downtime-d569474d4-2c7qz   0/1     ContainerCreating   0          10s......zero-downtime-d569474d4-2c7qz   1/1     Running             0          40szero-downtime-d569474d4-mxbs4   1/1     Running             0          13szero-downtime-d569474d4-q9khv   1/1     Running             0          67s/<code>

可用性檢測

如果我們從舊版本到新版本進行滾動更新,只是簡單的通過輸出顯示來判斷哪些 Pod 是存活並準備就緒的,那麼這個滾動更新的行為看上去肯定就是有效的,但是往往實際情況就是從舊版本到新版本的切換的過程並不總是十分順暢的,應用程序很有可能會丟棄掉某些客戶端的請求。

為了測試是否存在請求被丟棄,特別是那些針對即將要退出服務的實例的請求,我們可以使用一些負載測試工具來連接我們的應用程序進行測試。我們需要關注的重點是所有的 HTTP 請求,包括 keep-alive 的 HTTP 連接是否都被正確處理了,所以我們這裡可以使用 Apache Bench(AB Test) 或者 Fortio(Istio 測試工具) 這樣的測試工具來測試。

我們使用多個線程以併發的方式去連接到正在運行的應用程序,我們關心的是響應的狀態和失敗的連接,而不是延遲或吞吐量之類的信息。我們這裡使用 Fortio 這個測試工具,比如每秒 500 個請求和 8 個併發的 keep-alive 連接的測試命令如下所示(使用域名zero.qikqiak.com代理到上面的3個 Pod):

<code>$ fortio load -a -c 8 -qps 500 -t 60s "http://zero.qikqiak.com/"/<code>

關於 fortio 的具體使用可以查看官方文檔:https://github.com/fortio/fortio

使用 -a 參數可以將測試報告保存為網頁的形式,這樣我們可以直接在瀏覽器中查看測試報告。如果我們在進行滾動更新應用的過程中啟動測試,則可能會看到一些請求無法連接的情況:

Kubernetes 零宕機滾動更新

<code>Starting at 1000 qps with 8 thread(s) [gomax 2] for 1m0s : 7500 calls each (total 60000)Ended after 1m0.006243654s : 5485 calls. qps=91.407Aggregated Sleep Time : count 5485 avg -17.626081 +/- 15 min -54.753398956 max 0.000709054 sum -96679.0518[...]Code 200 : 5463 (99.6 %)Code 502 : 20 (0.4 %)Response Header Sizes : count 5485 avg 213.14166 +/- 13.53 min 0 max 214 sum 1169082Response Body/Total Sizes : count 5485 avg 823.18651 +/- 44.41 min 0 max 826 sum 4515178[...]/<code>

從上面的輸出可以看出有部分請求處理失敗了(502),我們可以運行幾種通過不同方式連接到應用程序的測試場景,比如通過 Kubernetes Ingress 或者直接從集群內部通過 Service 進行連接。我們會看到在滾動更新過程中的行為可能會有所不同,具體的還是需要取決於測試的配置參數,和通過 Ingress 的連接相比,從集群內部連接到服務的客戶端可能不會遇到那麼多的失敗連接。

原因分析

現在的問題是需要弄明白當應用在滾動更新期間重新路由流量時,從舊的 Pod 實例到新的實例究竟會發生什麼,首先讓我們先看看 Kubernetes 是如何管理工作負載連接的。如果我們執行測試的客戶端直接從集群內部連接到 zero-downtime 這個 Service,那麼首先會通過 集群的 DNS 服務解析到 Service 的 ClusterIP,然後轉發到 Service 後面的 Pod 實例,這是每個節點上面的 kube-proxy 通過更新 iptables 規則來實現的。

Kubernetes 零宕機滾動更新

kubernetes kube-proxy

Kubernetes 會根據 Pods 的狀態去更新 Endpoints 對象,這樣就可以保證 Endpoints 中包含的都是準備好處理請求的 Pod。

但是 Kubernetes Ingress 連接到實例的方式稍有不同,這就是為什麼當客戶端通過 Ingresss 連接到應用程序的時候,我們會在滾動更新過程中查看到不同的宕機行為。

大部分 Ingress Controller,比如 nginx-ingress、traefik 都是通過直接 watch Endpoints 對象來直接獲取 Pod 的地址的,而不用通過 iptables 做一層轉發了。

Kubernetes 零宕機滾動更新

kubernetes ingress

無論我們如何連接到應用程序,Kubernetes 的目標都是在滾動更新的過程中最大程度地減少服務的中斷。一旦新的 Pod 處於活動狀態並準備就緒後,Kubernetes 就將會停止就的 Pod,從而將 Pod 的狀態更新為 “Terminating”,然後從 Endpoints 對象中移除,並且發送一個 SIGTERM 信號給 Pod 的主進程。SIGTERM 信號就會讓容器以正常的方式關閉,並且不接受任何新的連接。Pod 從 Endpoints 對象中被移除後,前面的負載均衡器就會將流量路由到其他(新的)Pod 中去。這個也是造成我們的應用可用性差距的主要原因,因為在負責均衡器注意到變更並更新其配置之前,終止信號就會去停用 Pod,而這個重新配置過程又是異步發生的,所以並不能保證正確的順序,所以就可能導致很少的請求會被路由到終止的 Pod 上去。

零宕機

那麼如何增強我們的應用程序以實現真正的零宕機遷移呢?

首先,要實現這個目標的先決條件是我們的容器要正確處理終止信號,在 SIGTERM 信號上實現優雅關閉。下一步需要添加 readiness 可讀探針,來檢查我們的應用程序是否已經準備好來處理流量了。

可讀探針只是我們平滑滾動更新的起點,為了解決 Pod 停止的時候不會阻塞並等到負載均衡器重新配置的問題,我們需要使用 preStop 這個生命週期的鉤子,在容器終止之前調用該鉤子。

生命週期鉤子函數是同步的,所以必須在將最終終止信號發送到容器之前完成,在我們的示例中,我們使用該鉤子簡單的等待,然後 SIGTERM 信號將停止應用程序進程。同時,Kubernetes 將從 Endpoints 對象中刪除該 Pod,所以該 Pod 將會從我們的負載均衡器中排除,基本上來說我們的生命週期鉤子函數等待的時間可以確保在應用程序停止之前重新配置負載均衡器。

這裡我們在 zero-downtime 這個 Deployment 中添加一個 preStop 鉤子:

<code>apiVersion: apps/v1kind: Deploymentmetadata:  name: zero-downtime  labels:    app: zero-downtimespec:  replicas: 3  selector:    matchLabels:      app: zero-downtime  template:    spec:      containers:      - name: zero-downtime        image: nginx        livenessProbe:          # ...        readinessProbe:          # ...        lifecycle:          preStop:            exec:              command: ["/bin/bash", "-c", "sleep 20"]  strategy:    # .../<code>

我們這裡使用 preStop 設置了一個 20s 的寬限期,Pod 在真正銷燬前會先 sleep 等待 20s,這就相當於留了時間給 Endpoints 控制器和 kube-proxy 更新去 Endpoints 對象和轉發規則,這段時間 Pod 雖然處於 Terminating 狀態,即便在轉發規則更新完全之前有請求被轉發到這個 Terminating 的 Pod,依然可以被正常處理,因為它還在 sleep,沒有被真正銷燬。

現在,當我們去查看滾動更新期間的 Pod 行為時,我們將看到正在終止的 Pod 處於 Terminating 狀態,但是在等待時間結束之前不會關閉的,如果我們使用 Fortio 重新測試下,則會看到零失敗請求的理想行為:

Kubernetes 零宕機滾動更新

fortio zeor downtime test

<code>Starting at 1000 qps with 8 thread(s) [gomax 2] for 1m0s : 7500 calls each (total 60000)Ended after 1m0.091439891s : 10015 calls. qps=166.66Aggregated Sleep Time : count 10015 avg -23.316213 +/- 14.52 min -50.161414028 max 0.001811225 sum -233511.876[...]Code 200 : 10015 (100.0 %)Response Header Sizes : count 10015 avg 214 +/- 0 min 214 max 214 sum 2143210Response Body/Total Sizes : count 10015 avg 826 +/- 0 min 826 max 826 sum 8272390Saved result to data/2020-02-12-162008_Fortio.json All done 10015 calls 47.405 ms avg, 166.7 qps/<code>

總結

Kubernetes 在考慮到生產就緒性方面已經做得很好了,但是為了在生產環境中運行我們的企業級應用,我們就必須瞭解 Kubernetes 是如何在後臺運行的,以及我們的應用程序在啟動和關閉期間的行為。而且上面的方式是隻適用於短連接的,對於類似於 websocket 這種長連接應用需要做滾動更新的話目前還沒有找到一個很好的解決方案,有的團隊是將長連接轉換成短連接來進行處理的,我這邊還是在應用層面來做的支持,比如客戶端增加重試機制,連接斷掉以後會自動重新連接,大家如果有更好的辦法也可以留言互相討論下方案。



分享到:


相關文章: