本文发表于一年多前。旧文章可能包含过时内容。请检查页面中的信息自发布以来是否已变得不正确。
Kubernetes Topology Manager 进入 Beta 阶段 - 对齐吧!
这篇博文介绍了 TopologyManager
,这是 Kubernetes 1.18 版本中的一个 Beta 功能。TopologyManager
功能支持 CPU 和外围设备(如 SR-IOV VFs 和 GPU)的 NUMA 对齐,使您的工作负载能够在针对低延迟优化的环境中运行。
在引入 TopologyManager
之前,CPU 管理器和设备管理器会独立做出资源分配决策。这可能导致在多路 CPU 系统上进行不理想的分配,从而降低延迟关键应用程序的性能。通过引入 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 节点的一部分时,是指它的本地内存与该 NUMA 节点相关联。
当我们说某个外围设备是 NUMA 节点的一部分时,是根据到达它所需的互连数量最短来确定的。
例如,在图 1 中,CPU 0-3 被认为是 NUMA 节点 0 的一部分,而 CPU 4-7 是 NUMA 节点 1 的一部分。同样,GPU 0 和 NIC 0 被认为是 NUMA 节点 0 的一部分,因为它们连接到 Socket 0,而 Socket 0 的所有 CPU 都是 NUMA 节点 0 的一部分。对于 NUMA 节点 1 上的 GPU 1 和 NIC 1 也是如此。
图 1:一个包含 2 个 NUMA 节点、2 个包含 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 节点与套接字的一对一映射,但这在一般情况下并非如此。单个 NUMA 节点上可能有多个套接字,或者单个套接字上的各个 CPU 可能连接到不同的 NUMA 节点。此外,新兴技术,如子 NUMA 集群(在最新的 Intel 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:
此策略最严格,仅允许在*所有*请求的 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 中的所有容器。
- 对于每个容器,从一组“
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 个资源时,在具有 2 个 NUMA 节点的系统上,HintProvider
可能会提供以下提示。这些提示编码了两个资源都可以来自单个 NUMA 节点(0 或 1),或者它们可以分别来自不同的 NUMA 节点(但我们希望它们仅来自一个)。
{01: True}, {10: True}, {11: False}
目前,只有当 NUMANodeAffinity
编码了一个*最小*集,可以满足资源请求的 NUMA 节点时,所有 HintProviders
才将 Preferred
字段设置为 True
。通常,这仅对 TopologyHints
的位掩码中设置了单个 NUMA 节点的有效。然而,如果*只能*通过跨越多个 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
的笛卡尔积。 - 对于笛卡尔积中的每个条目,将每个
TopologyHint
的 NUMA 亲和性进行*按位与*运算。将其设置为结果“合并”提示中的 NUMA 亲和性。 - 如果条目中的所有提示都将
Preferred
设置为True
,则将结果“合并”提示中的Preferred
设置为True
。 - 如果条目中的任何一个提示将
Preferred
设置为False
,则将结果“合并”提示中的Preferred
设置为False
。如果其 NUMA 亲和性包含全零,也将“合并”提示中的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 节点。
在上面的示例中,pod 将被所有策略以提示 {01: True}
准入。
即将进行的增强
虽然 1.18 版本发布并晋升为 Beta 版带来了一些很棒的增强和修复,但仍然存在一些限制,此处有描述。我们已经着手解决这些限制以及更多问题。
本节将介绍我们计划在不久的将来为 TopologyManager
实现的一系列增强。此列表并非详尽无遗,但它能让你大致了解我们的发展方向。它按我们预计每个增强功能完成的时间顺序排列。
如果您想参与帮助实现任何这些增强功能,请加入每周的 Kubernetes SIG-node 会议以了解更多信息并成为社区努力的一部分!
支持设备特定约束
目前,NUMA 亲和性是 TopologyManager
为资源对齐考虑的唯一约束。此外,可以对 TopologyHint
进行的可扩展的唯一扩展涉及*节点级*约束,例如跨设备类型的 PCIe 总线对齐。尝试将任何*设备特定*约束添加到此结构(例如,一组 GPU 设备之间的内部 NVLINK 拓扑)将是不可行的。
因此,我们建议对设备插件接口进行扩展,允许插件声明其拓扑感知的分配偏好,而无需将任何设备特定的拓扑信息暴露给 kubelet。通过这种方式,TopologyManager
可以仅限于处理通用的节点级拓扑约束,同时仍然有一种方法可以将其设备特定的拓扑约束纳入其分配决策。
此提案的详细信息可以在此处找到,并且应该很快就可以在 Kubernetes 1.19 中使用了。
Hugepages 的 NUMA 对齐
如前所述,目前 TopologyManager
可用的两个 HintProviders
是 CPUManager
和 DeviceManager
。然而,目前正在努力添加对 hugepages 的支持。完成这项工作后,TopologyManager
将最终能够将内存、hugepages、CPU 和 PCI 设备全部分配在同一个 NUMA 节点上。
此工作的KEP目前正在审查中,并且一个原型正在进行中,以尽快实现此功能。
调度器感知
目前,TopologyManager
作为 Pod 准入控制器。它不直接参与 pod 的放置位置的调度决策。相反,当 Kubernetes 调度程序(或部署中运行的任何调度程序)将 pod 放置在节点上运行时,TopologyManager
将决定 pod 是否应被“准入”或“拒绝”。如果 pod 因缺乏可用的 NUMA 对齐资源而被拒绝,情况可能会变得有些棘手。这个 Kubernetes issue 很好地突出了并讨论了这种情况。
那么我们如何解决这个限制呢?我们有 Kubernetes 调度框架 来拯救!该框架提供了一组新的插件 API,与现有的 Kubernetes 调度程序集成,并允许实现调度功能(例如 NUMA 对齐),而无需诉诸其他可能不那么理想的替代方案,包括编写自己的调度程序,甚至更糟的是,创建分叉以添加自己的调度程序秘密武器。
这些扩展以与 TopologyManager
集成的细节尚未制定。我们仍然需要回答以下问题:
- 我们将需要重复逻辑来确定
TopologyManager
和调度程序中的设备亲和性吗? - 我们需要一个新的 API 将
TopologyHints
从TopologyManager
传递到调度程序插件吗?
这项功能的工作应该在未来几个月内开始,所以请继续关注!
每个 pod 的对齐策略
如前所述,一个策略是通过全局 kubelet
标志应用于节点上的*所有* pod,而不是允许用户逐个 pod(或逐个容器)选择不同的策略。
虽然我们同意这是一个很棒的功能,但在实现它之前需要克服许多障碍。最大的障碍是,此增强功能需要进行 API 更改,以便能够将所需的对齐策略表达在 Pod spec 或其关联的 RuntimeClass
中。
我们才刚刚开始认真讨论这个功能,并且最多还需要几个版本才能可用。
结论
随着 TopologyManager
在 1.18 版本中晋升为 Beta 版,我们鼓励大家尝试一下,并期待您的反馈。在过去几个版本中,已进行了许多修复和增强,极大地提高了 TopologyManager
及其 HintProviders
的功能性和可靠性。尽管仍存在一些限制,但我们已计划进行一系列增强来解决这些限制,并期待在即将发布的新版本中为您提供许多新功能。
如果您对其他增强功能有想法或希望使用某些功能,请不要犹豫告诉我们。该团队始终乐于接受有关增强和改进 TopologyManager
的建议。
希望这篇博文内容丰富且有用!如果您有任何问题或评论,请告诉我们。祝您部署顺利……团结一致!