本文发布已超过一年。较旧的文章可能包含过时内容。请确认页面中的信息自发布以来没有变得不正确。

Kubernetes 1.27:内存资源的服务质量 (Alpha)

Kubernetes v1.27 于 2023 年 4 月发布,引入了对 Memory QoS (alpha) 的更改,以改进 Linux 节点中的内存管理能力。

Memory QoS 的支持最初是在 Kubernetes v1.22 中添加的,后来发现计算 memory.high 公式存在一些限制。这些限制在 Kubernetes v1.27 中得到解决。

背景

Kubernetes 允许你在 Pod Spec 中选择性地指定容器所需每种资源的数量。最常指定的资源是 CPU 和内存。

例如,定义容器资源要求的 Pod Manifest 可能看起来像

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 使用将被限制。

在 Memory QoS 特性之前,容器运行时仅使用内存限制并忽略内存 request (请求过去以及现在仍然用于影响调度)。如果容器使用的内存超过配置的限制,Linux Out Of Memory (OOM) killer 将被调用。

让我们比较一下 Linux 上的容器运行时在 cgroups 中通常如何配置内存请求和限制,包括启用和未启用 Memory QoS 特性的情况。

  • 内存请求

    内存请求主要由 kube-scheduler 在 (Kubernetes) Pod 调度期间使用。在 cgroups v1 中,没有控制项指定 cgroups 必须始终保留的最小内存量。因此,容器运行时不使用在 Pod Spec 中设置的请求内存值。

    cgroups v2 引入了 memory.min 设置,用于指定在给定 cgroup 中应始终可用的最小内存量。如果 cgroup 的内存使用在其有效最小边界内,该 cgroup 的内存不会在任何条件下被回收。如果内核无法为 cgroup 中的进程维护至少 memory.min 字节的内存,内核会调用其 OOM killer。换句话说,内核保证至少有这么多的内存可用,否则会终止进程 (可能在 cgroup 之外) 以腾出更多内存。Memory QoS 将 memory.min 映射到 spec.containers[].resources.requests.memory,以确保 Kubernetes Pod 中容器的内存可用性。

  • 内存限制

    memory.limit 指定了内存限制,如果容器尝试分配超过此限制的内存,Linux 内核将终止进程,触发 OOM (内存不足) kill。如果被终止的进程是容器内部的主进程 (或唯一进程),容器可能会退出。

    在 cgroups v1 中,使用 memory.limit_in_bytes 接口设置内存使用限制。然而,与 CPU 不同,不可能应用内存限流:容器一旦超出内存限制,就会被 OOM kill。

    在 cgroups v2 中,memory.max 类似于 cgroup v1 中的 memory.limit_in_bytes。Memory QoS 将 memory.max 映射到 spec.containers[].resources.limits.memory,用于指定内存使用的硬限制。如果内存消耗超过此级别,内核会调用其 OOM Killer。

    cgroups v2 还添加了 memory.high 配置。Memory QoS 使用 memory.high 来设置内存使用限流阈值。如果超出 memory.high 限制,违规的 cgroups 将被限流,内核会尝试回收内存,这可能避免 OOM kill。

工作原理

Cgroups v2 内存控制器接口与 Kubernetes 容器资源映射

Memory QoS 利用 cgroups v2 的内存控制器来保障 Kubernetes 中的内存资源。此特性使用的 cgroup v2 接口包括

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

Memory QoS 设置项

memory.max 映射到 Pod spec 中指定的 limits.memory。kubelet 和容器运行时在相应的 cgroup 中配置此限制。内核强制执行此限制,防止容器使用超过配置的资源限制。如果容器中的进程尝试消耗超过指定限制的内存,内核会以 Out of Memory (OOM) 错误终止一个或多个进程。

memory.max maps to limits.memory

memory.max 映射到 limits.memory

memory.min 映射到 requests.memory,这会保留内存资源,这些资源不应被内核回收。Memory QoS 就是这样确保 Kubernetes Pod 内存可用性的。如果没有未受保护的可回收内存,就会调用 OOM killer 来腾出更多内存。

memory.min maps to requests.memory

memory.min 映射到 requests.memory

