本文已发布一年以上。较旧的文章可能包含过时内容。请检查页面中的信息自发布以来是否已不正确。
Kubernetes 拓扑管理器升级到 Beta 版 - 对齐!
这篇博客文章介绍了 TopologyManager
,它是 Kubernetes 1.18 版本中的一个 Beta 功能。TopologyManager
功能支持 CPU 和外围设备(如 SR-IOV VF 和 GPU)的 NUMA 对齐,让你的工作负载能够在针对低延迟优化的环境中运行。
在引入 TopologyManager
之前,CPU 和 Device Manager 是彼此独立地做出资源分配决策的。这可能导致在多插槽系统上出现不理想的分配,从而降低对延迟敏感型应用的性能。通过引入 TopologyManager
,我们现在有了一种方法来避免这种情况。
这篇博客文章将涵盖:
- NUMA 的简要介绍及其重要性
- 可供最终用户使用的用于确保 CPU 和设备 NUMA 对齐的策略
TopologyManager
工作原理的内部细节TopologyManager
的当前限制TopologyManager
的未来方向
那么,什么是 NUMA?为什么我应该关心它?
术语 NUMA 代表 Non-Uniform Memory Access(非统一内存访问)。它是一种在多 CPU 系统上可用的技术,允许不同的 CPU 以不同的速度访问不同的内存部分。任何直接连接到 CPU 的内存都被认为是该 CPU 的“本地”内存,可以非常快速地访问。任何未直接连接到 CPU 的内存都被认为是“非本地”内存,其访问时间会根据需要经过多少互连而有所不同。在现代系统中,“本地”与“非本地”内存的概念也可以扩展到 NIC 或 GPU 等外围设备。为了获得高性能,分配 CPU 和设备时应确保它们能够访问相同的本地内存。
NUMA 系统上的所有内存被分割成一组“NUMA 节点”,每个节点代表一组 CPU 或设备的本地内存。当一个 CPU 的本地内存与某个 NUMA 节点相关联时,我们说这个 CPU 属于这个 NUMA 节点。
我们根据到达外围设备所需经过的最少互连数来确定该设备属于哪个 NUMA 节点。
例如,在图 1 中,CPU 0-3 被认为是 NUMA 节点 0 的一部分,而 CPU 4-7 是 NUMA 节点 1 的一部分。同样,GPU 0 和 NIC 0 被认为是 NUMA 节点 0 的一部分,因为它们连接到 Socket 0,其 CPU 都属于 NUMA 节点 0。Socket 1 上的 GPU 1 和 NIC 1 也同样属于 NUMA 节点 1。
图 1: 具有 2 个 NUMA 节点、2 个 Socket(每个 Socket 有 4 个 CPU)、2 个 GPU 和 2 个 NIC 的示例系统。Socket 0 上的 CPU、GPU 0 和 NIC 0 都属于 NUMA 节点 0。Socket 1 上的 CPU、GPU 1 和 NIC 1 都属于 NUMA 节点 1。
尽管上面的例子显示了 NUMA 节点与 Socket 的一对一映射,但这在一般情况下并非必然如此。单个 NUMA 节点上可能有多个 Socket,或者单个 Socket 的各个 CPU 可能连接到不同的 NUMA 节点。此外,新兴技术如 Sub-NUMA Clustering (在最新的 Intel CPU 上可用) 允许单个 CPU 与多个 NUMA 节点相关联,只要它们对两个节点的内存访问时间相同 (或差异可忽略不计)。
TopologyManager
已构建为能够处理所有这些场景。
对齐!TopologyManager 协同努力!
如前所述,TopologyManager
允许用户按 NUMA 节点对齐其 CPU 和外围设备的分配。有几种策略可供选择:
none:
此策略不会尝试进行任何资源对齐。它的行为将与TopologyManager
完全不存在时相同。这是默认策略。best-effort:
使用此策略,TopologyManager
将尽最大努力在 NUMA 节点上尝试对齐分配,但即使部分分配的资源未对齐到同一个 NUMA 节点,也总是允许 Pod 启动。restricted:
此策略与best-effort
策略相同,不同之处在于如果分配的资源无法正确对齐,它将拒绝 Pod 准入。与single-numa-node
策略不同,如果完全不可能在单个 NUMA 节点上满足分配请求(例如,请求 2 个设备,而系统上仅有的 2 个设备在不同的 NUMA 节点上),部分分配可能来自多个 NUMA 节点。single-numa-node:
此策略是最严格的,仅当 *所有* 请求的 CPU 和设备都可以 *恰好* 从一个 NUMA 节点分配时,才允许 Pod 被准入。
需要注意的是,所选策略是应用于 Pod Spec 中每个容器的,而不是跨所有容器一起进行资源对齐。
此外,通过一个全局的 kubelet
标志,单个策略应用于节点上的 *所有* Pod,而不是允许用户按 Pod (或按容器) 选择不同的策略。我们希望将来放宽这一限制。
设置这些策略之一的 kubelet
标志如下所示:
--topology-manager-policy=
[none | best-effort | restricted | single-numa-node]
此外,TopologyManager
受功能门 (feature gate) 保护。此功能门自 Kubernetes 1.16 起可用,但自 1.18 起才默认启用。
功能门可以如下所示启用或禁用(更多详细信息请参阅此处):
--feature-gates="...,TopologyManager=<true|false>"
为了根据所选策略触发对齐,用户必须根据一组特定要求在其 Pod Spec 中请求 CPU 和外围设备。
对于外围设备,这意味着从设备插件提供的可用资源中请求设备(例如,intel.com/sriov
、nvidia.com/gpu
等)。这仅在设备插件已扩展以正确集成 TopologyManager
时才有效。目前,已知具有此扩展的插件只有 Nvidia GPU 设备插件和 Intel SRIOV 网络设备插件。关于如何扩展设备插件以集成 TopologyManager
的详细信息可以在此处找到。
对于 CPU,这要求 CPUManager
已配置并启用了其 --static
策略,并且 Pod 运行在 Guaranteed QoS 类中(即所有 CPU 和内存的 limits
都等于其相应的 CPU 和内存 requests
)。CPU 也必须以整数值请求(例如,1
、2
、1000m
等)。关于如何设置 CPUManager
策略的详细信息可以在此处找到。
例如,假设 CPUManager
正在运行并启用了其 --static
策略,并且 gpu-vendor.com
和 nic-vendor.com
的设备插件已扩展以正确集成 TopologyManager
,那么下面的 Pod Spec 足以触发 TopologyManager
运行其所选策略。
spec:
containers:
- name: numa-aligned-container
image: alpine
resources:
limits:
cpu: 2
memory: 200Mi
gpu-vendor.com/gpu: 1
nic-vendor.com/nic: 1
参考前面章节的图 1,这将产生以下之一的对齐分配:
{cpu: {0, 1}, gpu: 0, nic: 0}
{cpu: {0, 2}, gpu: 0, nic: 0}
{cpu: {0, 3}, gpu: 0, nic: 0}
{cpu: {1, 2}, gpu: 0, nic: 0}
{cpu: {1, 3}, gpu: 0, nic: 0}
{cpu: {2, 3}, gpu: 0, nic: 0}
{cpu: {4, 5}, gpu: 1, nic: 1}
{cpu: {4, 6}, gpu: 1, nic: 1}
{cpu: {4, 7}, gpu: 1, nic: 1}
{cpu: {5, 6}, gpu: 1, nic: 1}
{cpu: {5, 7}, gpu: 1, nic: 1}
{cpu: {6, 7}, gpu: 1, nic: 1}
就这样!只需遵循此模式,即可让 TopologyManager
确保请求拓扑感知设备和独占 CPU 的容器之间的 NUMA 对齐。
注意:如果 Pod 被某个 TopologyManager
策略拒绝,它将被置于 Terminated
状态,并出现 Pod 准入错误,原因为 "TopologyAffinityError
"。一旦 Pod 处于此状态,Kubernetes 调度器不会尝试重新调度它。因此,建议使用带副本的 Deployment
在此类故障发生时触发 Pod 的重新部署。也可以实现 外部控制循环 来触发具有 TopologyAffinityError
的 Pod 的重新部署。
这太棒了,那么它内部是如何工作的呢?
下面是 TopologyManager
执行的主要逻辑的伪代码:
for container := range append(InitContainers, Containers...) {
for provider := range HintProviders {
hints += provider.GetTopologyHints(container)
}
bestHint := policy.Merge(hints)
for provider := range HintProviders {
provider.Allocate(container, bestHint)
}
}
下图总结了在此循环期间执行的步骤:
步骤如下:
- 遍历 Pod 中的所有容器。
- 对于每个容器,针对容器请求的每种拓扑感知资源类型(例如,
gpu-vendor.com/gpu
、nic-vendor.com/nic
、cpu
等),从一组 "HintProviders
" 中收集 "TopologyHints
"。 - 使用所选策略,合并收集到的
TopologyHints
,以找到对齐所有资源类型资源分配的“最佳”提示。 - 回过头来遍历提示提供者集合,指导它们使用合并后的提示作为指导来分配它们控制的资源。
- 此循环在 Pod 准入时运行,如果任何这些步骤失败或无法根据所选策略满足对齐要求,将拒绝准入 Pod。失败前已分配的任何资源将相应清理。
以下章节将更详细地介绍 TopologyHints
和 HintProviders
的具体结构,以及每种策略使用的合并策略的一些细节。
TopologyHints
一个 TopologyHint
编码了一组约束,通过这些约束可以满足资源请求。目前,我们考虑的唯一约束是 NUMA 对齐。它定义如下:
type TopologyHint struct {
NUMANodeAffinity bitmask.BitMask
Preferred bool
}
NUMANodeAffinity
字段包含 NUMA 节点的位掩码 (bitmask),其中可以满足资源请求。例如,在具有 2 个 NUMA 节点的系统上,可能的掩码包括:
{00}, {01}, {10}, {11}
Preferred
字段包含一个布尔值,编码给定提示是否为“首选”。使用 best-effort
策略时,在生成“最佳”提示时,首选提示将被优先考虑于非首选提示。使用 restricted
和 single-numa-node
策略时,非首选提示将被拒绝。
通常,HintProviders
通过查看能够满足资源请求的当前可用资源集合来生成 TopologyHints
。更具体地说,它们为 NUMA 节点的每个可能的掩码生成一个 TopologyHint
,其中该掩码可以满足资源请求。如果一个掩码不能满足请求,它将被省略。例如,在一个具有 2 个 NUMA 节点的系统上,当被要求分配 2 个资源时,一个 HintProvider
可能提供以下提示。这些提示编码了两种资源可能来自单个 NUMA 节点(节点 0 或节点 1),或者它们可能各自来自不同的 NUMA 节点(但我们更倾向于它们只来自一个节点)。
{01: True}, {10: True}, {11: False}
目前,所有 HintProviders
仅当 NUMANodeAffinity
编码了能够满足资源请求的 *最小* NUMA 节点集合时,才将 Preferred
字段设置为 True
。通常,这只对位掩码中设置了单个 NUMA 节点的 TopologyHints
为 True
。但是,如果满足资源请求的 *唯一* 方法是跨越多个 NUMA 节点(例如,请求 2 个设备,而系统上仅有的 2 个设备在不同的 NUMA 节点上),它也可能为 True
。
{0011: True}, {0111: False}, {1011: False}, {1111: False}
注意: 以这种方式设置 Preferred
字段 *不是* 基于当前可用资源的集合。它是基于在某个最小 NUMA 节点集合上物理分配所请求资源数量的能力。
通过这种方式,当实际的首选分配在其他容器释放资源之前无法满足时,HintProvider
可以返回一份所有 所有 Preferred
字段均设置为 False
的提示列表。例如,考虑图 1 系统中的以下场景:
- 除了 2 个 CPU 外,所有 CPU 都已分配给容器
- 剩余的 2 个 CPU 位于不同的 NUMA 节点上
- 一个新的容器出现,请求 2 个 CPU
在这种情况下,唯一生成的提示将是 {11: False}
,而不是 {11: True}
。这是因为在这个系统上 可以 从同一个 NUMA 节点分配 2 个 CPU(只是目前不行,考虑到当前的分配状态)。这个想法是,当可以满足最低限度的对齐要求时,宁可拒绝 Pod 准入并重试部署,而不是允许 Pod 以次优的对齐方式进行调度。
HintProviders
HintProvider
是 kubelet
的一个内部组件,负责协调与 TopologyManager
的资源对齐分配。目前,Kubernetes 中唯一的 HintProviders
是 CPUManager
和 DeviceManager
。我们计划很快添加对 HugePages
的支持。
如前所述,TopologyManager
既从 HintProviders
收集 TopologyHints
,也使用合并的“最佳”提示触发在其上进行资源对齐分配。因此,HintProviders
实现了以下接口:
type HintProvider interface {
GetTopologyHints(*v1.Pod, *v1.Container) map[string][]TopologyHint
Allocate(*v1.Pod, *v1.Container) error
}
请注意,对 GetTopologyHints()
的调用返回一个 map[string][]TopologyHint
。这允许一个 HintProvider
为多种资源类型(而不仅仅是一种)提供提示。例如,DeviceManager
需要这样做,以便为其插件注册的每种资源类型传递回提示。
当 HintProviders
生成提示时,它们只考虑如何为系统上 当前 可用的资源满足对齐要求。已经分配给其他容器的任何资源都不会被考虑。
例如,考虑图 1 中的系统,有两个容器请求资源:
Container0 | Container1 |
spec: containers: - name: numa-aligned-container0 image: alpine resources: limits: cpu: 2 memory: 200Mi gpu-vendor.com/gpu: 1 nic-vendor.com/nic: 1 | spec: containers: - name: numa-aligned-container1 image: alpine resources: limits: cpu: 2 memory: 200Mi gpu-vendor.com/gpu: 1 nic-vendor.com/nic: 1 |
如果 Container0
是第一个在系统上考虑分配的容器,将为 spec 中的三种拓扑感知资源类型生成以下提示集。
cpu: {{01: True}, {10: True}, {11: False}}
gpu-vendor.com/gpu: {{01: True}, {10: True}}
nic-vendor.com/nic: {{01: True}, {10: True}}
由此产生的对齐分配是
{cpu: {0, 1}, gpu: 0, nic: 0}
当考虑 Container1
时,这些资源将被视为不可用,因此只会生成以下提示集:
cpu: {{01: True}, {10: True}, {11: False}}
gpu-vendor.com/gpu: {{10: True}}
nic-vendor.com/nic: {{10: True}}
由此产生的对齐分配是
{cpu: {4, 5}, gpu: 1, nic: 1}
注意: 与本节开头提供的伪代码不同,对 Allocate()
的调用实际上并不直接接受合并的“最佳”提示作为参数。相反,TopologyManager
实现了以下 Store
接口,HintProviders
在为特定容器生成提示后,可以通过该接口查询并检索该提示。
type Store interface {
GetAffinity(podUID string, containerName string) TopologyHint
}
将其分离成单独的 API 调用允许在 Pod 准入循环之外访问此提示。这对于调试以及在 kubectl
等工具中报告生成的提示非常有用(目前尚未提供)。
策略合并 (Policy.Merge)
给定策略定义的合并策略规定了它如何将所有 HintProviders
生成的 TopologyHints
集组合成单个 TopologyHint
,用于指导对齐的资源分配。
所有支持策略的通用合并策略都以相同的方式开始:
- 对每种资源类型生成的
TopologyHints
取笛卡尔积 (cross-product) - 对于笛卡尔积中的每个条目,将每个
TopologyHint
的 NUMA 亲和性进行按位与 (bitwise-and)
操作。将此结果设置为生成的“合并”提示中的 NUMA 亲和性。 - 如果条目中的所有提示的
Preferred
都设置为True
,则在生成的“合并”提示中将Preferred
设置为True
。 - 如果条目中即使有一个提示的
Preferred
设置为False
,则在生成的“合并”提示中将Preferred
设置为False
。如果“合并”提示的 NUMA 亲和性包含全 0,也将其Preferred
设置为False
。
沿用上一节中为 Container0
生成提示的例子,提示如下:
cpu: {{01: True}, {10: True}, {11: False}}
gpu-vendor.com/gpu: {{01: True}, {10: True}}
nic-vendor.com/nic: {{01: True}, {10: True}}
上述算法产生以下笛卡尔积条目集和“合并”提示:
笛卡尔积条目
| “合并”提示 |
{{01: True}, {01: True}, {01: True}} | {01: True} |
{{01: True}, {01: True}, {10: True}} | {00: False} |
{{01: True}, {10: True}, {01: True}} | {00: False} |
{{01: True}, {10: True}, {10: True}} | {00: False} |
{{10: True}, {01: True}, {01: True}} | {00: False} |
{{10: True}, {01: True}, {10: True}} | {00: False} |
{{10: True}, {10: True}, {01: True}} | {00: False} |
{{10: True}, {10: True}, {10: True}} | {01: True} |
{{11: False}, {01: True}, {01: True}} | {01: False} |
{{11: False}, {01: True}, {10: True}} | {00: False} |
{{11: False}, {10: True}, {01: True}} | {00: False} |
{{11: False}, {10: True}, {10: True}} | {10: False} |
生成此“合并”提示列表后,使用的特定 TopologyManager
策略的工作是决定将哪一个视为“最佳”提示。
一般来说,这涉及
- 按“窄度”对合并提示进行排序。窄度定义为提示的 NUMA 亲和性掩码中设置的位数。设置的位数越少,提示越窄。对于 NUMA 亲和性掩码中设置位数相同的提示,低位设置最多的提示被认为更窄。
- 按其
Preferred
字段对合并提示进行排序。将Preferred
设置为True
的提示被认为比Preferred
设置为False
的提示更有可能成为候选。 - 选择
Preferred
设置尽可能最佳的最窄提示。
对于 best-effort
策略,该算法总是会选择 某个 提示作为“最佳”提示,并且 Pod 将被准入。然后将此“最佳”提示提供给 HintProviders
,以便它们可以根据此提示进行资源分配。
然而,对于 restricted
和 single-numa-node
策略,任何选定的 Preferred
设置为 False
的提示将立即被拒绝,导致 Pod 准入失败,并且不进行资源分配。此外,single-numa-node
还会拒绝其亲和性掩码中设置了多个 NUMA 节点的选定提示。
在上面的示例中,所有策略都将使用提示 {01: True}
准入该 Pod。
即将到来的增强功能
虽然 1.18 版本及其升级到 Beta 带来了许多重要的增强和修复,但仍然存在一些限制,此处有所描述。我们已经在着手解决这些以及更多的限制。
本节将介绍我们计划在不久的将来为 TopologyManager
实现的一系列增强功能。这份列表并非详尽无遗,但它能很好地说明我们前进的方向。列表按我们预计完成每项增强功能的时间顺序排列。
如果您想参与其中,帮助实现这些增强功能中的任何一项,请加入每周的 Kubernetes SIG-node 会议,了解更多信息并成为社区工作的一部分!
支持设备特定约束
目前,NUMA 亲和性是 TopologyManager
考虑用于资源对齐的唯一约束。此外,可以对 TopologyHint
进行的唯一可扩展的扩展涉及 节点级 约束,例如跨设备类型的 PCIe 总线对齐。尝试向此结构添加任何 设备特定 的约束(例如,一组 GPU 设备之间的内部 NVLINK 拓扑)将是难以处理的。
因此,我们提议对设备插件接口进行扩展,允许插件声明其拓扑感知的分配偏好,而无需向 kubelet 暴露任何设备特定的拓扑信息。通过这种方式,可以将 TopologyManager
限制为仅处理常见的节点级拓扑约束,同时仍有办法在其分配决策中纳入设备特定的拓扑约束。
此提案的详细信息可以在此处找到,应该会在 Kubernetes 1.19 中提供。
hugepages 的 NUMA 对齐
如前所述,目前提供给 TopologyManager
的只有 CPUManager
和 DeviceManager
这两个 HintProviders
。然而,目前正在进行工作,以添加对 hugepages 的支持。完成这项工作后,TopologyManager
最终将能够在同一 NUMA 节点上分配内存、hugepages、CPU 和 PCI 设备。
这项工作的 KEP 目前正在审查中,一个原型正在进行中,以尽快实现此功能。
调度器感知
目前,TopologyManager
充当 Pod 准入控制器。它不直接参与 Pod 将被放置到何处的调度决策。相反,当 Kubernetes 调度器(或部署中运行的任何调度器)将一个 Pod 放置到节点上运行时,TopologyManager
将决定该 Pod 是否应该被“准入”或“拒绝”。如果 Pod 由于缺少可用的 NUMA 对齐资源而被拒绝,事情可能会变得有点复杂。这个 Kubernetes 问题很好地强调和讨论了这种情况。
那么我们如何解决这个限制呢?我们可以求助于 Kubernetes 调度框架!这个框架提供了一套新的插件 API,可以与现有的 Kubernetes 调度器集成,并允许实现调度功能(例如 NUMA 对齐),而无需借助于其他可能不太吸引人的替代方案,包括编写自己的调度器,或者更糟糕的是,创建一个分支来添加自己的调度秘诀。
如何实现这些扩展以与 TopologyManager
集成的具体细节尚未确定。我们仍然需要回答一些问题,例如:
- 我们是否需要在
TopologyManager
和调度器中重复设备亲和性判断逻辑? - 我们需要一个新的 API 来将
TopologyHints
从TopologyManager
传递到调度器插件吗?
这项功能的工作应该在未来几个月内开始,敬请关注!
每个 Pod 的对齐策略
如前所述,通过全局 kubelet
标志将单个策略应用于节点上的 所有 Pod,而不是允许用户按 Pod(或按容器)选择不同的策略。
虽然我们认为这将是一个很棒的功能,但在实现它之前还有很多障碍需要克服。最大的障碍是这项增强功能将需要 API 更改,以便能够在 Pod spec 或其关联的 RuntimeClass
中表达所需的对齐策略。
我们现在才开始认真讨论这项功能,距离可用至少还需要几个版本。
结论
随着 TopologyManager
在 1.18 版本升级到 Beta,我们鼓励大家尝试并期待您提出任何反馈意见。在过去的几个版本中,我们一直在努力进行许多修复和增强,极大地改善了 TopologyManager
及其 HintProviders
的功能性和可靠性。虽然仍有一些限制,但我们计划了一系列增强功能来解决这些问题,并期待在未来的版本中为您提供一系列新功能。
如果您有额外的增强功能想法或对某些功能有需求,请随时告诉我们。团队始终乐于接受建议,以增强和改进 TopologyManager
。
希望您觉得这篇博文内容丰富且实用!如果您有任何问题或意见,请告知我们。祝您部署愉快......对齐起来!