Kubernetes服務pod的健康檢測liveness和readiness詳解

Serverless和Devops發表於2021-05-22

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 值越大,初始銷燬的舊副本數量就越多。

正常更新理想情況下,我們這次版本釋出案例滾動更新的過程是:

  1. 首先建立4個新版本的pod,使副本總數量達到14個
  2. 然後再銷燬3箇舊版本的pod,使可用的副本數量降為7個
  3. 當這3箇舊版本的pod被 成功銷燬後,可再建立3個新版本的pod,使總的副本數量保持為14個
  4. 當新版本的pod通過Readiness 檢測後,會使可用的pod副本數量增加超過7個
  5. 然後可以繼續銷燬更多的舊版本的pod,使整體可用的pod數量回到7個
  6. 隨著舊版本的pod銷燬,使pod副本總數量低於14個,這樣就可以繼續建立更多的新版本的pod
  7. 這個新增銷燬流程會持續地進行,最終所有舊版本的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變化,加深理解 

相關文章