本文发表于一年多前。旧文章可能包含过时内容。请检查页面中的信息自发布以来是否已变得不正确。

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