軟件世界比以往任何時候都更快。為了保持競爭力,需要盡快推出新的軟件版本,而不會中斷活躍用戶訪問,影響用戶體驗。越來越多企業(yè)已將其應(yīng)用遷移到Kubernetes,而Kubernetes的構(gòu)建基于生產(chǎn)準備。但是,為了通過Kubernetes實現(xiàn)真正的零停機時間,我們需要采取更多步驟,而不會破壞或丟失任何一個用戶的請求
關(guān)于如何使用Kubernetes實現(xiàn)零停機時間的系列文章
滾動更新
默認情況下,Kubernetes部署應(yīng)用使用滾動更新策略對pod進行更新。此策略旨在通過在執(zhí)行更新時在任何時間點保證至少一定數(shù)量pod實例正常運行來防止應(yīng)用程序停機。只有在新部署版本的新pod啟動并準備好處理流量后,才會關(guān)閉舊pod。
工程師可以進一步指定Kubernetes在更新期間如何處理多個副本的確切方式。根據(jù)我們可能要配置的工作負載和可用計算資源,我們希望在任何時候不出現(xiàn)過多或不足的實例的現(xiàn)象。例如,給定三個所需的副本,我們應(yīng)該立即創(chuàng)建三個新的pod并等待它們?nèi)繂?,我們?yīng)該終止除了一個之外的所有舊pod,還是逐個進行轉(zhuǎn)換?
以下deployment的部分yaml文件顯示了具有默認RollingUpdate升級策略的應(yīng)用程序的Kubernetes部署定義,以及maxSurge
在更新期間最多一個過度配置的pod數(shù)量和maxUnavailable
最大不可用的pod。
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: myapp-without-hook
spec:
replicas: 3
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
...
此部署配置將按以下方式執(zhí)行版本更新過程:它將一次啟動一個新版本的pod,等待pod啟動并準備就緒,觸發(fā)其中一個舊pod的終止,然后繼續(xù)下一個新的pod,直到所有副本都已完成更新。
為了告訴Kubernetes我們的pod正在運行并準備好處理流量,我們需要配置存活探針與就緒探針。
更新過程
root@k8s-master-1:~/k8s_manifests/zero-downtime-tutorial# kubectl get pod
myapp-without-hook-77448c5b9f-648h5 1/1 Running 0 76s
myapp-without-hook-77448c5b9f-b9rxm 1/1 Running 0 76s
myapp-without-hook-77448c5b9f-gbrvc 1/1 Running 0 76s
myapp-without-hook-789fd7c548-m5bpw 0/1 ContainerCreating 0 2s
myapp-without-hook-77448c5b9f-648h5 1/1 Terminating 0 79s
...
myapp-without-hook-77448c5b9f-b9rxm 1/1 Running 0 83s
myapp-without-hook-77448c5b9f-gbrvc 1/1 Terminating 0 83s
myapp-without-hook-789fd7c548-79h9d 1/1 Running 0 5s
myapp-without-hook-789fd7c548-chbwh 0/1 ContainerCreating 0 1s
myapp-without-hook-789fd7c548-m5bpw 1/1 Running 0 9s
...
myapp-without-hook-77448c5b9f-b9rxm 1/1 Running 0 85s
myapp-without-hook-77448c5b9f-gbrvc 0/1 Terminating 0 85s
myapp-without-hook-789fd7c548-79h9d 1/1 Running 0 7s
myapp-without-hook-789fd7c548-chbwh 0/1 ContainerCreating 0 3s
myapp-without-hook-789fd7c548-m5bpw 1/1 Running 0 11s
...
myapp-without-hook-77448c5b9f-b9rxm 0/1 Terminating 0 91s
myapp-without-hook-77448c5b9f-gbrvc 0/1 Terminating 0 91s
myapp-without-hook-789fd7c548-79h9d 1/1 Running 0 13s
myapp-without-hook-789fd7c548-chbwh 1/1 Running 0 9s
myapp-without-hook-789fd7c548-m5bpw 1/1 Running 0 17s
負載壓測可用性差距
如果我們執(zhí)行從舊版本到新版本的滾動更新,并按照pod處于活動狀態(tài)并準備就緒的輸出,則首先行為似乎是有效的。但是,正如我們所看到的,從舊版本到新版本的轉(zhuǎn)換并不總是非常順利,也就是說,應(yīng)用程序可能會丟失一些客戶端的請求。
為了測試,正在請求的連接是否丟失,特別是那些針對正在停止使用的實例的請求,我們可以使用連接到我們的應(yīng)用程序的負載測試工具。我們感興趣的要點是是否所有HTTP請求都得到了正確處理,包括HTTP保持活動連接。為此,我們使用負載壓測工具,例如Apache Bench或Fortio。
我們使用多個線程(即多個連接)以并發(fā)方式通過HTTP連接到我們正在運行的應(yīng)用程序。這里我們不關(guān)注延遲或吞吐量,而是對響應(yīng)狀態(tài)和潛在的連接故障感興趣。
? root@ubuntu ~ fortio load -a -c 8 -qps 500 -t 60s "http://$NodeIP:$NodePort/"
這里是用NodePort的服務(wù)發(fā)布形式,發(fā)布我們的示例程序$NodeIP
$NodePort
root@k8s-master-1:~/k8s_manifests/zero-downtime-tutorial# kubectl get svc
myapp-without-hook NodePort 10.68.21.130 <none> 80:31467/TCP 3s
在Fortio的示例中,每秒500個請求和8個線程并發(fā)保持活動連接的調(diào)用如下所示
? ? root@ubuntu ~ fortio load -a -c 8 -qps 500 -t 60s "http://192.168.2.12:31467/"
Fortio 1.3.1 running at 500 queries per second, 1->1 procs, for 1m0s: http://192.168.2.12:31467/
13:26:32 I httprunner.go:82> Starting http test for http://192.168.2.12:31467/ with 8 threads at 500.0 qps
Starting at 500 qps with 8 thread(s) [gomax 1] for 1m0s : 3750 calls each (total 30000)
...
Sockets used: 23 (for perfect keepalive, would be 8)
Code -1 : 4 (0.0 %)
Code 200 : 29996 (100.0 %)
Response Header Sizes : count 30000 avg 235.96853 +/- 2.725 min 0 max 236 sum 7079056
Response Body/Total Sizes : count 30000 avg 330.25453 +/- 3.931 min 0 max 331 sum 9907636
All done 30000 calls (plus 8 warmup) 2.489 ms avg, 500.0 qps
輸出表明并非所有請求都可以成功處理code 200。以上的壓測結(jié)果顯示還是有4個請求code -1處理失敗。
我們可以運行多個測試場景,通過不同的方式連接到應(yīng)用程序,例如通過Kubernetes ingress,或通過服務(wù)直接從集群內(nèi)部連接。我們將看到滾動更新期間的行為可能會有所不同,具體取決于我們的測試設(shè)置如何連接。與通過入口連接相比,從群集內(nèi)部連接到服務(wù)的客戶端可能不會遇到任何數(shù)量的失敗連接。
更新過程發(fā)生了什么
現(xiàn)在的問題是,當Kubernetes在滾動更新期間重新路由流量時,從舊的實例版本到新的pod實例版本會發(fā)生什么。我們來看看Kubernetes如何管理工作負載連接。
如果我們的客戶端,即零停機測試,F(xiàn)ortio直接通過NodeIP:NodePort形式從集群外部連接到服務(wù),它通常使用通過集群DNS解析的服務(wù)VIP,最終到達Pod實例,具體的實現(xiàn)通過在每個Kubernetes節(jié)點上運行的kube-proxy實現(xiàn)的,并更新iptables轉(zhuǎn)發(fā)規(guī)則到pod的IP地址。
NodePort形式
Ingress的方式
無論我們?nèi)绾芜B接到我們的應(yīng)用程序,Kubernetes都旨在最大限度地減少滾動更新過程中的服務(wù)中斷。
新的pod處于活動狀態(tài)并正常處理客戶端請求,Kubernetes將使舊的pod停止服務(wù),從而更新pod的狀態(tài)為Terminating,將其從端點對象中刪除,然后發(fā)送一個SIGTERM
。在SIGTERM
導致容器優(yōu)雅正常退出,并且不接受任何新客戶端連接。將pod從endpoints列表中剔除后,負載均衡器會將流量路由到剩余的(新)流量。這就是我們部署中可用性差距的原因; 在終端信號之前,當負載均衡器注意到更改并且可以更新其配置時,已通過SIGTERM
信號停用Pod。這種重新配置是異步
發(fā)生的,因此不能保證正確的排序,將導致很少的不幸請求被路由到終止pod 從而出現(xiàn)少量的請求丟失現(xiàn)象。
走向零停機時間
如何增強我們的應(yīng)用程序以實現(xiàn)(真正的)零停機時間遷移?
首先,實現(xiàn)這一目標的先決條件是我們的容器正確處理SIGTERM
信號,即該進程將在Unix上正常退出??纯碐oogle 構(gòu)建容器最佳實踐如何實現(xiàn)這一目標。
下一步是包括準備探針,檢查我們的應(yīng)用程序是否已準備好處理流量。理想情況下,探針已經(jīng)檢查了需要預(yù)熱的功能狀態(tài),例如高速緩存或servlet初始化。
準備情況探測是我們平滑滾動更新的起點。為了解決pod終端當前沒有阻塞的問題并等到負載均衡器重新配置,我們將包含一個preStop
生命周期鉤子。在容器終止之前調(diào)用此掛鉤。
容器生命周期鉤子
生命周期鉤子是同步的,因此必須在最終終止信號被發(fā)送到容器之前完成。在我們的例子中,我們使用這個鉤子來簡單地等待,然后SIGTERM
才會終止應(yīng)用程序進程。同時,Kubernetes將從端點對象中刪除pod,因此pod將從我們的負載平衡器中排除。我們的生命周期鉤子等待時間可確保在應(yīng)用程序進程停止之前重新配置負載平衡器。
為了實現(xiàn)此行為,我們在myapp的deployment部署中定義了一個preStop鉤子,依賴于您選擇的技術(shù)如何實現(xiàn)就緒和存活探針以及生命周期鉤子行為;
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: myapp
spec:
terminationGracePeriodSeconds: 10
containers:
- name: nginx
#image: 314315960/zero-downtime-tutorial:green
image: 314315960/zero-downtime-tutorial:blue
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /healthy.html
port: 80
periodSeconds: 1
successThreshold: 1
failureThreshold: 2
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "rm /usr/share/nginx/html/healthy.html && sleep 10"]
readinessProbe
探針檢查該pod是否準備就緒
proStop
鉤子必須在刪除容器的調(diào)用之前完成,這里選擇刪除/healthy.html探針及程序睡10秒的以提供同步寬限期。只有在完成這一系列操作,pod才會繼續(xù)正常退出。
root@k8s-master-1:~/k8s_manifests/zero-downtime-tutorial# kubectl get deploy,svc,pod
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.extensions/myapp 3/3 3 3 6m38s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
myapp NodePort 10.68.57.14 <none> 80:26532/TCP 7m32s
service/myapp NodePort 10.68.57.14 <none> 80:26532/TCP 6m31s
NAME READY STATUS RESTARTS AGE
pod/myapp-56646bbf86-8dw9f 1/1 Running 0 6m38s
pod/myapp-56646bbf86-mbzgj 1/1 Running 0 6m38s
pod/myapp-56646bbf86-mpwk2 1/1 Running 0 6m38s
#更換image: 314315960/zero-downtime-tutorial:green
root@k8s-master-1:~/k8s_manifests/zero-downtime-tutorial# kubectl edit deployment myapp
deployment.extensions/myapp edited
Fortio 壓測
? root@ubuntu ? ~ ? fortio load -a -c 8 -qps 500 -t 60s "http://192.168.2.12:26532/"
Fortio 1.3.1 running at 500 queries per second, 1->1 procs, for 1m0s: http://192.168.2.12:26532/
14:11:27 I httprunner.go:82> Starting http test for http://192.168.2.12:26532/ with 8 threads at 500.0 qps
Starting at 500 qps with 8 thread(s) [gomax 1] for 1m0s : 3750 calls each (total 30000)
...
Sockets used: 17 (for perfect keepalive, would be 8)
Code 200 : 30000 (100.0 %)
Response Header Sizes : count 30000 avg 236 +/- 0 min 236 max 236 sum 7080000
Response Body/Total Sizes : count 30000 avg 330.2126 +/- 0.9771 min 329 max 331 sum 9906378
All done 30000 calls (plus 8 warmup) 2.332 ms avg, 499.9 qps
輸出表明所有30000個請求都可以成功處理code 200。
總結(jié)
Kubernetes在編寫具有生產(chǎn)準備的應(yīng)用程序方面做得非常出色。然而,為了在生產(chǎn)中運行我們的企業(yè)系統(tǒng),我們的工程師必須了解Kubernetes如何在引擎蓋下運行以及我們的應(yīng)用程序在啟動和關(guān)閉期間的行為過程。
以上用到的yaml和dockerfile,都已經(jīng)上傳到github
參考文檔:
https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/
https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/
https://kubernetes.io/docs/tutorials/kubernetes-basics/update/update-intro/
https://blog.sebastian-daschner.com/entries/zero-downtime-updates-kubernetes
https://github.com/chrismoos/zero-downtime-tutorial