在雲原生環境中,應用程式需要對平台發出的生命週期事件做出反應,才能有效地管理資源和提升可靠性。Kubernetes 提供了多種機制來管理容器的生命週期,包括訊號處理、生命週期鉤子以及初始化容器。瞭解這些機制並妥善運用,才能讓應用程式在雲原生環境中穩定執行。容器生命週期事件的處理至關重要,例如正確回應 SIGTERM 訊號以確保應用程式優雅關閉,避免資料遺失或服務中斷。除了監控容器狀態,平台還會發出命令,應用程式需要對這些命令做出反應,才能更好地與平台整合。

Managed Lifecycle:雲原生應用的生命週期管理

在雲原生環境中,容器化應用程式的生命週期管理至關重要。Managed Lifecycle 模式描述了應用程式如何對管理平台發出的生命週期事件做出反應,以實作良好的雲原生公民行為。

問題

在前一章中,我們討論了容器需要提供健康檢查 API,以便管理平台能夠持續監控應用程式的健康狀態。這是一種由平台提取應用程式資訊的機制。然而,除了監控容器的狀態外,平台有時還會發出命令,並期望應用程式對這些命令做出反應。

解決方案

僅檢查行程狀態不足以表明應用程式的健康狀態。同樣地,僅使用行程模型來啟動和停止行程也不夠。現實世界的應用程式需要更細粒度的互動和生命週期管理能力。有些應用程式需要幫助來預熱,而有些應用程式需要一個溫和且乾淨的關閉程式。

容器生命週期事件

當 Kubernetes 決定關閉一個容器時,無論是因為 Pod 被關閉還是因為失敗的活性探針導致容器重新啟動,容器都會收到一個 SIGTERM 訊號。SIGTERM 是一個溫和的提示,要求容器在 Kubernetes 傳送更嚴厲的 SIGKILL 訊號之前乾淨地關閉。

process.on('SIGTERM', () => {
  // 關閉邏輯
  server.close(() => {
    console.log('Server closed');
    process.exit(0);
  });
});

內容解密:

此程式碼段展示瞭如何在 Node.js 應用程式中監聽 SIGTERM 訊號。當收到 SIGTERM 訊號時,應用程式會關閉伺服器,並在關閉完成後離開行程。這種處理方式確保了應用程式能夠乾淨地關閉,避免了未完成的請求和資源洩漏。

SIGKILL 訊號

如果容器在收到 SIGTERM 訊號後沒有在設定的時間內(預設為 30 秒)關閉,Kubernetes 將傳送 SIGKILL 訊號強制終止容器。因此,應用程式應該盡快回應 SIGTERM 訊號,完成必要的清理工作。

最佳實踐

為了實作良好的雲原生公民行為,容器化應用程式應該:

  1. 監聽生命週期事件:如 SIGTERM 訊號,並做出適當的反應。
  2. 提供健康檢查 API:以便管理平台能夠監控應用程式的健康狀態。
  3. 整合追蹤和度量收集函式庫:如 OpenTracing 或 Prometheus,以提供更多的可觀察性。

透過遵循 Managed Lifecycle 模式,應用程式可以更好地與雲原生平台整合,提高系統的韌性和使用者經驗。

更多資訊

  • Kubernetes Best Practices: Handling Pod Lifecycle Events
  • Graceful Shutdown with Node.js and Kubernetes
  • Customizing the Termination Message

Kubernetes容器生命週期管理

Kubernetes提供了多種機制來管理容器的生命週期,包括訊號處理、生命週期鉤子(lifecycle hooks)以及初始化容器(init containers)。這些機制使得開發者能夠設計出更具彈性和可靠性的容器化應用。

訊號處理與優雅關閉

當需要終止一個Pod時,Kubernetes首先傳送一個SIGTERM訊號給容器中的主程式。如果容器程式沒有在規定的時間內關閉,Kubernetes會傳送一個SIGKILL訊號強制終止程式。這個寬限期可以透過.spec.terminationGracePeriodSeconds欄位來定義,但它不能被保證,因為它可以在向Kubernetes發出命令時被覆寫。

