默認的健康檢查
強大的自愈能力是 Kubernetes 這類容器編排引擎的一個重要特性。自愈的默認實現方式是自動重啟發生故障的容器。除此之外,用戶還可以利用 Liveness 和 Readiness 探測機制設置更精細的健康檢查,進而實現如下需求:
- 零停機部署。
- 避免部署無效的鏡像。
- 更加安全的滾動升級。
每個容器啟動時都會執行一個進程,此進程由 Dockerfile 的 CMD 或 ENTRYPOINT 指定。如果進程退出時返回碼非零,則認為容器發生故障,Kubernetes 就會根據 restartPolicy 重啟容器。
第一步: 下面我們模擬一個容器發生故障的場景,Pod 配置文件如下:
Pod 的 restartPolicy 設置為 OnFailure,默認為 Always。
sleep 10; exit 1 模擬容器啟動 10 秒后發生故障。、
第二步:執行 kubectl apply 創建 Pod,命名為 healthcheck。
[root@ken ~]# kubectl apply -f healthcheck.yml pod/healthcheck created
第三步:過幾分鍾查看 Pod 的狀態:
[root@ken ~]# kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES healthcheck 0/1 CrashLoopBackOff 2 85s 10.244.1.47 host1 <none> <none>
可看到容器當前已經重啟了 2次。
在上面的例子中,容器進程返回值非零,Kubernetes 則認為容器發生故障,需要重啟。但有不少情況是發生了故障,但進程並不會退出。比如訪問 Web 服務器時顯示 500 內部錯誤,可能是系統超載,也可能是資源死鎖,此時 httpd 進程並沒有異常退出,在這種情況下重啟容器可能是最直接最有效的解決方案,那我們如何利用 Health Check 機制來處理這類場景呢?
Liveness探測
Liveness 探測讓用戶可以自定義判斷容器是否健康的條件。如果探測失敗,Kubernetes 就會重啟容器。
第一步:創建如下 Pod:
啟動進程首先創建文件 /tmp/healthy,30 秒后刪除,在我們的設定中,如果 /tmp/healthy 文件存在,則認為容器處於正常狀態,反正則發生故障。
livenessProbe 部分定義如何執行 Liveness 探測:
探測的方法是:通過 cat 命令檢查 /tmp/healthy 文件是否存在。如果命令執行成功,返回值為零,Kubernetes 則認為本次 Liveness 探測成功;如果命令返回值非零,本次 Liveness 探測失敗。
initialDelaySeconds: 10 指定容器啟動 10 之后開始執行 Liveness 探測,我們一般會根據應用啟動的准備時間來設置。比如某個應用正常啟動要花 30 秒,那么 initialDelaySeconds 的值就應該大於 30。
periodSeconds: 5 指定每 5 秒執行一次 Liveness 探測。Kubernetes 如果連續執行 3 次 Liveness 探測均失敗,則會殺掉並重啟容器。
第二步:下面創建 Pod liveness:
[root@ken ~]# kubectl apply -f healthcheck.yml pod/liveness created
從配置文件可知,最開始的 30 秒,/tmp/healthy 存在,cat 命令返回 0,Liveness 探測成功
第三步:這段時間 kubectl describe pod liveness 的 Events部分會顯示正常的日志。
[root@ken ~]# kubectl describe pod liveness ... Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 30s default-scheduler Successfully assigned default/liveness to host1 Normal Pulling 29s kubelet, host1 pulling image "busybox" Normal Pulled 27s kubelet, host1 Successfully pulled image "busybox" Normal Created 27s kubelet, host1 Created container Normal Started 27s kubelet, host1 Started container
第四步:35秒后再次查看日志
35 秒之后,日志會顯示 /tmp/healthy 已經不存在,Liveness 探測失敗。再過幾十秒,幾次探測都失敗后,容器會被重啟。
[root@ken ~]# kubectl describe pod liveness ... Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 60s default-scheduler Successfully assigned default/liveness to host1 Normal Pulling 59s kubelet, host1 pulling image "busybox" Normal Pulled 57s kubelet, host1 Successfully pulled image "busybox" Normal Created 57s kubelet, host1 Created container Normal Started 57s kubelet, host1 Started container Warning Unhealthy 13s (x3 over 23s) kubelet, host1 Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory
第五步:查看pod
可以發現容器開始被重啟
Readiness 探測
除了 Liveness 探測,Kubernetes Health Check 機制還包括 Readiness 探測。
用戶通過 Liveness 探測可以告訴 Kubernetes 什么時候通過重啟容器實現自愈;Readiness 探測則是告訴 Kubernetes 什么時候可以將容器加入到 Service 負載均衡池中,對外提供服務。
第一步:Readiness 探測的配置語法與 Liveness 探測完全一樣
這個配置文件只是將前面例子中的 liveness 替換為了 readiness,我們看看有什么不同的效果。
第二步:部署
[root@ken ~]# kubectl apply -f readness.yml pod/readiness created [root@ken ~]# kubectl get pod readiness NAME READY STATUS RESTARTS AGE readiness 0/1 Running 0 17s [root@ken ~]# kubectl get pod readiness NAME READY STATUS RESTARTS AGE readiness 1/1 Running 0 18s [root@ken ~]# kubectl get pod readiness NAME READY STATUS RESTARTS AGE readiness 0/1 Running 0 84s
Pod readiness 的 READY 狀態經歷了如下變化:
剛被創建時,READY 狀態為不可用。
15 秒后(initialDelaySeconds + periodSeconds),第一次進行 Readiness 探測並成功返回,設置 READY 為可用。
30 秒后,/tmp/healthy 被刪除,連續 3 次 Readiness 探測均失敗后,READY 被設置為不可用。
第三步:通過 kubectl describe pod readiness 也可以看到 Readiness 探測失敗的日志。
[root@ken ~]# kubectl describe pod readiness ... Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 104s default-scheduler Successfully assigned default/readiness to host1 Normal Pulling 103s kubelet, host1 pulling image "busybox" Normal Pulled 101s kubelet, host1 Successfully pulled image "busybox" Normal Created 101s kubelet, host1 Created container Normal Started 100s kubelet, host1 Started container Warning Unhealthy 1s (x14 over 66s) kubelet, host1 Readiness probe failed: cat: can't open '/tmp/healthy': No such file or directory
下面對 Liveness 探測和 Readiness 探測做個比較:
Liveness 探測和 Readiness 探測是兩種 Health Check 機制,如果不特意配置,Kubernetes 將對兩種探測采取相同的默認行為,即通過判斷容器啟動進程的返回值是否為零來判斷探測是否成功。
兩種探測的配置方法完全一樣,支持的配置參數也一樣。不同之處在於探測失敗后的行為:Liveness 探測是重啟容器;Readiness 探測則是將容器設置為不可用,不接收 Service 轉發的請求。
Liveness 探測和 Readiness 探測是獨立執行的,二者之間沒有依賴,所以可以單獨使用,也可以同時使用。用 Liveness 探測判斷容器是否需要重啟以實現自愈;用 Readiness 探測判斷容器是否已經准備好對外提供服務。
健康檢測在scale up中的應用
對於多副本應用,當執行 Scale Up 操作時,新副本會作為 backend 被添加到 Service 的負責均衡中,與已有副本一起處理客戶的請求。考慮到應用啟動通常都需要一個准備階段,比如加載緩存數據,連接數據庫等,從容器啟動到正真能夠提供服務是需要一段時間的。我們可以通過 Readiness 探測判斷容器是否就緒,避免將請求發送到還沒有 ready 的 backend。
第一步:下面是示例應用的配置文件。
重點關注 readinessProbe 部分。這里我們使用了不同於 exec 的另一種探測方法 — httpGet。Kubernetes 對於該方法探測成功的判斷條件是 http 請求的返回代碼在 200-400 之間。
schema 指定協議,支持 HTTP(默認值)和 HTTPS。
path 指定訪問路徑。
port 指定端口。
上面配置的作用是:
容器啟動 10 秒之后開始探測。
如果 http://[container_ip]:8080/healthy 返回代碼不是 200-400,表示容器沒有就緒,不接收 Service web-svc 的請求。
每隔 5 秒再探測一次。
直到返回代碼為 200-400,表明容器已經就緒,然后將其加入到 web-svc 的負責均衡中,開始處理客戶請求。
探測會繼續以 5 秒的間隔執行,如果連續發生 3 次失敗,容器又會從負載均衡中移除,直到下次探測成功重新加入。
健康檢測在滾動更新中的應用
現有一個正常運行的多副本應用,接下來對應用進行更新(比如使用更高版本的 image),Kubernetes 會啟動新副本,然后發生了如下事件:
正常情況下新副本需要 10 秒鍾完成准備工作,在此之前無法響應業務請求。
但由於人為配置錯誤,副本始終無法完成准備工作(比如無法連接后端數據庫)。
先別繼續往下看,現在請花一分鍾思考這個問題:如果沒有配置 Health Check,會出現怎樣的情況?
因為新副本本身沒有異常退出,默認的 Health Check 機制會認為容器已經就緒,進而會逐步用新副本替換現有副本,其結果就是:當所有舊副本都被替換后,整個應用將無法處理請求,無法對外提供服務。如果這是發生在重要的生產系統上,后果會非常嚴重。
如果正確配置了 Health Check,新副本只有通過了 Readiness 探測,才會被添加到 Service;如果沒有通過探測,現有副本不會被全部替換,業務仍然正常進行。
第一步:用如下配置文件 app.v1.yml 模擬一個 10 副本的應用:
10 秒后副本能夠通過 Readiness 探測。
第二步:執行部署操作
[root@ken ~]# kubectl apply -f app.v1.yml deployment.apps/app created [root@ken ~]# kubectl get deployment app NAME READY UP-TO-DATE AVAILABLE AGE app 10/10 10 10 71s [root@ken ~]# kubectl get pod NAME READY STATUS RESTARTS AGE app-56878b4676-45bmq 1/1 Running 0 80s app-56878b4676-5w5ck 1/1 Running 0 80s app-56878b4676-6mnvz 1/1 Running 0 80s app-56878b4676-fb8fk 1/1 Running 0 80s app-56878b4676-gvdbr 1/1 Running 0 80s app-56878b4676-lppmt 1/1 Running 0 80s app-56878b4676-lwr6p 1/1 Running 0 80s app-56878b4676-n8l7w 1/1 Running 0 80s app-56878b4676-rn68g 1/1 Running 0 80s app-56878b4676-z8ltd 1/1 Running 0 80s
第三步:接下來滾動更新應用,配置文件 app.v2.yml 如下:
很顯然,由於新副本中不存在 /tmp/healthy,是無法通過 Readiness 探測的。驗證如下:
第二步:查看探測結果、
[root@ken ~]# kubectl apply -f app.v2.yml --record deployment.apps/app configured [root@ken ~]# kubectl get deployment app NAME READY UP-TO-DATE AVAILABLE AGE app 8/10 5 8 4m24s [root@ken ~]# kubectl get deployment app NAME READY UP-TO-DATE AVAILABLE AGE app 8/10 5 8 4m34s [root@ken ~]# kubectl get deployment app NAME READY UP-TO-DATE AVAILABLE AGE app 8/10 5 8 4m37s [root@ken ~]# kubectl get pod NAME READY STATUS RESTARTS AGE app-56878b4676-45bmq 1/1 Running 0 4m45s app-56878b4676-5w5ck 1/1 Running 0 4m45s app-56878b4676-fb8fk 1/1 Running 0 4m45s app-56878b4676-gvdbr 1/1 Running 0 4m45s app-56878b4676-lppmt 1/1 Running 0 4m45s app-56878b4676-lwr6p 1/1 Running 0 4m45s app-56878b4676-n8l7w 1/1 Running 0 4m45s app-56878b4676-rn68g 1/1 Running 0 4m45s app-84fc656775-6s88l 0/1 Running 0 42s app-84fc656775-drg26 0/1 Running 0 42s app-84fc656775-hjpsd 0/1 Running 0 42s app-84fc656775-npn2t 0/1 Running 0 42s app-84fc656775-slknn 0/1 Running 0 42s
先關注 kubectl get pod 輸出:
從 Pod 的 AGE 欄可判斷,最后 5 個 Pod 是新副本,目前處於 NOT READY 狀態。
舊副本從最初 10 個減少到 8 個。
再來看 kubectl get deployment app 的輸出:
DESIRED 10 表示期望的狀態是 10 個 READY 的副本。
CURRENT 13 表示當前副本的總數:即 8 個舊副本 + 5 個新副本。
UP-TO-DATE 5 表示當前已經完成更新的副本數:即 5 個新副本。
AVAILABLE 8 表示當前處於 READY 狀態的副本數:即 8個舊副本。
在我們的設定中,新副本始終都無法通過 Readiness 探測,所以這個狀態會一直保持下去。
上面我們模擬了一個滾動更新失敗的場景。不過幸運的是:Health Check 幫我們屏蔽了有缺陷的副本,同時保留了大部分舊副本,業務沒有因更新失敗受到影響。
接下來我們要回答:為什么新創建的副本數是 5 個,同時只銷毀了 2 個舊副本?
原因是:滾動更新通過參數 maxSurge 和 maxUnavailable 來控制副本替換的數量。
maxSurge
此參數控制滾動更新過程中副本總數的超過 DESIRED 的上限。maxSurge 可以是具體的整數(比如 3),也可以是百分百,向上取整。maxSurge 默認值為 25%。
在上面的例子中,DESIRED 為 10,那么副本總數的最大值為:
roundUp(10 + 10 * 25%) = 13
所以我們看到 CURRENT 就是 13。
maxUnavailable
此參數控制滾動更新過程中,不可用的副本相占 DESIRED 的最大比例。 maxUnavailable 可以是具體的整數(比如 3),也可以是百分百,向下取整。maxUnavailable 默認值為 25%。
在上面的例子中,DESIRED 為 10,那么可用的副本數至少要為:
10 – roundDown(10 * 25%) = 8
所以我們看到 AVAILABLE 就是 8。
maxSurge 值越大,初始創建的新副本數量就越多;maxUnavailable 值越大,初始銷毀的舊副本數量就越多。
理想情況下,我們這個案例滾動更新的過程應該是這樣的:
首先創建 3 個新副本使副本總數達到 13 個。
然后銷毀 2 個舊副本使可用的副本數降到 8 個。
當這 2 個舊副本成功銷毀后,可再創建 2 個新副本,使副本總數保持為 13 個。
當新副本通過 Readiness 探測后,會使可用副本數增加,超過 8。
進而可以繼續銷毀更多的舊副本,使可用副本數回到 8。
舊副本的銷毀使副本總數低於 13,這樣就允許創建更多的新副本。
這個過程會持續進行,最終所有的舊副本都會被新副本替換,滾動更新完成。
更新失敗回退
[root@ken ~]# kubectl rollout history deployment app deployment.extensions/app REVISION CHANGE-CAUSE 1 <none> 2 kubectl apply --filename=app.v2.yml --record=tr [root@ken ~]# kubectl rollout undo deployment app --to-revision=1 deployment.extensions/app rolled back [root@ken ~]# kubectl get deployment app NAME READY UP-TO-DATE AVAILABLE AGE app 10/10 10 10 10m
如果要定制 maxSurge 和 maxUnavailable,可以如下配置: