利用 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 策略(静态策略 部分)运行。可选地,可以为系统或 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。它们的总和将构成保留内存的总量。

在内存管理器中添加了一个新的 --reserved-memory 标志,以允许节点管理员将此总保留内存进行拆分,并相应地将其保留在多个 NUMA 节点中。

该标志指定每个 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 加入到 Guaranteed QoS 类别,必须指定 CPU 和内存请求。

故障排除

以下方法可用于排查 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"]

接下来,让我们登录到部署 Pod 的节点,并检查 /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 的内存使用量被限制(通过 cgroups 配置)到这些 NUMA 节点。

这自动意味着内存管理器实例化了一个新的组,该组包含这两个 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 消息中。只能为 Guaranteed QoS 类别的 Pod 检索此信息。

下一步

上次修改于 2024 年 2 月 20 日 下午 9:48 PST:将更多功能状态简码更改为数据驱动 (7b6866063f)