利用 NUMA 感知的内存管理器
Kubernetes v1.32 [稳定]
(默认启用: 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 提供保证内存(和巨页)分配。要立即让内存管理器投入运行,请遵循 内存管理器配置部分中的指南,然后准备并部署一个 Guaranteed
Pod,如 将 Pod 放置在 Guaranteed QoS 类中 部分所示。
内存管理器是一个提示提供程序,它为拓扑管理器提供拓扑提示,然后拓扑管理器根据这些拓扑提示对齐请求的资源。在 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)可以通过 WindowsCPUAndMemoryAffinity
特性门控启用 Windows 支持,并且它需要在容器运行时中提供支持。Windows 上仅支持 尽力而为策略。
内存管理器配置
应首先预配置其他管理器。接下来,应启用内存管理器特性,并使用 Static
策略运行(静态策略部分)。(可选)可以为系统或 kubelet 进程保留一些内存,以提高节点稳定性(保留内存标志部分)。
策略
内存管理器支持两种策略。你可以通过 kubelet
标志 --memory-manager-policy
选择策略。
None
(默认)Static
(仅限 Linux)BestEffort
(仅限 Windows)
None 策略
这是默认策略,不会以任何方式影响内存分配。它的作用与根本不存在内存管理器相同。
None
策略返回默认拓扑提示。此特殊提示表示提示提供程序(在本例中为内存管理器)对任何资源的 NUMA 亲和性没有偏好。
静态策略
对于 Guaranteed
Pod,Static
内存管理器策略返回与可以保证内存的 NUMA 节点集相关的拓扑提示,并通过更新内部 NodeMap 对象来保留内存。
对于 BestEffort
或 Burstable
Pod,由于没有对保证内存的请求,因此 Static
内存管理器策略会发送回默认拓扑提示,并且不会在内部 NodeMap 对象中保留内存。
此策略仅在 Linux 上受支持。
尽力而为策略
Kubernetes v1.32 [alpha]
(默认启用: false)此策略仅在 Windows 上受支持。
在 Windows 上,NUMA 节点分配的工作方式与 Linux 不同。没有机制来确保内存访问仅来自特定的 NUMA 节点。相反,Windows 调度程序将根据 CPU 分配选择最优的 NUMA 节点。如果 Windows 调度程序认为最优,则 Windows 可能会使用其他 NUMA 节点。
该策略会跟踪通过内部 NodeMap 可用和请求的内存量。内存管理器将尽最大努力确保在进行分配之前 NUMA 节点上有足够的可用内存。
这意味着在大多数情况下,内存分配应该按预期工作。
保留内存标志
节点管理员通常使用 节点可分配资源 机制来为 kubelet 或操作系统进程预留 K8S 节点系统资源,以增强节点稳定性。可以使用一组专用标志来设置节点预留的总内存量。这个预先配置的值随后被用来计算可供 Pod 使用的节点“可分配”内存的实际数量。
Kubernetes 调度器使用“可分配”资源来优化 Pod 调度过程。上述标志包括 --kube-reserved
、--system-reserved
和 --eviction-threshold
。它们的总和将构成预留内存的总量。
Memory Manager 添加了一个新的 --reserved-memory
标志,允许(节点管理员)分割这个总预留内存,并将其在多个 NUMA 节点之间进行相应的预留。
该标志指定一个以逗号分隔的列表,列出每个 NUMA 节点的不同内存类型的预留量。可以使用分号作为分隔符来指定跨多个 NUMA 节点的内存预留。此参数仅在 Memory Manager 功能的上下文中有效。Memory Manager 不会将此预留内存用于容器工作负载的分配。
例如,如果您的 NUMA 节点 “NUMA0” 有 10Gi
可用内存,并且指定了 --reserved-memory
在 “NUMA0” 上预留 1Gi
内存,那么 Memory Manager 会认为只有 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 节点的索引。
如果您不遵循上述公式,Memory Manager 将在启动时显示错误。
换句话说,上面的示例说明对于常规内存(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 将不会启动 Memory Manager 并显示错误。以下是一个正确的配置示例
--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
,Memory Manager 会识别出属于 Guaranteed
QoS 类的 Pod。Memory Manager 为每个 Guaranteed
Pod 向 Topology Manager 提供特定的拓扑提示。对于 QoS 类不是 Guaranteed
的 Pod,Memory Manager 向 Topology Manager 提供默认的拓扑提示。
以下 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 状态 - 指示拓扑亲和性错误
- 系统日志 - 包含用于调试的宝贵信息,例如关于生成的提示
- 状态文件 - Memory Manager 内部状态的转储(包括 节点映射和内存映射)
- 从 v1.22 开始,可以使用 设备插件资源 API 来检索有关为容器预留的内存的信息
Pod 状态 (TopologyAffinityError)
此错误通常在以下情况下发生
- 节点没有足够的可用资源来满足 Pod 的请求
- Pod 的请求由于特定的 Topology Manager 策略约束而被拒绝
该错误出现在 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 相关的系统日志。
在日志中可以找到 Memory Manager 为 Pod 生成的提示集。此外,日志中还应该存在 CPU Manager 生成的提示集。
Topology Manager 会合并这些提示以计算出单个最佳提示。最佳提示也应该出现在日志中。
最佳提示指示在何处分配所有资源。Topology Manager 会根据其当前策略测试此提示,并根据结果,决定是允许 Pod 进入节点还是拒绝它。
此外,搜索与 Memory Manager 相关的日志记录,例如,查找有关 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"]
接下来,让我们登录到部署该 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 节点。
这自动意味着 Memory Manager 实例化了一个包含这两个 NUMA 节点的新组,即索引为 0
和 1
的 NUMA 节点。
请注意,组的管理以相对复杂的方式处理,并且在 Memory Manager KEP 的 此处 和 此处 部分提供了进一步的阐述。
为了分析组中可用的内存资源,必须将属于该组的 NUMA 节点中的相应条目相加。
例如,可以通过将组中每个 NUMA 节点上可用的空闲内存相加来计算组中空闲“常规”内存的总量,即,在 NUMA 节点 0
的 "memory"
部分("free":0
)和 NUMA 节点 1
的 "free":103739236352
)。因此,该组中空闲“常规”内存的总量等于 0 + 103739236352
字节。
行 "systemReserved":3221225472
表示此节点的管理员通过使用 --reserved-memory
标志,预留了 3221225472
字节(即 3Gi
)来为 NUMA 节点 0
上的 kubelet 和系统进程提供服务。
设备插件资源 API
kubelet 提供 PodResourceLister
gRPC 服务,以实现资源和相关元数据的发现。通过使用其 List gRPC 端点,可以检索有关每个容器预留内存的信息,该信息包含在 protobuf ContainerMemory
消息中。此信息只能针对 Guaranteed QoS 类中的 Pod 检索。