由雲原生平台管理的容器化應用,無法掌控自己的生命週期。要成為稱職的雲原生公民,它們必須監聽管理平台發出的事件,並據此調整自身的生命週期。「受管生命週期」模式描述應用能夠、也應該如何回應這些生命週期事件。

問題#

「健康探針」一章說明了容器為何必須提供各種健康檢查的 API。那些健康檢查 API 是唯讀端點,平台持續探測它們以取得應用的洞察——那是平台從應用抽取資訊的機制。

但除了監控容器狀態之外,平台有時會發出命令並期待應用做出反應。受政策與外部因素驅動,雲原生平台可能在任何時刻決定啟動或停止它所管理的應用。至於哪些事件值得回應、以及該如何回應,則由容器化應用自己決定。

說到底,這是一組平台用來與應用溝通、下達命令的 API。應用可以選擇從生命週期管理中獲益,也可以在不需要這項服務時直接忽略它。

解法#

我們看過:光檢查行程狀態不足以說明應用是否健康,所以才需要各種監控容器健康的 API。同理,光用行程模型來啟動與停止行程也不夠好——真實世界的應用需要更細緻的互動與生命週期管理能力:有些應用需要協助暖機,有些需要溫和而乾淨的關閉程序。

圖 5-1:受管的容器生命週期

應用的部署單元是 Pod,而 Pod 由一個或多個容器組成。在 Pod 層級還有 init container(見「初始化容器」)等建構,以及撰寫本書時仍在提案階段的 defer-container,可協助管理容器生命週期。但本章描述的事件與掛鉤,全都套用在個別容器層級,而非 Pod 層級。

SIGTERM 訊號#

每當 Kubernetes 決定關閉一個容器——無論是因為它所屬的 Pod 正在關閉,或只是存活探針失敗導致容器要被重啟——該容器都會收到 SIGTERM 訊號。

SIGTERM 是在 Kubernetes 送出更粗暴的 SIGKILL 之前,溫和地戳一下容器,請它乾淨地關閉。收到 SIGTERM 之後,應用應盡快關閉:對某些應用來說可能是瞬間終止;另一些應用則可能需要完成處理中的請求、釋放開啟的連線、清理暫存檔,因而稍微花點時間。

無論哪種情況,回應 SIGTERM 都是乾淨關閉容器的正確時機

SIGKILL 訊號#

若容器行程在收到 SIGTERM 後仍未關閉,接下來的 SIGKILL 會強制關閉它。Kubernetes 不會立刻送出 SIGKILL,而是在發出 SIGTERM 後預設等待 30 秒的寬限期

這個寬限期可用 .spec.terminationGracePeriodSeconds 逐 Pod 定義,但不保證一定被遵守,因為對 Kubernetes 下指令時可以覆寫它。

目標應該放在:把容器化應用設計、實作成**短暫(ephemeral)**的,具備快速的啟動與關閉流程。

postStart 掛鉤#

只靠行程訊號管理生命週期有其侷限,因此 Kubernetes 另外提供了 postStartpreStop 這兩個生命週期掛鉤。

apiVersion: v1
kind: Pod
metadata:
  name: post-start-hook
spec:
  containers:
    - image: k8spatterns/random-generator:1.0
      name: random-generator
      lifecycle:
        postStart:
          exec:
            command:
              - sh
              - -c
              # 這裡等 30 秒。sleep 只是在模擬任何可能耗時的啟動程式碼;
              # 同時用一個觸發檔案,與平行啟動的主應用同步。
              - sleep 30 && echo "Wake up!" > /tmp/postStart_done

postStart 指令在容器建立之後執行,且與主容器行程非同步進行。雖然許多應用初始化與暖機邏輯都可以寫在容器啟動步驟裡,postStart 仍涵蓋了一些使用場景:

  • 延後容器的啟動狀態。 postStart 動作是阻斷式呼叫:在處理器完成之前,容器狀態維持 Waiting,Pod 狀態也因而維持 Pending。可以利用這個特性拖延容器的啟動狀態,好讓主容器行程有時間初始化。
  • 在 Pod 未滿足前置條件時阻止容器啟動。 例如當 postStart 掛鉤回傳非零結束碼表示錯誤時,主容器行程會被 Kubernetes 殺掉。

