本文發表於一年多前。舊文章可能包含過時內容。請檢查頁面中的資訊自發布以來是否已變得不正確。

Kubernetes 1.27:記憶體資源的服務質量(Alpha)

2023 年 4 月釋出的 Kubernetes v1.27 引入了對記憶體 QoS(alpha)的更改,以提高 Linux 節點中的記憶體管理能力。

對記憶體 QoS 的支援最初是在 Kubernetes v1.22 中新增的,後來發現了一些關於計算 memory.high 公式的限制。這些限制在 Kubernetes v1.27 中得到了解決。

背景

Kubernetes 允許你在 Pod 規約中可選地指定容器需要每種資源的量。最常指定的資源是 CPU 和記憶體。

例如,一個定義了容器資源需求的 Pod 清單可能看起來像這樣

apiVersion: v1
kind: Pod
metadata:
  name: example
spec:
  containers:
  - name: nginx
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "64Mi"
        cpu: "500m"
  • spec.containers[].resources.requests

    當你在 Pod 中為容器指定資源請求時,Kubernetes 排程器會使用此資訊來決定將 Pod 放置在哪個節點上。排程器確保對於每種資源型別,已排程容器的資源請求總和小於節點上的總可分配資源。

  • spec.containers[].resources.limits

    當你在 Pod 中為容器指定資源限制時,kubelet 會強制執行這些限制,以便執行中的容器不允許使用超過你設定的限制的資源。

當 kubelet 啟動一個作為 Pod 一部分的容器時,kubelet 會將容器的 CPU 和記憶體請求與限制傳遞給容器執行時。容器執行時會為容器分配 CPU 請求和 CPU 限制。只要系統有空閒的 CPU 時間,容器就能保證被分配到它們所請求的 CPU 量。容器不能使用超過配置限制的 CPU,也就是說,如果容器在給定的時間片內使用超過指定的 CPU 限制,其 CPU 使用將被限制。

在記憶體 QoS 特性出現之前,容器執行時只使用記憶體限制,而忽略了記憶體 request(請求過去和現在仍然用於影響排程)。如果容器使用的記憶體超過配置的限制,Linux 的記憶體不足(OOM)殺手將被呼叫。

讓我們比較一下,在有和沒有記憶體 QoS 特性的情況下,Linux 上的容器執行時通常如何在 cgroups 中配置記憶體請求和限制

  • 記憶體請求

    記憶體請求主要由 kube-scheduler 在(Kubernetes)Pod 排程期間使用。在 cgroups v1 中,沒有控制元件可以指定 cgroups 必須始終保留的最小記憶體量。因此,容器執行時沒有使用 Pod 規約中設定的請求記憶體值。

    cgroups v2 引入了一個 memory.min 設定,用於指定應為給定 cgroup 內的程序保留的最小記憶體量。如果一個 cgroup 的記憶體使用在其有效的 min 邊界內,該 cgroup 的記憶體將不會在任何條件下被回收。如果核心無法為 cgroup 內的程序維持至少 memory.min 位元組的記憶體,核心將呼叫其 OOM 殺手。換句話說,核心保證至少有這麼多記憶體可用,或者終止程序(可能在 cgroup 之外)以使更多記憶體可用。記憶體 QoS 將 memory.min 對映到 spec.containers[].resources.requests.memory,以確保 Kubernetes Pod 中容器的記憶體可用性。

  • 記憶體限制

    memory.limit 指定了記憶體限制,如果容器嘗試分配超過此限制的記憶體,Linux 核心將以 OOM(記憶體不足)的方式終止一個程序。如果被終止的程序是容器內的主要(或唯一)程序,容器可能會退出。

    在 cgroups v1 中,memory.limit_in_bytes 介面用於設定記憶體使用限制。然而,與 CPU 不同,它無法應用記憶體限制:一旦容器超過記憶體限制,它就會被 OOM 殺死。

    在 cgroups v2 中,memory.max 類似於 cgroupv1 中的 memory.limit_in_bytes。記憶體 QoS 將 memory.max 對映到 spec.containers[].resources.limits.memory,以指定記憶體使用的硬性限制。如果記憶體消耗超過此級別,核心將呼叫其 OOM 殺手。

    cgroups v2 還添加了 memory.high 配置。記憶體 QoS 使用 memory.high 來設定記憶體使用限制閾值。如果突破了 memory.high 限制,違規的 cgroup 將被限制,並且核心會嘗試回收記憶體,這可能會避免 OOM 殺死。

工作原理

Cgroups v2 記憶體控制器介面與 Kubernetes 容器資源對映

記憶體 QoS 使用 cgroups v2 的記憶體控制器來保證 Kubernetes 中的記憶體資源。此特性使用的 cgroupv2 介面有

  • memory.max
  • memory.min
  • memory.high.
Memory QoS Levels

記憶體 QoS 級別

memory.max 對映到 Pod 規約中指定的 limits.memory。kubelet 和容器執行時在相應的 cgroup 中配置該限制。核心強制執行該限制,以防止容器使用超過配置的資源限制。如果容器中的程序試圖消耗超過指定限制的記憶體,核心會以記憶體不足(OOM)錯誤終止一個或多個程序。

memory.max maps to limits.memory

memory.max 對映到 limits.memory

memory.min 對映到 requests.memory,這會導致記憶體資源的預留,這些資源永遠不應該被核心回收。這就是記憶體 QoS 如何確保 Kubernetes pod 的記憶體可用性。如果沒有未受保護的可回收記憶體可用,OOM 殺手會被呼叫以釋放更多記憶體。

memory.min maps to requests.memory

