使用 NUMA 感知内存管理器
Kubernetes v1.32 [稳定]
(默认启用: true)Kubernetes *内存管理器* 为 Guaranteed
QoS 类中的 Pod 提供保证的内存(和 HugePages)分配功能。
内存管理器采用提示生成协议,为 Pod 生成最合适的 NUMA 亲和性。内存管理器将这些亲和性提示提供给中央管理器(*拓扑管理器*)。根据提示和拓扑管理器策略,Pod 将被拒绝或被准入到节点上。
此外,内存管理器确保 Pod 请求的内存从最少数目的 NUMA 节点分配。
内存管理器仅适用于基于 Linux 的主机。
开始之前
你需要有一个 Kubernetes 集群,并且 kubectl 命令行工具需要配置为与你的集群通信。建议在至少有两个非控制平面主机的节点组成的集群上运行本教程。如果你还没有集群,你可以使用 minikube 创建一个,或者你可以使用以下 Kubernetes 游乐场之一
你的 Kubernetes 服务器版本必须是 v1.32 或更高。要检查版本,输入 kubectl version
。
将内存资源与 Pod Spec 中其他请求的资源对齐
- CPU 管理器应该被启用,并且在节点上配置适当的 CPU 管理器策略。参见控制 CPU 管理策略;
- 拓扑管理器应该被启用,并且在节点上配置适当的拓扑管理器策略。参见控制拓扑管理策略。
从 v1.22 开始,内存管理器默认通过 MemoryManager
特性门控启用。
在 v1.22 之前,必须使用以下标志启动 kubelet
--feature-gates=MemoryManager=true
以启用内存管理器特性。
内存管理器如何工作?
内存管理器目前为 Guaranteed QoS 类中的 Pod 提供保证的内存(和 HugePages)分配功能。要立即启用内存管理器,请遵循内存管理器配置一节中的指南,然后按照将 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]
(默认启用: false)Windows 支持可以通过 WindowsCPUAndMemoryAffinity
特性门控启用,并且需要容器运行时的支持。Windows 上仅支持 BestEffort 策略。
内存管理器配置
其他管理器应首先预配置。接下来,应启用内存管理器特性并使用 Static
策略运行(参见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]
(默认启用: false)此策略仅在 Windows 上受支持。
在 Windows 上,NUMA 节点分配的工作方式与 Linux 不同。没有机制可以确保内存访问仅来自特定的 NUMA 节点。相反,Windows 调度程序将根据 CPU 分配选择最优的 NUMA 节点。如果 Windows 调度程序认为最优,Windows 可能会使用其他 NUMA 节点。
该策略确实通过内部 NodeMap 跟踪可用和请求的内存量。内存管理器将尽力确保在进行分配之前 NUMA 节点上有足够的内存可用。
这意味着在大多数情况下,内存分配应该按预期运行。
预留内存标志
节点可分配资源 (Node Allocatable) 机制通常由节点管理员用来为 Kubelet 或操作系统进程预留 Kubernetes 节点系统资源,以增强节点稳定性。为此目的可以使用一组专用标志来设置节点预留的总内存量。然后使用此预配置值来计算节点可供 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
是必需的。
此外,避免以下配置:
- 重复项,即相同的 NUMA 节点或内存类型,但值不同;
- 对任何内存类型设置零限制;
- 机器硬件中不存在的 NUMA 节点 ID;
- 内存类型名称不同于
memory
或hugepages-<size>
(特定<size>
的 hugepages 也应该存在)。
语法
--reserved-memory N:memory-type1=value1,memory-type2=value2,...
N
(整数) - NUMA 节点索引,例如0
memory-type
(字符串) - 表示内存类型memory
- 常规内存hugepages-2Mi
或hugepages-1Gi
- HugePages
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(默认) = 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 要进入 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 到节点还是拒绝它。
此外,搜索日志中与内存管理器相关的出现,例如查找关于 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
标志在 NUMA 节点 0
上预留了 3221225472
字节(即 3Gi
)用于 Kubelet 和系统进程。
设备插件资源 API
Kubelet 提供 PodResourceLister
gRPC 服务,以便发现资源和相关的元数据。通过使用其List gRPC 端点,可以检索每个容器预留的内存信息,这些信息包含在 protobuf ContainerMemory
消息中。此信息仅适用于 Guaranteed QoS 类中的 Pod。