postStartpreStop 的呼叫機制類似健康探針,支援兩種處理器型別:

  • exec:直接在容器內執行指令。
  • httpGet:對某個 Pod 容器開啟的埠發出 HTTP GET 請求。

postStart 掛鉤中執行關鍵邏輯務必非常謹慎,因為它的執行沒有任何保證:

  • 掛鉤與容器行程平行執行,因此有可能在容器啟動之前就被執行
  • 掛鉤採「至少一次」(at-least-once)語意,實作必須自行處理重複執行。
  • 對於未抵達處理器的失敗 HTTP 請求,平台不會重試

preStop 掛鉤#

preStop 是容器被終止前送出的阻斷式呼叫,語意與 SIGTERM 相同。當「回應 SIGTERM」不可行時,就該用它來啟動容器的優雅關閉。preStop 動作必須先完成,刪除容器的呼叫才會送到容器執行期、進而觸發 SIGTERM 通知。

apiVersion: v1
kind: Pod
metadata:
  name: pre-stop-hook
spec:
  containers:
    - image: k8spatterns/random-generator:1.0
      name: random-generator
      lifecycle:
        preStop:
          httpGet: # 呼叫應用內部的 /shutdown 端點
            port: 8080
            path: /shutdown

其他生命週期控制手段#

本章目前聚焦於「容器生命週期事件發生時執行指令」的掛鉤。但還有另一個機制運作在 Pod 層級而非容器層級,用來執行初始化指令:init container

與一般應用容器不同,init container 依序執行、執行到完成為止,且在 Pod 內任何應用容器啟動之前執行完畢。這些保證讓它適合承擔 Pod 層級的初始化工作。

面向生命週期掛鉤Init Container
觸發於容器生命週期階段Pod 生命週期階段
啟動階段動作一道 postStart 指令一串待執行的 initContainers
關閉階段動作一道 preStop 指令目前尚無對等功能
時序保證postStart 指令與容器的 ENTRYPOINT 同時執行所有 init container 必須全部成功完成,應用容器才能啟動
使用場景執行容器特定、非關鍵的啟動/關閉清理用容器執行工作流式的循序操作;重用容器來執行任務

除非你需要特定的時序保證,否則選用哪個機制並沒有嚴格規則。以下是三種取徑,依序需要更多投入,但也提供更強的保證與更好的重用性

  1. 完全跳過生命週期掛鉤與 init container,改用 bash 腳本在容器啟動/關閉指令中執行特定動作。可行,但會讓容器與腳本緊密耦合,變成維護惡夢。
  2. 使用 Kubernetes 生命週期掛鉤執行動作。
  3. 更進一步,用 init container 執行個別動作。

理解容器與 Pod 生命週期的各個階段與可用掛鉤,是打造「能真正受益於 Kubernetes 管理」的應用的關鍵。

討論#

雲原生平台的主要好處之一,是能在可能不可靠的雲端基礎設施之上,可靠且可預期地執行與擴縮應用。這些平台為其上執行的應用提供了一組約束與契約,而應用遵守這些契約才符合自身利益——這樣才能享受平台提供的所有能力。

處理並回應這些事件,能確保你的應用優雅地啟動與關閉,把對下游服務的衝擊降到最低。就目前最基本的形式而言,這意味著容器應該表現得像任何設計良好的 POSIX 行程

未來可能會有更多事件,提示應用「即將被擴大」或「請釋放資源以免被關閉」。關鍵是要建立這樣的心態:應用的生命週期不再由人掌控,而是由平台完全自動化。

除了管理應用生命週期,Kubernetes 這類編排平台的另一項重責大任,是把容器分派到一整批節點上。下一個模式「自動化配置」會說明如何從外部影響排程決策。

更多資訊#

  • Managed Lifecycle Example
  • Container Lifecycle Hooks
  • Attaching Handlers to Container Lifecycle Events
  • Terminating with Grace
  • Graceful Shutdown of Pods with Kubernetes
  • Defer Containers