設計和實作容器化應用時,應當使其具備短暫性,能夠快速啟動和關閉。這樣可以提高應用的可靠性和彈性。

PostStart鉤子

除了使用程式訊號進行生命週期管理外,Kubernetes還提供了額外的生命週期鉤子,如postStartpreStop。一個包含postStart鉤子的Pod清單示例如下:

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
          - sleep 30 && echo "Wake up!" > /tmp/postStart_done

內容解密:

  1. postStart鉤子在容器建立後非同步執行,可以用於執行一些初始化或預熱邏輯。
  2. sleep 30模擬了一個耗時的操作,例如應用初始化或快取預熱。
  3. 使用觸發檔案(/tmp/postStart_done)來與主應用程式同步,確保某些條件滿足後再繼續執行。
  4. postStart是一個阻塞呼叫,容器的狀態會保持在Waiting,直到postStart處理程式完成,這會導致Pod的狀態保持在Pending

postStart鉤子可以用來延遲容器的啟動狀態,讓主容器程式有足夠的時間進行初始化。也可以用來檢查某些先決條件,如果不滿足,可以透過傳回非零離開碼來阻止容器的啟動。

PreStop鉤子

preStop鉤子在容器被終止之前執行,是一個阻塞呼叫。它與SIGTERM訊號具有相同的語義,用於在無法回應SIGTERM訊號時優雅地關閉容器。示例如下:

apiVersion: v1
kind: Pod
metadata:
  name: pre-stop-hook
spec:
  containers:
  - image: k8spatterns/random-generator:1.0
    name: random-generator
    lifecycle:
      preStop:
        httpGet:
          path: /shutdown
          port: 8080

內容解密:

  1. preStop鉤子用於在容器終止前執行一些清理工作,例如關閉連線或釋放資源。
  2. 在這個例子中,preStop鉤子透過HTTP GET請求呼叫應用內部的/shutdown端點來實作優雅關閉。
  3. 即使preStop鉤子是阻塞的,但如果它持有或傳回失敗結果,並不會阻止容器的刪除和程式的終止。

其他生命週期控制機制

除了容器級別的生命週期鉤子外,Kubernetes還提供了Pod級別的初始化機制,即初始化容器(init containers)。初始化容器按順序執行,直到完成,並且在應用容器啟動之前執行。這使得初始化容器可以用於Pod級別的初始化任務。

特性生命週期鉤子初始化容器
啟用時機容器生命週期階段Pod生命週期階段
啟動階段操作postStart命令一系列initContainers的執行
關閉階段操作preStop命令無等效功能
時序保證postStart與容器ENTRYPOINT平行執行所有init容器必須成功完成,應用容器才能啟動
使用案例執行特定容器的非關鍵啟動/關閉清理使用容器執行工作流程式順序操作;重用容器執行任務

綜上所述,Kubernetes提供了多種機制來管理容器的生命週期,使得開發者能夠設計出更具彈性和可靠性的容器化應用。無論是透過訊號處理、生命週期鉤子還是初始化容器,這些機制都能夠幫助開發者更好地控制應用的生命週期,從而提高應用的可靠性和可維護性。

容器與 Pod 的生命週期管理

在 Kubernetes 環境中,容器和 Pod 的生命週期管理是確保應用程式能夠可靠執行和優雅離開的關鍵。本章節將探討如何利用初始化容器(init containers)、生命週期鉤子(lifecycle hooks)和進入點重寫(entrypoint rewriting)等技術來控制和管理容器及 Pod 的生命週期。

初始化容器與生命週期鉤子

初始化容器是一種特殊的容器,它們在 Pod 的主容器啟動之前執行,並且可以用來執行一些初始化任務,例如設定環境、複製檔案或執行資料函式庫遷移等。生命週期鉤子則允許在容器的生命週期中的特定時刻執行自定義的程式碼,例如在容器啟動或終止時。

進入點重寫

