利用 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 规范中的其他请求资源对齐
- 应启用 CPU 管理器,并在节点上配置适当的 CPU 管理器策略。请参阅 控制 CPU 管理策略;
- 应启用拓扑管理器,并在节点上配置适当的拓扑管理器策略。请参阅 控制拓扑管理策略。
从 v1.22 开始,内存管理器通过 MemoryManager
功能开关 默认启用。
在 v1.22 之前,kubelet
必须使用以下标志启动
--feature-gates=MemoryManager=true
以启用内存管理器功能。
内存管理器如何运作?
内存管理器目前为保证 QoS 类别中的 Pod 提供保证的内存(和巨页)分配。要立即将内存管理器投入运行,请遵循 内存管理器配置 部分中的指南,然后按照 将 Pod 放置在保证 QoS 类别中 部分的说明准备和部署 Guaranteed
Pod。
内存管理器是一个提示提供程序,它为拓扑管理器提供拓扑提示,然后根据这些拓扑提示对请求的资源进行对齐。它还强制执行 Pod 的 cgroups
(即 cpuset.mems
)。有关 Pod 准入和部署过程的完整流程图,请参阅 内存管理器 KEP:设计概述 和以下内容
在此过程中,内存管理器会更新存储在 节点映射和内存映射 中的内部计数器,以管理保证的内存分配。
内存管理器在启动和运行时更新节点映射,如下所示。
启动
当节点管理员使用 --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 对象来保留内存。
对于 BestEffort
或 Burstable
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
是强制性的。
此外,请避免以下配置
- 重复,即相同的 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 不会启动内存管理器并显示错误。以下是一个正确配置的示例
--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'
让我们验证上面的配置
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 放置在保证 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 允许进入节点,要么拒绝它。
此外,搜索与内存管理器相关的事件,例如,找出有关 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 的内存消耗受限于这些 NUMA 节点(通过 cgroups
配置)。
这自动意味着内存管理器实例化了一个新的组,该组包含这两个 NUMA 节点,即索引为 0
和 1
的 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 检索。