利用 NUMA 感知内存管理器

功能状态: Kubernetes v1.22 [beta]

Kubernetes 内存管理器 允许在 Guaranteed QoS 类别 中为 Pod 提供保证的内存(和巨页)分配功能。

内存管理器采用提示生成协议,以产生最适合 Pod 的 NUMA 亲和性。内存管理器将这些亲和性提示提供给中央管理器(拓扑管理器)。根据提示和拓扑管理器策略,Pod 将被拒绝或允许进入节点。

此外,内存管理器确保为 Pod 请求的内存从最少数量的 NUMA 节点分配。

内存管理器仅与基于 Linux 的主机相关。

开始之前

您需要拥有一个 Kubernetes 集群,并且 kubectl 命令行工具必须配置为与您的集群通信。建议在至少有两个节点(不充当控制平面主机)的集群上运行本教程。如果您还没有集群,可以使用 minikube 创建一个,或者可以使用以下 Kubernetes 游乐场之一

您的 Kubernetes 服务器必须为 v1.21 或更高版本。要检查版本,请输入 kubectl version

要将内存资源与 Pod 规范中的其他请求资源对齐

从 v1.22 开始,内存管理器通过 MemoryManager 功能开关 默认启用。

在 v1.22 之前,kubelet 必须使用以下标志启动

--feature-gates=MemoryManager=true

以启用内存管理器功能。

内存管理器如何运作?

内存管理器目前为保证 QoS 类别中的 Pod 提供保证的内存(和巨页)分配。要立即将内存管理器投入运行,请遵循 内存管理器配置 部分中的指南,然后按照 将 Pod 放置在保证 QoS 类别中 部分的说明准备和部署 Guaranteed Pod。

内存管理器是一个提示提供程序,它为拓扑管理器提供拓扑提示,然后根据这些拓扑提示对请求的资源进行对齐。它还强制执行 Pod 的 cgroups(即 cpuset.mems)。有关 Pod 准入和部署过程的完整流程图,请参阅 内存管理器 KEP:设计概述 和以下内容

Memory Manager in the pod admission and deployment process

在此过程中,内存管理器会更新存储在 节点映射和内存映射 中的内部计数器,以管理保证的内存分配。

内存管理器在启动和运行时更新节点映射,如下所示。

启动

当节点管理员使用 --reserved-memory保留内存标志 部分)时,就会发生这种情况。在这种情况下,节点映射将更新以反映此保留,如 内存管理器 KEP:启动时的内存映射(带示例) 中所示。

当配置 Static 策略时,管理员必须提供 --reserved-memory 标志。

运行时

内存管理器 KEP:运行时的内存映射(带示例) 说明了成功部署 Pod 如何影响节点映射,以及它与 Kubernetes 或操作系统如何进一步处理潜在的内存不足 (OOM) 情况有关。

内存管理器操作中的重要主题是 NUMA 组的管理。每次 Pod 的内存请求超过单个 NUMA 节点的容量时,内存管理器都会尝试创建一个包含多个 NUMA 节点并具有扩展内存容量的组。该问题已解决,如 内存管理器 KEP:如何在多个 NUMA 节点上启用保证的内存分配? 中所述。此外,请参阅 内存管理器 KEP:模拟 - 内存管理器如何工作?(带示例),了解组管理是如何发生的。

内存管理器配置

应首先预先配置其他管理器。接下来,应启用内存管理器功能,并使用 Static 策略(Static 策略 部分)运行。可选地,可以为系统或 kubelet 进程保留一定量的内存,以提高节点稳定性(保留内存标志 部分)。

策略

内存管理器支持两种策略。您可以通过 kubelet 标志 --memory-manager-policy 选择策略

  • None(默认)
  • Static

None 策略

这是默认策略,不会以任何方式影响内存分配。它与内存管理器根本不存在时的行为相同。

None 策略返回默认拓扑提示。此特殊提示表示提示提供程序(在本例中为内存管理器)对任何资源都没有 NUMA 亲和性偏好。

Static 策略

对于 Guaranteed Pod,Static 内存管理器策略返回与可以保证内存的 NUMA 节点集相关的拓扑提示,并通过更新内部 NodeMap 对象来保留内存。

对于 BestEffortBurstable Pod,Static 内存管理器策略会发送回默认拓扑提示,因为没有保证内存的请求,并且不会在内部 NodeMap 对象中保留内存。

保留内存标志

节点可分配 机制通常由节点管理员用来为 kubelet 或操作系统进程保留 K8S 节点系统资源,以提高节点稳定性。可以使用一组专用标志来设置节点的总保留内存量。此预先配置的值随后用于计算节点的“可分配”内存的实际数量,该内存可供 Pod 使用。

Kubernetes 调度程序将“可分配”纳入其中,以优化 Pod 调度过程。上述标志包括 --kube-reserved--system-reserved--eviction-threshold。它们的总和将占总保留内存量。

