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

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

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

调度队列

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

调度队列由以下数据结构组成:

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

调度框架和插件

Kubernetes 调度器是遵循 Kubernetes 调度框架实现的。

并且,所有调度功能都作为插件实现(例如,Pod 亲和性是在 InterPodAffinity 插件中实现的)。

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

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

    如果调度器决定一个 Pod 无法调度,该 Pod 将进入调度队列的 Unschedulable Pod Pool 组件。但是,如果调度器决定将 Pod 放置在某个节点上,则该 Pod 将进入绑定周期。

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

除了一些例外情况,大多数未调度的 Pod 在每个调度周期后都会进入不可调度 Pod 池。Unschedulable Pod Pool 组件至关重要,因为调度周期是逐一处理 Pod 的。如果调度器必须不断重试放置不可调度的 Pod,而不是将这些 Pod 转移到 Unschedulable Pod Pool,那么多个调度周期将会浪费在这些 Pod 上。

使用 QueuingHint 改进 Pod 调度重试

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

在 v1.32 之前,每个插件会通过 EnqueueExtensions (EventsToRegister) 注册哪些集群变化可以解决它们的失败,即集群中的对象创建、更新或删除(称为集群事件),并且调度队列会根据在先前调度周期中拒绝该 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

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

pod-a 被移入 Unschedulable Pod Pool。调度队列记录了哪个插件导致了该 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

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

QueueingHint 的历史以及 v1.32 的新功能

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

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

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

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

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

参与进来

这些功能由 Kubernetes SIG Scheduling 管理。

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

我如何了解更多信息?