本文发表于一年多前。旧文章可能包含过时内容。请检查页面中的信息自发布以来是否已变得不正确。

Kubernetes 1.26:支持大规模并行批处理工作负载的 Job 跟踪功能已正式可用

Kubernetes 1.26 版本包含了一个稳定的 Job 控制器实现,能够可靠地跟踪大量具有高并行度的 Job。自 Kubernetes 1.22 以来,SIG AppsWG Batch 一直在致力于这一基础性改进。经过多次迭代和规模验证,这现在已成为 Job 控制器的默认实现。

与 Indexed 完成模式配合使用,Job 控制器可以处理大规模并行的批处理 Job,支持多达 10 万个并发 Pod。

新的实现还使得Pod 失败策略的开发成为可能,该功能在 1.26 版本中处于 Beta 阶段。

我如何使用这个功能?

要使用带 finalizer 的 Job 跟踪功能,请升级到 Kubernetes 1.25 或更高版本并创建新的 Job。如果您能够启用 JobTrackingWithFinalizers 特性门控,也可以在 v1.23 和 v1.24 中使用此功能。

如果您的集群运行的是 Kubernetes 1.26,带 finalizer 的 Job 跟踪是一个稳定功能。对于 v1.25,它受该特性门控控制,您的集群管理员可能已明确禁用它——例如,如果您的策略是不使用 Beta 功能。

升级前创建的 Job 仍将使用旧的行为进行跟踪。这是为了避免对正在运行的 Pod 追溯性地添加 finalizer,这可能会引入竞争条件。

对于大型 Job,为了获得最佳性能,Kubernetes 项目建议使用Indexed 完成模式。在这种模式下,控制平面能够以更少的 API 调用来跟踪 Job 进度。

如果您是批处理、HPCAIML 或相关工作负载的 Operator 开发人员,我们鼓励您使用 Job API 将精确的进度跟踪委托给 Kubernetes。如果 Job API 中缺少某些功能,导致您不得不管理普通的 Pod,Working Group Batch 欢迎您的反馈和贡献。

弃用通知

在该功能开发期间,控制平面为启用该功能时创建的 Job 添加了注解 batch.kubernetes.io/job-tracking。这为旧的 Job 提供了一个安全的过渡,但它从未打算永久保留。

在 1.26 版本中,我们弃用了注解 batch.kubernetes.io/job-tracking,控制平面将在 Kubernetes 1.27 中停止添加它。与此同时,我们将移除旧的 Job 跟踪实现。因此,Job 控制器将使用 finalizer 跟踪所有 Job,并忽略没有上述 finalizer 的 Pod。

在将集群升级到 1.27 之前,我们建议您验证没有正在运行的、不带该注解的 Job,或者等待这些 Job 完成。否则,您可能会观察到控制平面重新创建一些 Pod。我们预计这不会影响任何用户,因为该功能自 Kubernetes 1.25 起已默认启用,为旧 Job 的完成提供了足够的缓冲时间。

新实现解决了什么问题?

通常,Kubernetes 的工作负载控制器,如 ReplicaSet 或 StatefulSet,依赖于 API 中 Pod 或其他对象的存在来确定工作负载的状态以及是否需要替换。例如,如果属于 ReplicaSet 的 Pod 终止或不再存在,ReplicaSet 控制器需要创建一个替换 Pod 来满足期望的副本数(.spec.replicas)。

自诞生以来,Job 控制器也依赖于 API 中 Pod 的存在来跟踪 Job 状态。Job 具有完成失败处理策略,需要已完成 Pod 的最终状态来决定是创建替换 Pod 还是将 Job 标记为已完成或失败。因此,Job 控制器依赖于 Pod(即使是已终止的 Pod)保留在 API 中,以便跟踪状态。

这种依赖性使得 Job 状态的跟踪不可靠,因为 Pod 可能因多种原因从 API 中被删除,包括:

  • 当节点宕机时,垃圾收集器移除孤立的 Pod。
  • 当已终止的 Pod 数量达到阈值时,垃圾收集器将其移除。
  • Kubernetes 调度器为了容纳更高优先级的 Pod 而抢占一个 Pod。
  • 污点管理器驱逐一个不容忍 NoExecute 污点的 Pod。
  • 外部控制器(不属于 Kubernetes 的一部分)或人为删除 Pod。

新的实现

当控制器需要在对象被移除前对其执行操作时,它应该为其管理的对象添加一个 finalizer。finalizer 会阻止对象从 API 中被删除,直到 finalizer 被移除。一旦控制器完成了对被删除对象的清理和统计,它就可以从对象中移除 finalizer,然后控制平面会从 API 中移除该对象。

这就是新的 Job 控制器所做的事情:在 Pod 创建期间添加 finalizer,并在 Pod 终止并已在 Job 状态中被统计后移除 finalizer。然而,这并非那么简单。

主要挑战在于至少涉及两个对象:Pod 和 Job。finalizer 存在于 Pod 对象中,而统计信息则存在于 Job 对象中。没有机制可以原子地移除 Pod 中的 finalizer 并更新 Job 状态中的计数器。此外,在任何给定时间可能有一个以上的已终止 Pod。

为了解决这个问题,我们实现了一个三阶段的方法,每个阶段都对应一次 API 调用。

  1. 对于每个已终止的 Pod,将其唯一 ID(UID)添加到所属 Job 的 .status 中存储的短期列表中(.status.uncountedTerminatedPods)。
  2. 从 Pod 中移除 finalizer。
  3. 原子地执行以下操作:
    • 从短期列表中移除 UID
    • 在 Job 的 status 中增加总体的 succeededfailed 计数器。

额外的复杂性来自于 Job 控制器可能会乱序接收到第 1 步和第 2 步中 API 更改的结果。我们通过添加一个内存中的缓存来存储已移除的 finalizer,解决了这个问题。

尽管如此,我们在 Beta 阶段仍然遇到了一些问题,导致在某些情况下一些 Pod 的 finalizer 卡住无法移除(#108645#109485#111646)。因此,我们决定在 1.23 和 1.24 版本中将该特性门控默认设置为禁用。

问题解决后,我们在 1.25 版本中重新启用了该功能。从那时起,我们收到了客户的报告,他们通过 Job API 在其集群中同时运行数万个 Pod。看到这一成功,我们决定在 1.26 版本中将该功能升级为稳定版,这是我们长期承诺的一部分,即让 Job API 成为在 Kubernetes 集群中运行大型批处理 Job 的最佳方式。

要了解有关该功能的更多信息,您可以阅读 KEP

致谢

与任何 Kubernetes 特性一样,许多人都为完成这项工作做出了贡献,从测试、提交错误到代码审查。

我谨代表 SIG Apps,特别感谢 Jordan Liggitt (Google) 帮助我调试并为不止一个竞争条件构思解决方案,以及 Maciej Szulik (Red Hat) 的详尽审查。