利用 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
策略(静态策略 部分)运行。可选地,可以为系统或 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
。它们的总和将构成保留内存的总量。
在内存管理器中添加了一个新的 --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>
的大页面也应该存在)。
语法
--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 加入到 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"]
接下来,让我们登录到部署 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 节点,即索引为 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
消息中。只能为 Guaranteed QoS 类别的 Pod 检索此信息。