为允许节点管理员将此总保留内存拆分(并相应地保留在多个 NUMA 节点上),向内存管理器添加了一个新的 --reserved-memory 标志。

该标志指定每个 NUMA 节点的不同内存类型的内存保留量的逗号分隔列表。可以使用分号作为分隔符来指定跨多个 NUMA 节点的内存保留。此参数仅在内存管理器功能的上下文中才有用。内存管理器不会使用此保留内存来分配容器工作负载。

例如,如果您有一个 NUMA 节点“NUMA0”,其可用内存为 10Gi,并且 --reserved-memory 指定为在“NUMA0”上保留 1Gi 内存,则内存管理器假定只有 9Gi 可用于容器。

您可以省略此参数,但是,您应该注意,所有 NUMA 节点的保留内存量应等于 节点可分配功能 指定的内存量。如果至少有一个节点可分配参数非零,则需要为至少一个 NUMA 节点指定 --reserved-memory。实际上,eviction-hard 阈值默认情况下等于 100Mi,因此,如果使用 Static 策略,则 --reserved-memory 是强制性的。

此外,请避免以下配置

  1. 重复,即相同的 NUMA 节点或内存类型,但具有不同的值;
  2. 为任何内存类型设置零限制;
  3. 机器硬件中不存在的 NUMA 节点 ID;
  4. memoryhugepages-<size> 不同的内存类型名称(特定 <size> 的巨页也应该存在)。

语法

--reserved-memory N:memory-type1=value1,memory-type2=value2,...

  • N(整数) - NUMA 节点索引,例如 0
  • memory-type(字符串) - 表示内存类型
    • memory - 常规内存
    • hugepages-2Mihugepages-1Gi - 巨页
  • value (字符串) - 保留内存的数量,例如 1Gi

示例用法

--reserved-memory 0:memory=1Gi,hugepages-1Gi=2Gi

--reserved-memory 0:memory=1Gi --reserved-memory 1:memory=2Gi

--reserved-memory '0:memory=1Gi;1:memory=2Gi'

当您为 --reserved-memory 标志指定值时,您必须遵守您之前通过节点可分配功能标志提供的设置。也就是说,以下规则必须适用于每种内存类型

sum(reserved-memory(i)) = kube-reserved + system-reserved + eviction-threshold,

其中 i 是 NUMA 节点的索引。

如果您不遵循上述公式,内存管理器将在启动时显示错误。

换句话说,上面的示例说明,对于传统内存(type=memory),我们总共保留了 3Gi,即

sum(reserved-memory(i)) = reserved-memory(0) + reserved-memory(1) = 1Gi + 2Gi = 3Gi

与节点可分配配置相关的 kubelet 命令行参数示例

  • --kube-reserved=cpu=500m,memory=50Mi
  • --system-reserved=cpu=123m,memory=333Mi
  • --eviction-hard=memory.available<500Mi

以下是一个正确配置的示例

--feature-gates=MemoryManager=true
--kube-reserved=cpu=4,memory=4Gi
--system-reserved=cpu=1,memory=1Gi
--memory-manager-policy=Static
--reserved-memory '0:memory=3Gi;1:memory=2148Mi'

让我们验证上面的配置

  1. kube-reserved + system-reserved + eviction-hard(default) = reserved-memory(0) + reserved-memory(1)
  2. 4GiB + 1GiB + 100MiB = 3GiB + 2148MiB
  3. 5120MiB + 100MiB = 3072MiB + 2148MiB
  4. 5220MiB = 5220MiB(这是正确的)

将 Pod 放置在保证 QoS 类别中

如果选择的策略不是 None,则内存管理器会识别处于 Guaranteed QoS 类别的 Pod。内存管理器为每个 Guaranteed Pod 向拓扑管理器提供特定的拓扑提示。对于不在 Guaranteed QoS 类别的 Pod,内存管理器会向拓扑管理器提供默认的拓扑提示。

以下摘自 Pod 清单,将 Pod 分配到 Guaranteed QoS 类别。

requests 等于 limits 时,具有整数 CPU 的 Pod 在 Guaranteed QoS 类别中运行

spec:
  containers:
  - name: nginx
    image: nginx
    resources:
      limits:
        memory: "200Mi"
        cpu: "2"
        example.com/device: "1"
      requests:
        memory: "200Mi"
        cpu: "2"
        example.com/device: "1"

此外,当 requests 等于 limits 时,共享 CPU 的 Pod 也在 Guaranteed QoS 类别中运行。

spec:
  containers:
  - name: nginx
    image: nginx
    resources:
      limits:
        memory: "200Mi"
        cpu: "300m"
        example.com/device: "1"
      requests:
        memory: "200Mi"
        cpu: "300m"
        example.com/device: "1"

请注意,必须为 Pod 指定 CPU 和内存请求,才能将其分配到保证 QoS 类别。

故障排除

