Kubernetes服務pod的健康檢測liveness和readiness詳解
接下來給大家講解下在K8S上,我們如果對我們的業務服務進行健康檢測。
Health Check、restartPolicy
這裡我們再進一步,來聊聊K8s上面服務的健康檢測特性。在K8s上,強大的自愈能力是這個容器編排引擎的非常重要的一個特性,自愈的預設實現方式是通過自動重啟發生故障的容器,使之恢復正常。除此之外,我們還可以利用Liveness 和 Readiness檢測機制來設定更為精細的健康檢測指標,從而實現如下的需求:
- 零停機部署
- 避免部署無效的服務映象
- 更加安全地滾動升級
下面我們先來實踐學習下K8s的Healthz Check功能,我們先來學習下K8s預設的健康檢測機制:
每個容器啟動時都會執行一個程式,此程式是由Dockerfile的CMD 或 ENTRYPOINT來指定,當容器內程式退出時返回狀態碼為非零,則會認為容器發生了故障,K8s就會根據restartPolicy來重啟這個容器,以達到自愈的效果。
下面我們來動手實踐下,模擬一個容器發生故障時的場景 :
# 先來生成一個pod的yaml配置檔案,並對其進行相應修改
# kubectl run busybox --image=busybox --dry-run=client -o yaml > testHealthz.yaml
# vim testHealthz.yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: busybox
name: busybox
spec:
containers:
- image: busybox
name: busybox
resources: {}
args:
- /bin/sh
- -c
- sleep 10; exit 1 # 並新增pod執行指定指令碼命令,模擬容器啟動10秒後發生故障,退出狀態碼為1
dnsPolicy: ClusterFirst
restartPolicy: OnFailure # 將預設的Always修改為OnFailure
status: {}
重啟策略 | 說明 |
---|---|
Always | 當容器失效時,由kubelet自動重啟該容器 |
OnFailure | 當容器終止執行且退出碼不為0時,由kubelet自動重啟該容器 |
Never | 不論容器執行狀態如何,kubelet都不會重啟該容器 |
執行配置建立pod
# kubectl apply -f testHealthz.yaml
pod/busybox created
# 觀察幾分鐘,利用-w 引數來持續監聽pod的狀態變化
# kubectl get pod -w
NAME READY STATUS RESTARTS AGE
busybox 0/1 ContainerCreating 0 4s
busybox 1/1 Running 0 6s
busybox 0/1 Error 0 16s
busybox 1/1 Running 1 22s
busybox 0/1 Error 1 34s
busybox 0/1 CrashLoopBackOff 1 47s
busybox 1/1 Running 2 63s
busybox 0/1 Error 2 73s
busybox 0/1 CrashLoopBackOff 2 86s
busybox 1/1 Running 3 109s
busybox 0/1 Error 3 2m
busybox 0/1 CrashLoopBackOff 3 2m15s
busybox 1/1 Running 4 3m2s
busybox 0/1 Error 4 3m12s
busybox 0/1 CrashLoopBackOff 4 3m23s
busybox 1/1 Running 5 4m52s
busybox 0/1 Error 5 5m2s
busybox 0/1 CrashLoopBackOff 5 5m14s
上面可以看到這個測試pod被重啟了5次,然而服務始終正常不了,就會保持在CrashLoopBackOff了,等待運維人員來進行下一步錯誤排查
注:kubelet會以指數級的退避延遲(10s,20s,40s等)重新啟動它們,上限為5分鐘
這裡我們是人為模擬服務故障來進行的測試,在實際生產工作中,對於業務服務,我們如何利用這種重啟容器來恢復的機制來配置業務服務呢,答案是`liveness`檢測
Liveness
Liveness檢測讓我們可以自定義條件來判斷容器是否健康,如果檢測失敗,則K8s會重啟容器,我們來個例子實踐下,準備如下yaml配置並儲存為liveness.yaml:
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: liveness
spec:
restartPolicy: OnFailure
containers:
- name: liveness
image: busybox
args:
- /bin/sh
- -c
- touch /tmp/healthy; sleep 30; rm -f /tmp/healthy; sleep 600
livenessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 10 # 容器啟動 10 秒之後開始檢測
periodSeconds: 5 # 每隔 5 秒再檢測一次
啟動程式首先建立檔案 /tmp/healthy,30 秒後刪除,在我們的設定中,如果 /tmp/healthy 檔案存在,則認為容器處於正常狀態,反正則發生故障。
livenessProbe 部分定義如何執行 Liveness 檢測:
檢測的方法是:通過 cat 命令檢查 /tmp/healthy 檔案是否存在。如果命令執行成功,返回值為零,K8s 則認為本次 Liveness 檢測成功;如果命令返回值非零,本次 Liveness 檢測失敗。
initialDelaySeconds: 10 指定容器啟動 10 之後開始執行 Liveness 檢測,我們一般會根據應用啟動的準備時間來設定。比如某個應用正常啟動要花 30 秒,那麼 initialDelaySeconds 的值就應該大於 30。
periodSeconds: 5 指定每 5 秒執行一次 Liveness 檢測。K8s 如果連續執行 3 次 Liveness 檢測均失敗,則會殺掉並重啟容器。
接著來建立這個Pod:
# kubectl apply -f liveness.yaml
pod/liveness created
從配置檔案可知,最開始的 30 秒,/tmp/healthy 存在,cat 命令返回 0,Liveness 檢測成功,這段時間 kubectl describe pod liveness 的 Events部分會顯示正常的日誌
# kubectl describe pod liveness
......
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 53s default-scheduler Successfully assigned default/liveness to 10.0.1.203
Normal Pulling 52s kubelet Pulling image "busybox"
Normal Pulled 43s kubelet Successfully pulled image "busybox"
Normal Created 43s kubelet Created container liveness
Normal Started 42s kubelet Started container liveness
35 秒之後,日誌會顯示 /tmp/healthy 已經不存在,Liveness 檢測失敗。再過幾十秒,幾次檢測都失敗後,容器會被重啟。
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 3m53s default-scheduler Successfully assigned default/liveness to 10.0.1.203
Normal Pulling 73s (x3 over 3m52s) kubelet Pulling image "busybox"
Normal Pulled 62s (x3 over 3m43s) kubelet Successfully pulled image "busybox"
Normal Created 62s (x3 over 3m43s) kubelet Created container liveness
Normal Started 62s (x3 over 3m42s) kubelet Started container liveness
Warning Unhealthy 18s (x9 over 3m8s) kubelet Liveness probe failed: cat: can't open '/tmp/healthy': No such file or directory
Normal Killing 18s (x3 over 2m58s) kubelet Container liveness failed liveness probe, will be restarted
除了 Liveness 檢測,Kubernetes Health Check 機制還包括 Readiness 檢測。
Readiness
我們可以通過Readiness檢測來告訴K8s什麼時候可以將pod加入到服務Service的負載均衡池中,對外提供服務,這個在生產場景服務釋出新版本時非常重要,當我們上線的新版本發生程式錯誤時,Readiness會通過檢測釋出,從而不匯入流量到pod內,將服務的故障控制在內部,在生產場景中,建議這個是必加的,Liveness不加都可以,因為有時候我們需要保留服務出錯的現場來查詢日誌,定位問題,告之開發來修復程式。
Readiness 檢測的配置語法與 Liveness 檢測完全一樣,下面是個例子:
apiVersion: v1
kind: Pod
metadata:
labels:
test: liveness
name: liveness
spec:
restartPolicy: OnFailure
containers:
- name: liveness
image: busybox
args:
- /bin/sh
- -c
- touch /tmp/healthy; sleep 30; rm -f /tmp/healthy; sleep 600
readinessProbe: # 這裡將livenessProbe換成readinessProbe即可,其它配置都一樣
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 10 # 容器啟動 10 秒之後開始檢測
periodSeconds: 5 # 每隔 5 秒再檢測一次
儲存上面這個配置為readiness.yaml,並執行它生成pod:
# kubectl apply -f readiness.yaml
pod/liveness created
# 觀察,在剛開始建立時,檔案並沒有被刪除,所以檢測一切正常
# kubectl get pod
NAME READY STATUS RESTARTS AGE
liveness 1/1 Running 0 50s
# 然後35秒後,檔案被刪除,這個時候READY狀態就會發生變化,K8s會斷開Service到pod的流量
# kubectl describe pod liveness
......
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 56s default-scheduler Successfully assigned default/liveness to 10.0.1.203
Normal Pulling 56s kubelet Pulling image "busybox"
Normal Pulled 40s kubelet Successfully pulled image "busybox"
Normal Created 40s kubelet Created container liveness
Normal Started 40s kubelet Started container liveness
Warning Unhealthy 5s (x2 over 10s) kubelet Readiness probe failed: cat: can't open '/tmp/healthy': No such file or directory
# 可以看到pod的流量被斷開,這時候即使服務出錯,對外界來說也是感知不到的,這時候我們運維人員就可以進行故障排查了
# kubectl get pod
NAME READY STATUS RESTARTS AGE
liveness 0/1 Running 0 61s
下面對 Liveness 檢測和 Readiness 檢測做個比較:
Liveness 檢測和 Readiness 檢測是兩種 Health Check 機制,如果不特意配置,Kubernetes 將對兩種檢測採取相同的預設行為,即通過判斷容器啟動程式的返回值是否為零來判斷檢測是否成功。
兩種檢測的配置方法完全一樣,支援的配置引數也一樣。不同之處在於檢測失敗後的行為:Liveness 檢測是重啟容器;Readiness 檢測則是將容器設定為不可用,不接收 Service 轉發的請求。
Liveness 檢測和 Readiness 檢測是獨立執行的,二者之間沒有依賴,所以可以單獨使用,也可以同時使用。用 Liveness 檢測判斷容器是否需要重啟以實現自愈;用 Readiness 檢測判斷容器是否已經準備好對外提供服務。
Health Check 在 業務生產中滾動更新(rolling update)的應用場景
對於運維人員來說,將服務的新專案程式碼更新上線,確保其穩定執行是一項很關鍵,且重複性很高的任務,在傳統模式下,我們一般是用saltsatck或者ansible等批量管理工具來推送程式碼到各臺伺服器上進行更新,那麼在K8s上,這個更新流程就被簡化了,在後面高階章節我會講到CI/CD自動化流程,大致就是開發人員開發好程式碼上傳程式碼倉庫即會觸發CI/CD流程,這之間基本無需運維人員的參與。那麼在這麼高度自動化的流程中,我們運維人員怎麼確保服務能穩定上線呢?Health Check裡面的Readiness 能發揮很關鍵的作用,這個其實在上面也有講過,這裡我們再以例項來說一遍,加深印象:
我們準備一個deployment資源的yaml檔案
# cat myapp-v1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mytest
spec:
replicas: 10 # 這裡準備10個數量的pod
selector:
matchLabels:
app: mytest
template:
metadata:
labels:
app: mytest
spec:
containers:
- name: mytest
image: busybox
args:
- /bin/sh
- -c
- sleep 10; touch /tmp/healthy; sleep 30000
readinessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 10
periodSeconds: 5
執行這個配置
# kubectl apply -f myapp-v1.yaml --record
deployment.apps/mytest created
# 等待一會,可以看到所有pod已正常執行
# kubectl get pod
NAME READY STATUS RESTARTS AGE
mytest-d9f48585b-2lmh2 1/1 Running 0 3m22s
mytest-d9f48585b-5lh9l 1/1 Running 0 3m22s
mytest-d9f48585b-cwb8l 1/1 Running 0 3m22s
mytest-d9f48585b-f6tzc 1/1 Running 0 3m22s
mytest-d9f48585b-hb665 1/1 Running 0 3m22s
mytest-d9f48585b-hmqrw 1/1 Running 0 3m22s
mytest-d9f48585b-jm8bm 1/1 Running 0 3m22s
mytest-d9f48585b-kxm2m 1/1 Running 0 3m22s
mytest-d9f48585b-lqpr9 1/1 Running 0 3m22s
mytest-d9f48585b-pk75z 1/1 Running 0 3m22s
接著我們來準備更新這個服務,並且人為模擬版本故障來進行觀察,新準備一個配置myapp-v2.yaml
# cat myapp-v2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mytest
spec:
strategy:
rollingUpdate:
maxSurge: 35% # 滾動更新的副本總數最大值(以10的基數為例):10 + 10 * 35% = 13.5 --> 14
maxUnavailable: 35% # 可用副本數最大值(預設值兩個都是25%): 10 - 10 * 35% = 6.5 --> 7
replicas: 10
selector:
matchLabels:
app: mytest
template:
metadata:
labels:
app: mytest
spec:
containers:
- name: mytest
image: busybox
args:
- /bin/sh
- -c
- sleep 30000 # 可見這裡並沒有生成/tmp/healthy這個檔案,所以下面的檢測必然失敗
readinessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 10
periodSeconds: 5
很明顯這裡因為我們更新的這個v2版本里面不會生成/tmp/healthy檔案,那麼自動是無法通過Readiness 檢測的,詳情如下:
# kubectl apply -f myapp-v2.yaml --record
deployment.apps/mytest configured
# kubectl get deployment mytest
NAME READY UP-TO-DATE AVAILABLE AGE
mytest 7/10 7 7 4m58s
# READY 現在正在執行的只有7個pod
# UP-TO-DATE 表示當前已經完成更新的副本數:即 7 個新副本
# AVAILABLE 表示當前處於 READY 狀態的副本數
# kubectl get pod
NAME READY STATUS RESTARTS AGE
mytest-7657789bc7-5hfkc 0/1 Running 0 3m2s
mytest-7657789bc7-6c5lg 0/1 Running 0 3m2s
mytest-7657789bc7-c96t6 0/1 Running 0 3m2s
mytest-7657789bc7-nbz2q 0/1 Running 0 3m2s
mytest-7657789bc7-pt86c 0/1 Running 0 3m2s
mytest-7657789bc7-q57gb 0/1 Running 0 3m2s
mytest-7657789bc7-x77cg 0/1 Running 0 3m2s
mytest-d9f48585b-2bnph 1/1 Running 0 5m4s
mytest-d9f48585b-965t4 1/1 Running 0 5m4s
mytest-d9f48585b-cvq7l 1/1 Running 0 5m4s
mytest-d9f48585b-hvpnq 1/1 Running 0 5m4s
mytest-d9f48585b-k89zs 1/1 Running 0 5m4s
mytest-d9f48585b-wkb4b 1/1 Running 0 5m4s
mytest-d9f48585b-wrkzf 1/1 Running 0 5m4s
# 上面可以看到,由於 Readiness 檢測一直沒通過,所以新版本的pod都是Not ready狀態的,這樣就保證了錯誤的業務程式碼不會被外界請求到
# kubectl describe deployment mytest
# 下面擷取一些這裡需要的關鍵資訊
......
Replicas: 10 desired | 7 updated | 14 total | 7 available | 7 unavailable
......
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 5m55s deployment-controller Scaled up replica set mytest-d9f48585b to 10
Normal ScalingReplicaSet 3m53s deployment-controller Scaled up replica set mytest-7657789bc7 to 4 # 啟動4個新版本的pod
Normal ScalingReplicaSet 3m53s deployment-controller Scaled down replica set mytest-d9f48585b to 7 # 將舊版本pod數量降至7
Normal ScalingReplicaSet 3m53s deployment-controller Scaled up replica set mytest-7657789bc7 to 7 # 新增3個啟動至7個新版本
綜合上面的分析,我們很真實的模擬一次K8s上次錯誤的程式碼上線流程,所幸的是這裡有Health Check的Readiness檢測幫我們遮蔽了有錯誤的副本,不至於被外面的流量請求到,同時保留了大部分舊版本的pod,因此整個服務的業務並沒有因這此更新失敗而受到影響。
接下來我們詳細分析下滾動更新的原理,為什麼上面服務新版本建立的pod數量是7個,同時只銷毀了3箇舊版本的pod呢?
原因就在於這段配置:
我們不顯式配置這段的話,預設值均是25%
strategy:
rollingUpdate:
maxSurge: 35%
maxUnavailable: 35%
滾動更新通過引數maxSurge和maxUnavailable來控制pod副本數量的更新替換。
maxSurge
這個引數控制滾動更新過程中pod副本總數超過設定總副本數量的上限。maxSurge 可以是具體的整數(比如 3),也可以是百分比,向上取整。maxSurge 預設值為 25%
在上面測試的例子裡面,pod的總副本數量是10,那麼在更新過程中,總副本數量的上限大最值計劃公式為:
10 + 10 * 35% = 13.5 --> 14
我們檢視下更新deployment的描述資訊:
Replicas: 10 desired | 7 updated | 14 total | 7 available | 7 unavailable
舊版本available 的數量7個 + 新版本unavailable`的數量7個 = 總數量 14 total
maxUnavailable
這個引數控制滾動更新過程中不可用的pod副本總數量的值,同樣,maxUnavailable 可以是具體的整數(比如 3),也可以是百分百,向下取整。maxUnavailable 預設值為 25%。
在上面測試的例子裡面,pod的總副本數量是10,那麼要保證正常可用的pod副本數量為:
10 - 10 * 35% = 6.5 --> 7
所以我們在上面檢視的描述資訊裡,7 available 正常可用的pod數量值就為7
maxSurge 值越大,初始建立的新副本數量就越多;maxUnavailable 值越大,初始銷燬的舊副本數量就越多。
正常更新理想情況下,我們這次版本釋出案例滾動更新的過程是:
- 首先建立4個新版本的pod,使副本總數量達到14個
- 然後再銷燬3箇舊版本的pod,使可用的副本數量降為7個
- 當這3箇舊版本的pod被 成功銷燬後,可再建立3個新版本的pod,使總的副本數量保持為14個
- 當新版本的pod通過Readiness 檢測後,會使可用的pod副本數量增加超過7個
- 然後可以繼續銷燬更多的舊版本的pod,使整體可用的pod數量回到7個
- 隨著舊版本的pod銷燬,使pod副本總數量低於14個,這樣就可以繼續建立更多的新版本的pod
- 這個新增銷燬流程會持續地進行,最終所有舊版本的pod會被新版本的pod逐漸替換,整個滾動更新完成
而我們這裡的實際情況是在第4步就卡住了,新版本的pod數量無法能過Readiness 檢測。上面的描述資訊最後面的事件部分的日誌也詳細說明了這一切:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 5m55s deployment-controller Scaled up replica set mytest-d9f48585b to 10
Normal ScalingReplicaSet 3m53s deployment-controller Scaled up replica set mytest-7657789bc7 to 4 # 啟動4個新版本的pod
Normal ScalingReplicaSet 3m53s deployment-controller Scaled down replica set mytest-d9f48585b to 7 # 將舊版本pod數量降至7
Normal ScalingReplicaSet 3m53s deployment-controller Scaled up replica set mytest-7657789bc7 to 7 # 新增3個啟動至7個新版本
這裡按正常的生產處理流程,在獲取足夠的新版本錯誤資訊提交給開發分析後,我們可以通過kubectl rollout undo 來回滾到上一個正常的服務版本:
# 先檢視下要回滾版本號前面的數字,這裡為1
# kubectl rollout history deployment mytest
deployment.apps/mytest
REVISION CHANGE-CAUSE
1 kubectl apply --filename=myapp-v1.yaml --record=true
2 kubectl apply --filename=myapp-v2.yaml --record=true
# kubectl rollout undo deployment mytest --to-revision=1
deployment.apps/mytest rolled back
# kubectl get deployment mytest
NAME READY UP-TO-DATE AVAILABLE AGE
mytest 10/10 10 10 96m
# kubectl get pod
NAME READY STATUS RESTARTS AGE
mytest-d9f48585b-2bnph 1/1 Running 0 96m
mytest-d9f48585b-8nvhd 1/1 Running 0 2m13s
mytest-d9f48585b-965t4 1/1 Running 0 96m
mytest-d9f48585b-cvq7l 1/1 Running 0 96m
mytest-d9f48585b-hvpnq 1/1 Running 0 96m
mytest-d9f48585b-k89zs 1/1 Running 0 96m
mytest-d9f48585b-qs5c6 1/1 Running 0 2m13s
mytest-d9f48585b-wkb4b 1/1 Running 0 96m
mytest-d9f48585b-wprlz 1/1 Running 0 2m13s
mytest-d9f48585b-wrkzf 1/1 Running 0 96m
OK,到這裡為止,我們真實的模擬了一次有問題的版本釋出及回滾,並且可以看到,在這整個過程中,雖然出現了問題,但我們的業務依然是沒有受到任何影響的,這就是K8s的魅力所在。
pod小怪戰鬥(作業)
# 把上面整個更新及回滾的案例,自己再測試一遍,注意觀察其中的pod變化,加深理解