進入點重寫是一種技術,它允許將容器預設的啟動命令替換為一個通用的監控程式。這種技術在 Kubernetes-based 的 CI/CD 平台(如 Tekton 和 Argo CD)中非常有用,因為它們需要在 Pod 中順序執行多個容器,並且需要對容器的生命週期進行精細控制。

簡單的 Pod 宣告

下面是一個簡單的 Pod 宣告範例,它啟動了一個名為 random-generator 的容器:

apiVersion: v1
kind: Pod
metadata:
  name: simple-random-generator
spec:
  containers:
  - image: k8spatterns/random-generator:1.0
    name: random-generator
    command:
    - "random-generator-runner"
    args:
    - "--seed"
    - "42"

使用監控程式的 Pod 宣告

下面是一個使用進入點重寫技術的 Pod 宣告範例,它將原來的啟動命令替換為一個監控程式:

apiVersion: v1
kind: Pod
metadata:
  name: wrapped-random-generator
spec:
  volumes:
  - name: wrapper
    emptyDir: { }
  initContainers:
  - name: copy-supervisor
    image: k8spatterns/supervisor
    volumeMounts:
    - mountPath: /var/run/wrapper
      name: wrapper
    command: [ cp ]
    args: [ supervisor, /var/run/wrapper/supervisor ]
  containers:
  - image: k8spatterns/random-generator:1.0
    name: random-generator
    volumeMounts:
    - mountPath: /var/run/wrapper
      name: wrapper
    command:
    - "/var/run/wrapper/supervisor"
    args:
    - "random-generator-runner"
    - "--seed"
    - "42"

程式碼解析

在這個範例中,我們使用了初始化容器 copy-supervisor 將監控程式 supervisor 複製到分享的 emptyDir 磁碟區中。然後,在主容器 random-generator 中,我們將啟動命令替換為監控程式 /var/run/wrapper/supervisor,並將原來的命令 random-generator-runner 作為引數傳遞給監控程式。

內容解密:

  1. 初始化容器:用於複製監控程式到分享磁碟區。
  2. 分享磁碟區:用於在初始化容器和主容器之間分享監控程式。
  3. 進入點重寫:將主容器的啟動命令替換為監控程式。
  4. 監控程式:負責監控主容器的執行,並處理生命週期事件。

自動化排程

自動化排程是 Kubernetes 排程器的核心功能,用於將新的 Pod 分配給符合容器資源請求和排程策略的節點。本章節描述了 Kubernetes 排程演算法的原理,以及如何從外部影響排程決策。

問題描述

一個規模較大的根據微服務的系統,由數十甚至數百個隔離的程式組成。容器和 Pod 為包裝和佈署提供了良好的抽象,但並未解決將這些程式放置在合適節點上的問題。隨著微服務數量不斷增長,將它們個別分配到節點上已變成一項難以管理的活動。

容器之間存在依賴關係、對節點的依賴以及資源需求,而且所有這些都會隨著時間而變化。叢集上可用的資源也會隨著時間而變化,透過縮小或擴充套件叢集,或透過已經放置的容器消耗資源。我們放置容器的方式會影響分散式系統的可用性、效能和容量。同樣,這使得將容器排程到節點上成為一個動態的目標。

解決方案

在 Kubernetes 中,將 Pod 分配給節點是由排程器完成的。它是 Kubernetes 的一部分,具有高度的可組態性,並且仍在不斷發展和改進。在本章中,我們將介紹主要的排程控制機制、影響放置的驅動力、選擇某個選項的原因以及產生的後果。Kubernetes 排程器是一個強大且節省時間的工具。它在整個 Kubernetes 平台中扮演著基礎性的角色,但與其他 Kubernetes 元件(API 伺服器、Kubelet)類別似,它可以在隔離環境中執行,也可以根本不使用。