以下方法可用于排查 Pod 无法部署或在节点上被拒绝的原因

  • Pod 状态 - 指示拓扑亲和性错误
  • 系统日志 - 包含用于调试的有价值信息,例如有关生成的提示的信息
  • 状态文件 - 内存管理器内部状态的转储(包括 节点映射和内存映射
  • 从 v1.22 开始,可以使用 设备插件资源 API 来检索有关为容器保留的内存的信息

Pod 状态(TopologyAffinityError)

此错误通常发生在以下情况下

  • 节点没有足够的可用资源来满足 Pod 的请求
  • Pod 的请求由于特定的拓扑管理器策略约束而被拒绝

错误出现在 Pod 的状态中

kubectl get pods
NAME         READY   STATUS                  RESTARTS   AGE
guaranteed   0/1     TopologyAffinityError   0          113s

使用 kubectl describe pod <id>kubectl get events 获取详细的错误消息

Warning  TopologyAffinityError  10m   kubelet, dell8  Resources cannot be allocated with Topology locality

系统日志

搜索与特定 Pod 相关的系统日志。

可以在日志中找到内存管理器为 Pod 生成的提示集。此外,CPU 管理器生成的提示集也应该存在于日志中。

拓扑管理器合并这些提示以计算单个最佳提示。最佳提示也应该存在于日志中。

最佳提示指示在何处分配所有资源。拓扑管理器根据其当前策略测试此提示,并根据结果,它要么将 Pod 允许进入节点,要么拒绝它。

此外,搜索与内存管理器相关的事件,例如,找出有关 cgroupscpuset.mems 更新的信息。

检查节点上的内存管理器状态

首先,让我们部署一个示例 Guaranteed Pod,其规范如下

apiVersion: v1
kind: Pod
metadata:
  name: guaranteed
spec:
  containers:
  - name: guaranteed
    image: consumer
    imagePullPolicy: Never
    resources:
      limits:
        cpu: "2"
        memory: 150Gi
      requests:
        cpu: "2"
        memory: 150Gi
    command: ["sleep","infinity"]

接下来,让我们登录到部署它的节点,并检查 /var/lib/kubelet/memory_manager_state 中的状态文件

{
   "policyName":"Static",
   "machineState":{
      "0":{
         "numberOfAssignments":1,
         "memoryMap":{
            "hugepages-1Gi":{
               "total":0,
               "systemReserved":0,
               "allocatable":0,
               "reserved":0,
               "free":0
            },
            "memory":{
               "total":134987354112,
               "systemReserved":3221225472,
               "allocatable":131766128640,
               "reserved":131766128640,
               "free":0
            }
         },
         "nodes":[
            0,
            1
         ]
      },
      "1":{
         "numberOfAssignments":1,
         "memoryMap":{
            "hugepages-1Gi":{
               "total":0,
               "systemReserved":0,
               "allocatable":0,
               "reserved":0,
               "free":0
            },
            "memory":{
               "total":135286722560,
               "systemReserved":2252341248,
               "allocatable":133034381312,
               "reserved":29295144960,
               "free":103739236352
            }
         },
         "nodes":[
            0,
            1
         ]
      }
   },
   "entries":{
      "fa9bdd38-6df9-4cf9-aa67-8c4814da37a8":{
         "guaranteed":[
            {
               "numaAffinity":[
                  0,
                  1
               ],
               "type":"memory",
               "size":161061273600
            }
         ]
      }
   },
   "checksum":4142013182
}

可以从状态文件中推断出,Pod 被固定到两个 NUMA 节点,即

"numaAffinity":[
   0,
   1
],

固定术语意味着 Pod 的内存消耗受限于这些 NUMA 节点(通过 cgroups 配置)。

这自动意味着内存管理器实例化了一个新的组,该组包含这两个 NUMA 节点,即索引为 01 的 NUMA 节点。

请注意,组的管理以相对复杂的方式处理,在内存管理器 KEP 中的 部分提供了进一步的说明。

为了分析组中可用的内存资源,必须将属于该组的 NUMA 节点的相应条目加起来。

例如,组中可用空闲“传统”内存的总量可以通过将组中每个 NUMA 节点上可用的空闲内存加起来来计算,即 NUMA 节点 0"free":0)和 NUMA 节点 1"free":103739236352)的 "memory" 部分。因此,该组中空闲“传统”内存的总量等于 0 + 103739236352 字节。

"systemReserved":3221225472 表示节点管理员使用 --reserved-memory 标志在 NUMA 节点 0 上保留了 3221225472 字节(即 3Gi)来为 kubelet 和系统进程提供服务。

设备插件资源 API

kubelet 提供了一个 PodResourceLister gRPC 服务,以启用资源和相关元数据的发现。通过使用其 List gRPC 端点,可以检索有关每个容器的保留内存的信息,该信息包含在 protobuf ContainerMemory 消息中。此信息只能为保证 QoS 类别的 Pod 检索。

下一步

上次修改时间:2024 年 2 月 20 日,美国太平洋时间上午 9:48:将更多功能状态简码改为数据驱动 (7b6866063f)