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 亲和性InterPodAffinity 插件中实现。)

调度器按称为周期的阶段处理待处理的 Pod,如下所示

  1. 调度周期:调度器逐个从调度队列的 activeQ 组件中获取待处理的 Pod。对于每个 Pod,调度器运行每个调度插件的过滤/评分逻辑。然后,调度器确定 Pod 的最佳节点,或者确定该 Pod 此时无法调度。

    如果调度器确定 Pod 无法调度,则该 Pod 将进入调度队列的“无法调度的 Pod 池”组件。但是,如果调度器决定将 Pod 放置在节点上,则该 Pod 将进入绑定周期。

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

除了一些例外,大多数未调度的 Pod 在每个调度周期后都会进入无法调度的 Pod 池。由于调度周期逐个处理 Pod 的方式,“无法调度的 Pod 池”组件至关重要。如果调度器必须不断重试放置无法调度的 Pod,而不是将这些 Pod 卸载到“无法调度的 Pod 池”,则会在这些 Pod 上浪费多个调度周期。

使用 QueuingHint 改进重试 Pod 调度

只有当集群中的更改可能允许调度器将这些 Pod 放置在节点上时,无法调度的 Pod 才会移回调度队列的 ActiveQ 或 BackoffQ 组件。

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

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

但是,这些方法存在两个问题

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

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

例如,考虑一个名为 pod-a 的 Pod,它具有所需的 Pod 亲和性。pod-a 在调度周期中被 InterPodAffinity 插件拒绝,因为没有节点存在与 pod-a 的 Pod 亲和性规范匹配的现有 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。

在解决 InterPodAffinity 失败之前,pod-a 将永远无法被调度。有一些场景可以解决此失败,一个示例是现有的正在运行的 Pod 获得标签更新并变得与 Pod 亲和性匹配。在这种情况下,InterPodAffinity 插件的 QueuingHint 回调函数会检查集群中发生的每个 Pod 标签更新。然后,如果一个 Pod 获得的标签更新与 pod-a 的 Pod 亲和性要求匹配,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 管理。

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

如何了解更多?