memory.min 對映到 requests.memory

對於記憶體保護,除了限制記憶體使用的原始方式外,記憶體 QoS 還會限制接近其記憶體限制的工作負載,確保系統不會因記憶體使用的零星增加而不堪重負。當您啟用 MemoryQoS 功能時,KubeletConfiguration 中會提供一個新欄位 memoryThrottlingFactor。它預設設定為 0.9。memory.high 對映到使用 memoryThrottlingFactorrequests.memorylimits.memory 按以下公式計算的限制值,並將該值向下取整到最近的頁面大小

memory.high formula

memory.high 公式

總結

檔案描述
memory.maxmemory.max 指定了容器允許使用的最大記憶體限制。如果容器內的程序試圖消耗超過配置限制的記憶體,核心會以記憶體不足(OOM)錯誤終止該程序。

它對映到 Pod 清單中指定的容器記憶體限制。
memory.minmemory.min 指定 cgroups 必須始終保留的最小記憶體量,即系統永遠不應回收的記憶體。如果沒有未受保護的可回收記憶體可用,將呼叫 OOM kill。

它對映到 Pod 清單中指定的容器記憶體請求。
memory.highmemory.high 指定記憶體使用限制的閾值。這是控制 cgroup 記憶體使用的主要機制。如果 cgroup 的記憶體使用超過此處指定的高邊界,cgroup 的程序將被限制並承受巨大的回收壓力。

Kubernetes 使用一個公式來計算 memory.high,具體取決於容器的記憶體請求、記憶體限制或節點可分配記憶體(如果容器的記憶體限制為空)以及一個限制因子。有關該公式的更多詳細資訊,請參閱 KEP

memory.min 在 cgroups 層次結構中的計算

當容器記憶體請求被提出時,kubelet 會在容器建立期間透過 CRI 中的 Unified 欄位將 memory.min 傳遞給後端 CRI 執行時(如 containerd 或 CRI-O)。對於 Pod 中的每個第 ith 個容器,容器級 cgroups 中的 memory.min 將被設定為

memory.min =  pod.spec.containers[i].resources.requests[memory]

由於 memory.min 介面要求所有祖先 cgroups 目錄都已設定,因此 pod 和節點 cgroups 目錄需要正確設定。

對於 pod 中的每個第 ith 個容器,pod 級 cgroup 中的 memory.min

memory.min = \sum_{i=0}^{no. of pods}pod.spec.containers[i].resources.requests[memory]

對於節點上每個第 ith 個 pod 中的每個第 jth 個容器,節點級 cgroup 中的 memory.min

memory.min = \sum_{i}^{no. of nodes}\sum_{j}^{no. of pods}pod[i].spec.containers[j].resources.requests[memory]

Kubelet 將直接使用 libcontainer 庫(來自 runc 專案)來管理 Pod 級別和節點級別 cgroups 的 cgroups 層次結構,而容器 cgroups 的限制則由容器執行時管理。

對 Pod QoS 類的支援

根據使用者對 Kubernetes v1.22 中 Alpha 功能的反饋,一些使用者希望在每個 Pod 的基礎上選擇退出 MemoryQoS,以確保不會出現過早的記憶體限制。因此,在 Kubernetes v1.27 中,Memory QOS 也支援根據 Pod 類的服務質量(QoS)來設定 memory.high。以下是根據 QOS 類別的不同 memory.high 的情況

  1. Guaranteed pods 根據其 QoS 定義,要求記憶體請求=記憶體限制,並且不會被超額分配。因此,透過不設定 memory.high,在這些 pod 上停用了 MemoryQoS 功能。這確保了 Guaranteed pod 可以充分使用其記憶體請求,直至其設定的限制,而不會遇到任何限制。

  2. Burstable pods 根據其 QoS 定義,要求 Pod 中至少有一個容器設定了 CPU 或記憶體的請求或限制。

    • 當設定了 requests.memory 和 limits.memory 時,公式按原樣使用

      memory.high when requests and limits are set

      當設定了請求和限制時的 memory.high

    • 當設定了 requests.memory 而未設定 limits.memory 時,公式中的 limits.memory 將被替換為節點可分配記憶體

      memory.high when requests and limits are not set

      當未設定請求和限制時的 memory.high

  3. BestEffort 根據其 QoS 定義,不需要任何記憶體或 CPU 的限制或請求。對於這種情況,kubernetes 將 requests.memory 設定為 0,並將公式中的 limits.memory 替換為節點可分配記憶體

    memory.high for BestEffort Pod

    BestEffort Pod 的 memory.high

總結:只有 Burstable 和 BestEffort QoS 類的 Pod 會設定 memory.high。Guaranteed QoS Pod 不會設定 memory.high,因為它們的記憶體是有保障的。

我該如何使用它?

在你的 Linux 節點上啟用記憶體 QoS 功能的先決條件是

  1. 驗證與 Kubernetes 對 cgroups v2 的支援相關的要求是否得到滿足。
  2. 確保 CRI 執行時支援記憶體 QoS。在撰寫本文時,只有 containerd 和 CRI-O 提供了與記憶體 QoS (alpha) 相容的支援。這是在以下 PR 中實現的

在 Kubernetes v1.27 中,記憶體 QoS 仍然是一個 alpha 功能。你可以透過在 kubelet 配置檔案中設定 MemoryQoS=true 來啟用該功能

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
featureGates:
  MemoryQoS: true

我如何參與?

非常感謝所有為該功能的設計、實施和審查做出貢獻的貢獻者

對於有興趣參與未來關於記憶體 QoS 功能討論的人,你可以透過多種方式聯絡 SIG Node