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

作者:Dixita Narang (Google)

译者:Wilson Wu (DaoCloud)

Kubernetes v1.27 于 2023 年 4 月发布,引入了对内存 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 上的容器运行时通常如何在 cgroup 中配置内存请求和限制:

  • 内存请求

    内存请求主要由 kube-scheduler 在(Kubernetes)Pod 调度时使用。 在 cgroups v1 中,没有任何控件来设置 cgroup 必须始终保留的最小内存量。 因此,容器运行时不使用 Pod 规约中设置的内存请求值。

    cgroups v2 中引入了一个 memory.min 设置,用于设置给定 cgroup 中的进程确定可用的最小内存量。 如果 cgroup 的内存使用量在其有效最小边界内,则该 cgroup 的内存在任何情况下都不会被回收。 如果内核无法为 cgroup 中的进程维护至少 memory.min 字节的内存,内核将调用其 OOM 杀手机制。 换句话说,内核保证至少有这么多内存可用,或者终止进程(可能在 cgroup 之外)以腾出更多内存。 MemoryQoS 机制将 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。 MemoryQoS 机制将 memory.max 映射到 spec.containers[].resources.limits.memory 以设置内存用量的硬性限制。如果内存消耗超过此水平,内核将调用其 OOM 杀手机制。

    cgroups v2 中还添加了 memory.high 配置。MemoryQoS 机制使用 memory.high 来设置内存用量抑制上限。 如果超出了 memory.high 限制,则违规的 cgroup 会受到抑制,并且内核会尝试回收内存,这可能会避免 OOM 终止。

如何工作

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

MemoryQoS 机制使用 cgroups v2 的内存控制器来保证 Kubernetes 中的内存资源。 此特性使用的 cgroupv2 接口有:

  • memory.max
  • memory.min
  • memory.high
内存 QoS 级别

内存 QoS 级别

memory.max 映射到 Pod 规约中指定的 limits.memory。 kubelet 和容器运行时在对应的 cgroup 中配置限制值。内核强制执行限制机制以防止容器用量超过所配置的资源限制。 如果容器中的进程尝试消耗的资源超过所设置的限制值,内核将终止进程并报告内存不足(OOM)错误。

memory.max 映射到 limit.memory

memory.max 映射到 limit.memory

memory.min 被映射到 requests.memory,这会导致内存资源被预留而永远不会被内核回收。 这就是 MemoryQoS 机制确保 Kubernetes Pod 内存可用性的方式。 如果没有不受保护的、可回收的内存,则内核会调用 OOM 杀手以提供更多可用内存。

memory.min 映射到 requests.memory

memory.min 映射到 requests.memory

对于内存保护,除了原来的限制内存用量的方式之外,MemoryQoS 机制还会对用量接近其内存限制的工作负载进行抑制, 确保系统不会因内存使用的零星增加而不堪重负。当你启用 MemoryQoS 特性时, KubeletConfiguration 中将提供一个新字段 memoryThrottlingFactor。默认设置为 0.9。 memory.high 被映射到通过 memoryThrottlingFactorrequests.memorylimits.memory 计算得出的抑制上限,计算方法如下式所示,所得的值向下舍入到最接近的页面大小:

memory.high 公式

memory.high 公式

注意:如果容器没有指定内存限制,则 limits.memory 将被替换为节点可分配内存的值。

总结:

文件描述
memory.maxmemory.max 指定允许容器使用的最大内存限制。 如果容器内的进程尝试使用的内存量超过所配置的限制值,内核将终止该进程并显示内存不足(OOM)错误。

此配置映射到 Pod 清单中指定的容器内存限制。
memory.minmemory.min 指定 cgroup 必须始终保留的最小内存量, 即系统永远不应回收的内存。如果没有可用的未受保护的可回收内存,则会调用 OOM 终止程序。

此配置映射到 Pod 清单中指定的容器的内存请求。
memory.highmemory.high 指定内存用量抑制上限。这是控制 cgroup 内存用量的主要机制。 如果 cgroups 内存使用量超过此处指定的上限,则 cgroups 进程将受到抑制并标记回收压力较大。

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

注意memory.high 仅可在容器级别的 cgroups 上设置, 而 memory.min 则可在容器、Pod 和节点级别的 cgroups 上设置。

针对 cgroup 层次结构的 memory.min 计算

当发出容器内存请求时,kubelet 在创建容器期间通过 CRI 中的 Unified 字段将 memory.min 传递给后端 CRI 运行时(例如 containerd 或 CRI-O)。容器级别 cgroup 中的 memory.min 将设置为:

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

对于 Pod 中每个 ith 容器

由于 memory.min 接口要求祖先 cgroups 目录全部被设置, 因此需要正确设置 Pod 和节点的 cgroups 目录。

Pod 级别 cgroup 中的 memory.min

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

对于 Pod 中每个 ith 容器

节点级别 cgroup 中的 memory.min

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

对于节点中每个 ith Pod 中的每个 jth 容器

Kubelet 将直接使用 libcontainer 库(来自 runc 项目)管理 Pod 级别和节点级别 cgroups 的层次结构,而容器 cgroups 限制由容器运行时管理。

支持 Pod QoS 类别

根据用户对 Kubernetes v1.22 中 Alpha 特性的反馈,一些用户希望在 Pod 层面选择不启用 MemoryQoS, 以确保不会出现早期内存抑制现象。因此,在 Kubernetes v1.27 中 MemoryQoS 还支持根据 服务质量(QoS)对 Pod 类设置 memory.high。以下是按 QoS 类设置 memory.high 的几种情况:

  1. Guaranteed Pods:根据其 QoS 定义,要求 Pod 的内存请求等于其内存限制,并且不允许超配。 因此,通过不设置 memory.high,MemoryQoS 特性会针对这些 Pod 被禁用。 这样做可以确保 Guaranteed Pod 充分利用其内存请求,也就是其内存限制,并且不会被抑制。
  1. Burstable Pod:根据其 QoS 定义,要求 Pod 中至少有一个容器具有 CPU 或内存请求或限制设置。

    • 当 requests.memory 和 limits.memory 都被设置时,公式按原样使用:
     
         
    当请求和限制被设置时的 memory.high

    当请求和限制被设置时的 memory.high

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

    当请求和限制未被设置时的 memory.high

  1. BestEffort Pod:根据其 QoS 定义,不需要设置内存或 CPU 限制或请求。对于这种情况, kubernetes 设置 requests.memory = 0 并将公式中的 limits.memory 替换为节点可分配内存:

    BestEffort Pod 的 memory.high

    BestEffort Pod 的 memory.high

总结:只有 Burstable 和 BestEffort QoS 类别中的 Pod 才会设置 memory.high。 Guaranteed QoS 的 Pod 不会设置 memory.high,因为它们的内存是有保证的。

我该如何使用它?

在 Linux 节点上启用 MemoryQoS 特性的先决条件是:

  1. 验证是否满足 Kubernetes 对 cgroup v2 支持的相关要求

  2. 确保 CRI 运行时支持内存 QoS。在撰写本文时, 只有 Containerd 和 CRI-O 提供与内存 QoS(alpha)兼容的支持。是在以下 PR 中实现的:

MemoryQoS 在 Kubernetes v1.27 中仍然是 Alpha 特性。 你可以通过在 kubelet 配置文件中设置 MemoryQoS=true 来启用该特性:

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

我如何参与?

非常感谢所有帮助设计、实施和审查此功能的贡献者:

对于那些有兴趣参与未来内存 QoS 特性讨论的人,你可以通过多种方式联系 SIG Node: