Kubernetes Readiness 零停機更新

要實現 Kubernetes 零停機更新倚賴健康檢查的功能,Kubernetes 提供的健康檢查(Health Check)有兩種 LivenessReadiness, 兩種檢查的語法設定完全相同,但有不同的意義,本篇僅介紹 Readiness

readinessProbe 用來探測容器是否已經準備好接受流量,表示一個就緒的狀態。
試想當我們部署一個新的版本,如果直接將外部流量導向新的版本, 此時容器的啟動指令 command 可能才運行到一半,或因為一些例外無法回應請求, 表示這個容器還沒有準備好對外服務,如果此時 K8S 就將舊版本的 pod 換成這些新版本的 pod 作為服務提供, 就會導致這個在線服務中斷。

換句話說 readinessProbe 就是用來檢查容器是否能夠處理流量, 唯有確保容器是健康的(可處理請求),才將服務對外,正確設定 readinessProbe 就能實現零停機更新。

readinessProbe 使用方法

specs:
  containers:
    - name: server
      image: your-image
      readinessProbe:
        httpGet:
          scheme: HTTP # HTTP(default) or HTTPS
          path: /healthy
          port: 8080
          httpHeaders:
          - name: Host
            value: example.com
        initialDelaySeconds: 10
        periodSeconds: 5

上例 readinessProbe 使用 HTTP 請求 /healthy (port 8080),如果 HTTP 回應狀態碼為 200-400,表示檢測成功,容器可以對外服務。
initialDelaySeconds 表示容器啟動後多久開始檢測,可以設定一個相對長的時間,因為我們可預期容器的啟動指令不會立刻能夠處理請求,periodSeconds 表示檢測的間隔時間。
httpHeaders 不是必要的設定,可以用來設定向容器發出檢測請求的 headers,例如:你的服務需要驗證 Host 標頭,就可以透過此方法設定。

使用指令檢測

除了 httpGet 以外 readinessProbe 還支援使用指令檢測,例如:

specs:
  containers:
    - name: server
      image: your-image
      readinessProbe:
        exec:
          command: ["curl", "https://www.google.com"]
        initialDelaySeconds: 10
        periodSeconds: 5

curl https://www.google.com 可以替換成任何指令。
成功的定義為指令執行結束的狀態碼為 0,如果指令返回非零的狀態碼,表示檢測失敗,會等 5 秒後再次檢測。

優雅的關機與 Pod 終止

運用前面提到的 readinessProbe 我們可以確保 pod 在真正能處理請求的時候才對外公開, 但要做到零停機更新還有一個問題需要克服,這個問題發生在更新版本時舊版 Pod 的終止流程, Kubernetes 在終止 Pod 以前會向主程序發出 SIGTERM 信號,讓主程序得以進行優雅關機, 正在進行優雅關機的主程序無法再對外服務,但仍可能收到外部流量導致服務中斷, 把問題講得白話一點「舊 Pod 的程序已經進入無法處理請求的狀態,但仍處於對外服務的狀態」,這個問題跟 Pod 終止的生命週期有關。

Pod 終止的生命週期

  1. Pod 被設為 Terminating 狀態,並從 Service Endpoints 移除。 此時 Pod 不會再收到外部流量,但 Pod 內部的 Container 本身可能還未終止。
  2. Pod 的 preStop hook 被調用,如果沒設定就略過,接著會向主程序發出 SIGTERM 信號,讓主程序可以進行優雅的關機。
  3. Pod 依照 terminationGracePeriodSeconds 的設定(預設 30 秒),給予主程序有限的時間去處理優雅關機的工作。preStop 的運行時間也包含在這時間內。
  4. 如果在超時以前,主程序就完成優雅的關機而結束,Pod 就會被移除;如果超時後主程序仍未結束,就會收到 SIGKILL 信號遭強制刪除,接著 Pod 被移除。

步驟 1 與 2 是非同步進行,步驟 1 確實比步驟 2 先發生,但步驟 2 不會等步驟 1,因此兩步驟存在 race condition, 舉一個實際的例子會比較好理解,假設事件發生的順序如下:

  1. Pod 被設為 Terminating,發出「從 Service Endpoints 移除的請求」。
  2. Pod 收到 SIGTERM 信號,開始處理優雅關機。
  3. 使用者的流量抵達,被轉交給 Pod,此時 Pod 已經不具備處理請求的能力,使用者最後會收到 5xx 系列的錯誤回應。
  4. Pod 真正的從 Service Endpoints 中被移除。

一個避免服務中斷的方法是藉由 preStop hook 來延緩 Pod 收到 SIGTERM 信號的時間:

lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 10"]

概念上就是給步驟 1 有 10 秒的緩衝時間,讓「從 Service Endpoints 移除的請求」這件事有足夠的時間被完成。 當 Pod 真的與外接脫鉤的時候,才允許 Pod 的主程序進入結束的程序(收到 SIGTERM 信號),這樣就可以避免服務中斷的問題。

留言