为了保护内存,除了原始限制内存使用的方式外,Memory QoS 会对接近其内存限制的工作负载进行限流,确保系统不会因内存使用的零星增加而过载。启用 MemoryQoS 特性时,KubeletConfiguration 中新增了一个字段 memoryThrottlingFactor,默认值为 0.9。memory.high 映射到使用 memoryThrottlingFactorrequests.memorylimits.memory 按下方公式计算出的限流阈值,并向下取整到最近的页面大小:

memory.high formula

memory.high 计算公式

总结

文件描述
memory.maxmemory.max 指定容器允许使用的最大内存限制。如果容器内的进程尝试消耗超过配置限制的内存,内核会以 Out of Memory (OOM) 错误终止该进程。

它映射到 Pod Manifest 中指定的容器内存限制。
memory.minmemory.min 指定 cgroups 必须始终保留的最小内存量,即永远不应被系统回收的内存。如果没有未受保护的可回收内存,就会调用 OOM kill。

它映射到 Pod Manifest 中指定的容器内存请求。
memory.highmemory.high 指定内存使用限流阈值。这是控制 cgroup 内存使用的主要机制。如果 cgroup 的内存使用超出此处指定的高位边界,cgroup 中的进程将被限流,并承受巨大的回收压力。

Kubernetes 使用一个公式计算 memory.high,该公式取决于容器的内存请求、内存限制或节点可分配内存 (如果容器的内存限制为空) 以及一个限流因子。有关该公式的更多详细信息,请参阅KEP

cgroups 层级结构的 memory.min 计算

发出容器内存请求时,kubelet 在容器创建期间通过 CRI 中的 Unified 字段将 memory.min 传递给后端 CRI 运行时 (如 containerd 或 CRI-O)。对于 Pod 中的每个第 i 个容器,容器级别 cgroups 中的 memory.min 将被设置为

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

由于 memory.min 接口要求所有祖先 cgroups 目录都已设置,因此需要正确设置 Pod 和节点 cgroups 目录。

对于 Pod 中的每个第 i 个容器,Pod 级别 cgroup 中的 memory.min

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

对于节点上每个第 i 个 Pod 中的每个第 j 个容器,节点级别 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 限制由容器运行时管理。

支持 Pod QoS 等级

基于 Kubernetes v1.22 Alpha 特性的用户反馈,一些用户希望按 Pod 退出 MemoryQoS,以确保不会过早发生内存限流。因此,在 Kubernetes v1.27 中,Memory QoS 也支持根据 Pod 等级的服务质量 (QoS) 设置 memory.high。以下是根据 QoS 等级设置 memory.high 的不同情况:

  1. Guaranteed (保证型) Pod 根据其 QoS 定义要求内存请求等于内存限制且不过度承诺。因此,通过不设置 memory.high,在这些 Pod 上禁用了 MemoryQoS 特性。这确保了 Guaranteed Pod 可以充分使用其内存请求直到设定的限制,并且不会遇到任何限流。

  2. Burstable (突发型) Pod 根据其 QoS 定义要求 Pod 中至少有一个容器设置了 CPU 或内存请求或限制。

    • 当设置了 requests.memory 和 limits.memory 时,公式按原样使用:

      memory.high when requests and limits are set

      设置 requests 和 limits 时的 memory.high

    • 当设置了 requests.memory 但未设置 limits.memory 时,公式中的 limits.memory 将被替换为节点可分配内存:

      memory.high when requests and limits are not set

      未设置 requests 和 limits 时的 memory.high

  3. BestEffort (尽力而为型) Pod 根据其 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 节点上启用 Memory QoS 特性的前提条件是:

  1. 验证与 Kubernetes 对 cgroups v2 的支持相关的要求是否满足。
  2. 确保 CRI Runtime 支持 Memory QoS。截至撰写本文时,只有 containerd 和 CRI-O 提供了兼容 Memory QoS(alpha)的支持。这在以下 PR 中实现

对于 Kubernetes v1.27,Memory QoS 仍然是一个 alpha 功能。你可以通过在 kubelet 配置文件中设置 MemoryQoS=true 来启用此功能

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

我如何参与?

非常感谢所有为该功能的设计、实现和评审做出贡献的人

对于那些有兴趣参与未来 Memory QoS 功能讨论的人,你可以通过以下几种方式联系 SIG Node