Kubernetes v1.32:QueueingHint 为优化 Pod 调度带来新的可能性

Kubernetes 调度器是选择新 Pod 运行节点的核心组件。调度器会逐一处理这些新 Pod。因此,您的集群越大,调度器的吞吐量就越重要。

多年来,Kubernetes SIG Scheduling 通过多项增强改进了调度器的吞吐量。本文介绍了 Kubernetes v1.32 中调度器的一项重要改进:一个名为 QueueingHint调度上下文元素。本页面提供了调度器的背景知识,并解释了 QueueingHint 如何提高调度吞吐量。

调度队列

调度器将所有未调度的 Pod 存储在一个名为调度队列的内部组件中。

调度队列包含以下数据结构:

  • ActiveQ:存放新创建的 Pod 或准备好重试调度的 Pod。
  • BackoffQ:存放准备好重试但正在等待退避周期结束的 Pod。退避周期取决于调度器对该 Pod 进行不成功调度尝试的次数。
  • 不可调度 Pod 池:存放调度器不会尝试调度的 Pod,原因如下之一:
    • 调度器之前尝试但未能调度这些 Pod。自那次尝试以来,集群没有以可能使这些 Pod 可调度的方式发生变化。
    • 这些 Pod 被 PreEnqueue 插件阻止进入调度周期,例如,它们具有调度门,并被调度门插件阻止。

调度框架和插件

Kubernetes 调度器是根据 Kubernetes 调度框架实现的。

并且,所有调度特性都实现为插件(例如,Pod AffinityInterPodAffinity 插件中实现)。

调度器按照以下称为周期的阶段处理待处理的 Pod:

  1. 调度周期:调度器会从调度队列的 ActiveQ 组件中逐一获取待处理的 Pod。对于每个 Pod,调度器会运行每个调度插件的过滤/打分逻辑。然后调度器决定 Pod 的最佳节点,或者决定 Pod 当前无法调度。

    如果调度器决定某个 Pod 无法调度,该 Pod 将进入调度队列的不可调度 Pod 池组件。然而,如果调度器决定将 Pod 放置在某个节点上,则该 Pod 进入绑定周期。

  2. 绑定周期:调度器将节点放置决定告知 Kubernetes API 服务器。此操作将 Pod 绑定到选定的节点。

除了一些例外,大多数未调度的 Pod 在每个调度周期后都会进入不可调度 Pod 池。不可调度 Pod 池组件至关重要,因为它与调度周期逐一处理 Pod 的方式有关。如果调度器不得不持续重试放置不可调度 Pod,而不是将这些 Pod 卸载到不可调度 Pod 池,那么多个调度周期将浪费在这些 Pod 上。

使用 QueueingHint 改进 Pod 调度重试

不可调度 Pod 只有在集群发生变化可能使调度器能将这些 Pod 放置在节点上时,才会移回调度队列的 ActiveQ 或 BackoffQ 组件。

在 v1.32 之前,每个插件都会通过 EnqueueExtensions (EventsToRegister) 注册哪些集群变化(集群中的对象创建、更新或删除,称为集群事件)可以解决它们的失败,并且调度队列会使用在一个之前的调度周期中拒绝了该 Pod 的插件所注册的事件来重试该 Pod。

此外,我们还有一个名为 preCheck 的内部特性,它基于 Kubernetes 核心调度约束进一步帮助过滤事件以提高效率;例如,当节点状态为 NotReady 时,preCheck 可以过滤掉与节点相关的事件。

然而,对于这些方法,我们存在两个问题:

  • 使用事件重新排队过于宽泛,可能导致不必要的调度重试。
    • 一个新的已调度 Pod 可能解决 InterPodAffinity 的失败,但并非所有新 Pod 都能做到。例如,如果创建了一个新 Pod,但它没有与不可调度 Pod 的 InterPodAffinity 匹配的标签,该 Pod 将无法调度。
  • preCheck 依赖于树内插件的逻辑,且无法扩展到自定义插件,如 issue #110175 中所述。

QueueingHints 在这里发挥作用;QueueingHint 会订阅特定类型的集群事件,并决定每个传入事件是否可能使 Pod 可调度。

例如,考虑一个名为 pod-a 的 Pod,它具有必需的 Pod Affinity。pod-a 在调度周期中被 InterPodAffinity 插件拒绝,因为没有节点存在与 pod-a 的 Pod Affinity 规范匹配的 Pod。

A diagram showing the scheduling queue and pod-a rejected by InterPodAffinity plugin

显示调度队列和被 InterPodAffinity 插件拒绝的 pod-a 的图示

pod-a 进入不可调度 Pod 池。调度队列记录是哪个插件导致了该 Pod 的调度失败。对于 pod-a,调度队列记录是 InterPodAffinity 插件拒绝了该 Pod。

pod-a 在 InterPodAffinity 失败解决之前永远无法调度。有一些场景可以解决这个失败,一个例子是现有运行的 Pod 获得标签更新,并变得符合 Pod Affinity。对于这种情况,InterPodAffinity 插件的 QueuingHint 回调函数会检查集群中发生的每个 Pod 标签更新。然后,如果某个 Pod 获得了一个匹配 pod-a 的 Pod Affinity 要求的标签更新,InterPodAffinity 插件的 QueuingHint 会提示调度队列将 pod-a 移回 ActiveQ 或 BackoffQ 组件。

A diagram showing the scheduling queue and pod-a being moved by InterPodAffinity QueueingHint

显示调度队列和被 InterPodAffinity QueueingHint 移动的 pod-a 的图示

QueueingHint 的历史以及 v1.32 中的新特性

在 SIG Scheduling,我们自 Kubernetes v1.28 以来一直在开发 QueueingHint。

尽管 QueueingHint 不是面向用户的,但在最初添加此特性时,我们实现了 SchedulerQueueingHints 特性门作为安全措施。在 v1.28 中,我们通过一些树内插件实验性地实现了 QueueingHints,并将特性门默认启用。

然而,用户报告了内存泄漏,因此我们在 v1.28 的一个补丁版本中禁用了特性门。从 v1.28 到 v1.31,我们持续在其余树内插件中实现 QueueingHint 并修复 Bug。

在 v1.32 中,我们再次默认启用了此特性。我们完成了在所有插件中实现 QueueingHints 的工作,并找到了内存泄漏的原因!

我们感谢所有参与此特性开发的贡献者,以及那些报告和调查早期问题的贡献者。

参与其中

这些特性由 Kubernetes SIG Scheduling 管理。

请加入我们并分享您的反馈。

我如何了解更多?