本文发表时间已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已变得不正确。
Kubernetes 拓扑管理器移至 Beta 版 - 对齐!
这篇博客文章描述了 TopologyManager
,它是 Kubernetes 1.18 版本中的一个 beta 功能。 TopologyManager
功能可以实现 CPU 和外围设备(如 SR-IOV VF 和 GPU)的 NUMA 对齐,从而使你的工作负载能够在针对低延迟优化的环境中运行。
在引入 TopologyManager
之前,CPU 和设备管理器会独立做出资源分配决策。这可能会导致在多插槽系统上出现不良分配,从而导致延迟关键应用程序的性能下降。随着 TopologyManager
的引入,我们现在有办法避免这种情况。
这篇博客文章涵盖了
- 简要介绍 NUMA 以及它为何重要
- 最终用户可用来确保 CPU 和设备 NUMA 对齐的策略
TopologyManager
的内部工作原理的详细信息TopologyManager
当前的限制TopologyManager
的未来方向
那么,什么是 NUMA?我为什么要关心它?
术语 NUMA 代表非一致性内存访问。它是一种在多 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 的一部分,因为它们连接到插槽 0,插槽 0 的 CPU 都是 NUMA 节点 0 的一部分。GPU 1 和 NIC 1 在 NUMA 节点 1 上也是如此。
图 1:一个具有 2 个 NUMA 节点、2 个具有 4 个 CPU 的插槽、2 个 GPU 和 2 个 NIC 的示例系统。插槽 0 上的 CPU、GPU 0 和 NIC 0 都是 NUMA 节点 0 的一部分。插槽 1 上的 CPU、GPU 1 和 NIC 1 都是 NUMA 节点 1 的一部分。
尽管上面的示例显示了 NUMA 节点到插槽的一对一映射,但在一般情况下并非如此。单个 NUMA 节点上可能存在多个插槽,或者单个插槽的单个 CPU 可能连接到不同的 NUMA 节点。此外,新兴技术(如子 NUMA 集群(在最近的英特尔 CPU 上可用))允许单个 CPU 与多个 NUMA 节点关联,只要它们对两个节点的内存访问时间相同(或具有可忽略不计的差异)。
构建 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:
此策略是最严格的,并且仅当可以从完全一个 NUMA 节点分配所有请求的 CPU 和设备时,才允许准入 pod。
请务必注意,所选策略会单独应用于 pod 规范中的每个容器,而不是跨所有容器一起对齐资源。
此外,单个策略会通过全局 kubelet
标志应用于节点上的所有 pod,而不是允许用户在每个 pod 或每个容器的基础上选择不同的策略。我们希望在将来放宽此限制。
可以在下面看到设置其中一个策略的 kubelet
标志
--topology-manager-policy=
[none | best-effort | restricted | single-numa-node]
此外,TopologyManager
受功能门保护。此功能门自 Kubernetes 1.16 以来已可用,但自 1.18 以来才默认启用。
可以按如下方式启用或禁用功能门(如 此处 详细所述)
--feature-gates="...,TopologyManager=<true|false>"
为了根据所选策略触发对齐,用户必须根据一组特定的要求在其 pod 规范中请求 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 规范足以触发 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 中的所有容器。
- 对于每个容器,从一组“
HintProviders
”中为容器请求的每个拓扑感知资源类型(例如,gpu-vendor.com/gpu
、nic-vendor.com/nic
、cpu
等)收集“TopologyHints
”。 - 使用所选的策略,合并收集的
TopologyHints
,以查找在所有资源类型之间对齐资源分配的“最佳”提示。 - 循环返回提示提供程序集,指示它们使用合并的提示作为指导来分配它们控制的资源。
- 此循环在 pod 准入时运行,如果任何这些步骤失败或无法根据所选策略满足对齐,则将无法准入 pod。在失败之前分配的任何资源都会被相应地清除。
以下各节将详细介绍 TopologyHints
和 HintProviders
的确切结构,以及每种策略使用的合并策略的一些细节。
TopologyHints
TopologyHint
编码了一组约束,从中可以满足给定的资源请求。目前,我们只考虑 NUMA 对齐。它的定义如下:
type TopologyHint struct {
NUMANodeAffinity bitmask.BitMask
Preferred bool
}
NUMANodeAffinity
字段包含一个 NUMA 节点的位掩码,资源请求可以在这些节点上得到满足。例如,在具有 2 个 NUMA 节点的系统上,可能的掩码包括:
{00}, {01}, {10}, {11}
Preferred
字段包含一个布尔值,用于编码给定的提示是否为“首选”。使用 best-effort
策略时,在生成“最佳”提示时,首选提示将优先于非首选提示。对于 restricted
和 single-numa-node
策略,非首选提示将被拒绝。
通常,HintProviders
通过查看当前可用的、可以满足资源请求的资源集来生成 TopologyHints
。更具体地说,它们为可以满足该资源请求的每个可能的 NUMA 节点掩码生成一个 TopologyHint
。如果某个掩码无法满足请求,则将其省略。例如,当被要求分配 2 个资源时,HintProvider
可能会在具有 2 个 NUMA 节点的系统上提供以下提示。这些提示表明,这两个资源可以来自单个 NUMA 节点(0 或 1),也可以各自来自不同的 NUMA 节点(但我们希望它们只来自一个)。
{01: True}, {10: True}, {11: False}
目前,当且仅当 NUMANodeAffinity
编码了可以满足资源请求的最小 NUMA 节点集时,所有 HintProviders
都将 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
是在系统上考虑分配的第一个容器,则将为规范中的三个拓扑感知资源类型生成以下提示集:
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
的叉积 - 对于叉积中的每个条目,将每个
TopologyHint
的 NUMA 亲和性进行按位与
运算。将其设置为生成的“合并”提示中的 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 亲和性掩码中设置的位数相同的提示,将设置了最多低位(low order bits)的提示视为较窄。
- 按
Preferred
字段对合并的提示进行排序。Preferred
设置为True
的提示被认为比Preferred
设置为False
的提示更有可能是候选提示。 - 选择具有
Preferred
最佳可能设置的最窄提示。
在 best-effort
策略的情况下,此算法将始终选择某个提示作为“最佳”提示,并且允许 Pod 准入。然后,将此“最佳”提示提供给 HintProviders
,以便它们可以基于此提示进行资源分配。
但是,在 restricted
和 single-numa-node
策略的情况下,任何 Preferred
设置为 False
的选定提示都会立即被拒绝,导致 Pod 准入失败并且不分配任何资源。此外,single-numa-node
还会拒绝在其亲和性掩码中设置了多个 NUMA 节点的选定提示。
在上面的示例中,所有策略都将允许 Pod 准入,提示为 {01: True}
。
即将进行的增强
虽然 1.18 版本和晋升为 Beta 带来了一些很棒的增强和修复,但仍然存在许多限制,请此处所述。我们已经在努力解决这些限制以及更多。
本节将介绍我们计划在不久的将来为 TopologyManager
实现的一系列增强功能。此列表并不详尽,但它很好地说明了我们前进的方向。它按我们预计完成每个增强功能的时间顺序排列。
如果您想参与帮助进行任何这些增强功能,请加入每周的 Kubernetes SIG-node 会议以了解更多信息并成为社区工作的一部分!
支持特定于设备的约束
目前,NUMA 亲和性是 TopologyManager
用于资源对齐的唯一约束。此外,对 TopologyHint
进行的唯一可扩展扩展涉及节点级约束,例如跨设备类型的 PCIe 总线对齐。尝试将任何特定于设备的约束添加到此结构将是棘手的(例如,一组 GPU 设备之间的内部 NVLINK 拓扑)。
因此,我们建议对设备插件接口进行扩展,这将允许插件声明其拓扑感知分配首选项,而无需向 kubelet 公开任何特定于设备的拓扑信息。通过这种方式,可以将 TopologyManager
限制为仅处理常见的节点级拓扑约束,同时仍然有一种方法将特定于设备的拓扑约束合并到其分配决策中。
有关此提案的详细信息,请参阅此处,该提案应在 Kubernetes 1.19 发布后即可使用。
针对大页内存的 NUMA 对齐
如前所述,目前可供 TopologyManager
使用的 HintProviders
只有 CPUManager
和 DeviceManager
这两个。然而,目前正在进行增加对大页内存支持的工作。完成这项工作后,TopologyManager
最终将能够将内存、大页内存、CPU 和 PCI 设备全部分配到同一个 NUMA 节点上。
关于这项工作的 KEP 目前正在审查中,并且一个原型正在进行中,以尽快实现此功能。
调度器感知
目前,TopologyManager
充当 Pod 准入控制器。它不直接参与 Pod 的调度决策。相反,当 Kubernetes 调度器(或部署中运行的任何调度器)将 Pod 放置在节点上运行时,TopologyManager
将决定是否“允许”或“拒绝”该 Pod。如果由于缺少可用的 NUMA 对齐资源而拒绝 Pod,情况可能会变得有点棘手。这个 Kubernetes 问题很好地强调并讨论了这种情况。
那么我们如何解决这个限制呢?我们有 Kubernetes 调度框架来解救!该框架提供了一组新的插件 API,这些 API 与现有的 Kubernetes 调度器集成,并允许实现调度功能(例如 NUMA 对齐),而无需诉诸其他可能不太吸引人的替代方案,包括编写自己的调度器,或者更糟糕的是,创建分支来添加自己的调度器秘密武器。
如何实现这些扩展以与 TopologyManager
集成的细节尚未确定。我们仍然需要回答诸如以下的问题:
- 我们是否需要在
TopologyManager
和调度器中复制逻辑来确定设备亲和性? - 我们需要一个新的 API 来从
TopologyManager
获取TopologyHints
到调度器插件吗?
此功能的工作应在未来几个月内开始,敬请期待!
每个 Pod 的对齐策略
如前所述,单个策略通过全局 kubelet
标志应用于节点上的所有 Pod,而不是允许用户在每个 Pod(或每个容器)的基础上选择不同的策略。
虽然我们同意这将是一个很棒的功能,但在实现它之前需要克服相当多的障碍。最大的障碍是,此增强功能将需要 API 更改,以便能够在 Pod 规范或其关联的 RuntimeClass
中表达所需的对齐策略。
我们现在才开始围绕此功能进行认真的讨论,并且即使在最好的情况下,距离可用也还需要几个版本。
结论
随着 TopologyManager
在 1.18 版本中晋升为 Beta 版,我们鼓励大家尝试一下,并期待大家提供任何反馈。在过去的几个版本中,我们已经进行了许多修复和增强工作,大大提高了 TopologyManager
及其 HintProviders
的功能和可靠性。虽然仍然存在一些限制,但我们计划了一系列增强功能来解决这些限制,并期待在即将发布的版本中为您提供许多新功能。
如果您对其他增强功能有想法或对某些功能有需求,请随时告诉我们。团队始终乐于接受改进和增强 TopologyManager
的建议。
我们希望您觉得这篇博文内容丰富且有用!如果您有任何问题或意见,请告诉我们。 祝您部署愉快……保持对齐!