在非常高的層面上,Kubernetes 排程器執行的主要操作是從 API 伺服器檢索每個新建立的 Pod 定義,並將其分配給一個節點。它為每個 Pod 找到最合適的節點(只要有這樣的節點),無論是初始應用程式放置、擴充套件還是將應用程式從不健康的節點遷移到更健康的節點。它透過考慮執行時的依賴關係、資源需求和引導策略來實作高用性;透過水平擴充套件 Pod;以及透過將 Pod 放置在附近以實作效能和低延遲互動。然而,為了使排程器正確地執行其工作並允許宣告式放置,它需要具有可用容量的節點和具有宣告的資源組態檔和引導策略的容器。

可用節點資源

首先,Kubernetes 叢集需要具有足夠資源容量來執行新的 Pod 的節點。每個節點都具有可用的容量來執行 Pod,排程器確保為 Pod 請求的容器資源總和小於可用的可分配節點容量。考慮到一個專門用於 Kubernetes 的節點,其容量是使用範例 6-1 中的公式計算的。

範例 6-1:節點容量
可分配 [應用程式 Pod 的容量] =
節點容量 [節點上的可用容量]
- Kube-Reserved [Kubernetes 守護程式,如 kubelet、容器執行時]
- System-Reserved [作業系統守護程式,如 sshd、udev]
- 收回閾值 [保留記憶體以防止系統 OOM]

如果您不為支援作業系統和 Kubernetes 本身的系統守護程式保留資源,則 Pod 可以被排程到節點的全部容量,這可能會導致 Pod 和系統守護程式爭奪資源,從而導致節點上的資源匱乏問題。即使這樣,節點上的記憶體壓力也會透過 OOMKilled 錯誤或導致節點暫時離線而影響在其上執行的所有 Pod。OOMKilled 是一種錯誤訊息,當 Linux 核心的 Out-of-Memory(OOM)終止程式因系統記憶體不足而終止程式時顯示。收回閾值是 Kubelet 為了在節點上保留記憶體並在可用記憶體低於保留值時嘗試收回 Pod 而採取的最後手段。

還需要注意的是,如果容器執行在不受 Kubernetes 管理的節點上,則這些容器使用的資源不會反映在 Kubernetes 的節點容量計算中。一個解決方法是執行一個佔位 Pod,它不執行任何操作,但具有與未被跟蹤的容器的資源使用量相對應的 CPU 和記憶體請求。這樣的 Pod 只被建立來表示和保留未被跟蹤的容器的資源消耗,並幫助排程器建立更好的節點資源模型。

容器資源需求

另一個高效 Pod 放置的重要要求是定義容器的執行時依賴關係和資源需求。我們在第 2 章“可預測的需求”中有更詳細的介紹。這歸結為具有宣告其資源組態檔(具有請求和限制)和環境依賴關係(如儲存或埠)的容器。只有這樣,Pod 才會被最佳地分配到節點上,並且可以在不相互影響的情況下執行,並且在峰值使用期間面臨資源匱乏。

排程器組態

拼圖的下一部分是為您的叢集需求設定正確的篩選或優先順序組態。排程器具有預設的一組組態。

程式碼範例與解析

以下是一個簡單的範例,展示如何定義一個 Pod 的資源請求和限制:

apiVersion: v1
kind: Pod
metadata:
  name: example-pod
spec:
  containers:
  - name: example-container
    image: example/image
    resources:
      requests:
        cpu: 100m
        memory: 128Mi
      limits:
        cpu: 200m
        memory: 256Mi

程式碼解密:

  1. apiVersionkind 定義了 Kubernetes 物件的版本和型別,在此例中是一個 Pod。
  2. metadata 部分包含了 Pod 的名稱等後設資料。
  3. spec 部分定義了 Pod 的詳細規格,包括其內部的容器。
  4. containers 列出了 Pod 中的容器,這裡只有一個名為 example-container 的容器。
  5. resources 部分定義了容器的資源請求和限制。
    • requests 定義了容器請求的資源量,用於排程器決定將 Pod 安排在哪個節點上。
    • limits 定義了容器被允許使用的最大資源量,用於防止容器使用過多資源,影響其他容器。

這個範例展示瞭如何為容器定義基本的資源需求,這對於 Kubernetes 的排程器來說是至關重要的,因為它需要這些資訊來決定如何最佳地放置 Pod。