利用 NUMA 感知内存管理器
Kubernetes v1.32 [stable]
(默认启用:true)Kubernetes 内存管理器 实现了在 Guaranteed
QoS 类 中为 Pod 保证内存(和巨页)分配的功能。
内存管理器采用提示生成协议来为 Pod 生成最合适的 NUMA 亲和性。内存管理器将这些亲和性提示提供给中央管理器(拓扑管理器)。根据提示和拓扑管理器策略,Pod 被拒绝或被接纳到节点。
此外,内存管理器确保 Pod 请求的内存是从最少数量的 NUMA 节点分配的。
内存管理器仅适用于基于 Linux 的主机。
准备工作
你需要拥有一个 Kubernetes 集群,并且 kubectl 命令行工具已配置为与你的集群通信。建议在至少有两个不是控制平面主机的节点的集群上运行本教程。如果你还没有集群,你可以使用 minikube 创建一个,或者你可以使用这些 Kubernetes 游乐场之一
你的 Kubernetes 服务器必须是 v1.32 或更高版本。要检查版本,请输入 kubectl version
。
为了使内存资源与 Pod 规范中请求的其他资源对齐
- 应在节点上启用 CPU 管理器并配置适当的 CPU 管理器策略。请参阅 控制 CPU 管理策略;
- 应在节点上启用拓扑管理器并配置适当的拓扑管理器策略。请参阅 控制拓扑管理策略。
从 v1.22 开始,内存管理器通过 MemoryManager
特性门控 默认启用。
在 v1.22 之前,必须使用以下标志启动 kubelet
--feature-gates=MemoryManager=true
以启用内存管理器功能。
内存管理器如何运作?
内存管理器目前为 Guaranteed QoS 类中的 Pod 提供有保证的内存(和巨页)分配。要立即启用内存管理器,请遵循 内存管理器配置 部分中的指南,然后,按照 将 Pod 放入 Guaranteed QoS 类 部分的说明准备和部署一个 Guaranteed
Pod。
内存管理器是一个提示提供者,它为拓扑管理器提供拓扑提示,然后拓扑管理器根据这些拓扑提示对齐请求的资源。在 Linux 上,它还为 Pod 强制执行 cgroups
(即 cpuset.mems
)。Pod 准入和部署过程的完整流程图在 内存管理器 KEP:设计概述 和下方展示
在此过程中,内存管理器会更新存储在 节点映射和内存映射 中的内部计数器,以管理有保证的内存分配。
内存管理器在启动和运行时按如下方式更新节点映射。
启动
这发生在节点管理员使用 --reserved-memory
(保留内存标志 部分)时。在这种情况下,节点映射会更新以反映此保留,如 内存管理器 KEP:启动时的内存映射(带示例) 所示。
配置 Static
策略时,管理员必须提供 --reserved-memory
标志。
运行时
参考 内存管理器 KEP:运行时的内存映射(带示例) 说明了成功的 Pod 部署如何影响节点映射,它还涉及 Kubernetes 或操作系统如何进一步处理潜在的内存不足 (OOM) 情况。
内存管理器操作中一个重要主题是 NUMA 组的管理。每当 Pod 的内存请求超过单个 NUMA 节点容量时,内存管理器会尝试创建一个包含多个 NUMA 节点并具有扩展内存容量的组。这个问题已在 内存管理器 KEP:如何启用跨多个 NUMA 节点的有保证内存分配? 中详细阐述。此外,参考 内存管理器 KEP:模拟 - 内存管理器如何工作?(带示例) 说明了组的管理如何进行。
Windows 支持
Kubernetes v1.32 [alpha]
(默认禁用)Windows 支持可以通过 WindowsCPUAndMemoryAffinity
特性门控启用,并且需要容器运行时的支持。Windows 上仅支持 BestEffort 策略。
内存管理器配置
其他管理器应首先预配置。接下来,应启用内存管理器功能并使用 Static
策略运行(静态策略 部分)。可选地,可以为系统或 kubelet 进程保留一定量的内存,以提高节点稳定性(保留内存标志 部分)。
策略
内存管理器支持两种策略。你可以通过 kubelet
标志 --memory-manager-policy
选择策略
None
(默认)Static
(仅限 Linux)BestEffort
(仅限 Windows)
None 策略
这是默认策略,不以任何方式影响内存分配。它的作用就像内存管理器根本不存在一样。
None
策略返回默认拓扑提示。此特殊提示表示提示提供者(在本例中为内存管理器)对任何资源的 NUMA 亲和性没有偏好。
Static 策略
对于 Guaranteed
Pod,Static
内存管理器策略返回与可以保证内存的 NUMA 节点集相关的拓扑提示,并通过更新内部 NodeMap 对象来保留内存。
对于 BestEffort
或 Burstable
Pod,Static
内存管理器策略发送回默认拓扑提示,因为没有请求保证内存,并且不在内部 NodeMap 对象中保留内存。
此策略仅在 Linux 上受支持。
BestEffort 策略
Kubernetes v1.32 [alpha]
(默认禁用)此策略仅在 Windows 上受支持。
在 Windows 上,NUMA 节点分配的工作方式与 Linux 不同。没有机制可以确保内存访问仅来自特定的 NUMA 节点。相反,Windows 调度程序将根据 CPU 分配选择最优化 NUMA 节点。如果 Windows 调度程序认为最佳,Windows 可能会使用其他 NUMA 节点。
该策略通过内部 NodeMap 跟踪可用和请求的内存量。内存管理器将在分配之前尽力确保 NUMA 节点上有足够的内存可用。
这意味着在大多数情况下,内存分配应按预期运行。
保留内存标志
节点可分配 机制通常由节点管理员用于为 kubelet 或操作系统进程保留 K8S 节点系统资源,以增强节点稳定性。为此目的可以使用一组专用标志来设置节点保留内存的总量。此预配置值随后用于计算可供 Pod 使用的节点“可分配”内存的实际量。
Kubernetes 调度程序包含“可分配”以优化 Pod 调度过程。上述标志包括 --kube-reserved
、--system-reserved
和 --eviction-threshold
。它们的总和将构成保留内存的总量。
内存管理器中添加了一个新的 --reserved-memory
标志,以允许此总保留内存由节点管理员拆分并在多个 NUMA 节点上相应保留。
该标志指定了每 NUMA 节点不同内存类型的逗号分隔内存保留列表。跨多个 NUMA 节点的内存保留可以使用分号作为分隔符指定。此参数仅在内存管理器功能的上下文中有效。内存管理器不会将此保留内存用于容器工作负载的分配。
例如,如果你有一个可用内存为 10Gi
的 NUMA 节点“NUMA0”,并且指定了 --reserved-memory
以在“NUMA0”保留 1Gi
内存,则内存管理器假定只有 9Gi
可用于容器。
你可以省略此参数,但是,你应该注意所有 NUMA 节点的保留内存量应等于 节点可分配功能 指定的内存量。如果至少一个节点可分配参数不为零,则你需要为至少一个 NUMA 节点指定 --reserved-memory
。实际上,eviction-hard
阈值默认为 100Mi
,因此如果使用 Static
策略,则 --reserved-memory
是强制性的。
此外,避免以下配置
- 重复项,即相同的 NUMA 节点或内存类型,但值不同;
- 将任何内存类型的限制设置为零;
- 机器硬件中不存在的 NUMA 节点 ID;
- 内存类型名称不同于
memory
或hugepages-<size>
(特定<size>
的巨页也应该存在)。
语法
--reserved-memory N:memory-type1=value1,memory-type2=value2,...
N
(整数)- NUMA 节点索引,例如0
memory-type
(字符串)- 表示内存类型memory
- 常规内存hugepages-2Mi
或hugepages-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
注意
默认的硬驱逐阈值为 100MiB,而**不是**零。请记住通过设置--reserved-memory
将你保留的内存量增加该硬驱逐阈值。否则,kubelet 将不会启动内存管理器并显示错误。这是一个正确配置的示例
--kube-reserved=cpu=4,memory=4Gi
--system-reserved=cpu=1,memory=1Gi
--memory-manager-policy=Static
--reserved-memory '0:memory=3Gi;1:memory=2148Mi'
在 Kubernetes 1.32 之前,你还需要添加
--feature-gates=MemoryManager=true
让我们验证上面的配置
kube-reserved + system-reserved + eviction-hard(default) = reserved-memory(0) + reserved-memory(1)
4GiB + 1GiB + 100MiB = 3GiB + 2148MiB
5120MiB + 100MiB = 3072MiB + 2148MiB
5220MiB = 5220MiB
(正确)
将 Pod 放入 Guaranteed 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 和内存请求才能将其置于 Guaranteed 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 接纳到节点还是拒绝它。
此外,搜索日志中与内存管理器相关的事件,例如,查找有关 cgroups
和 cpuset.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 的内存消耗受限于(通过 cgroups
配置)这些 NUMA 节点。
这自动意味着内存管理器实例化了一个新组,该组包含这两个 NUMA 节点,即索引为 0
和 1
的 NUMA 节点。
请注意,组的管理以相对复杂的方式处理,更多详细说明请参见内存管理器 KEP 的 此 和 此 部分。
为了分析组中可用的内存资源,必须将属于该组的 NUMA 节点中的相应条目相加。
例如,组中可用“常规”内存的总量可以通过将组中每个 NUMA 节点(即 NUMA 节点 0
的 "memory"
部分("free":0
)和 NUMA 节点 1
的 "memory"
部分("free":103739236352
))的可用空闲内存相加来计算。因此,该组中可用“常规”内存的总量等于 0 + 103739236352
字节。
行 "systemReserved":3221225472
表示此节点的管理员已使用 --reserved-memory
标志保留了 3221225472
字节(即 3Gi
)以在 NUMA 节点 0
上为 kubelet 和系统进程提供服务。
设备插件资源 API
kubelet 提供了一个 PodResourceLister
gRPC 服务,以实现资源和关联元数据的发现。通过使用其 List gRPC 端点,可以检索每个容器保留内存的信息,该信息包含在 protobuf ContainerMemory
消息中。此信息只能针对 Guaranteed QoS 类中的 Pod 检索。