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

Kubernetes 1.28:改进了 Job 的故障处理

这篇博客讨论了 Kubernetes 1.28 中为改进批处理用户的 Job 而引入的两个新特性:Pod 替换策略逐索引的回退限制

这些特性延续了Pod 故障策略所开启的努力,旨在改进 Job 中 Pod 故障的处理方式。

Pod 替换策略

默认情况下,当一个 Pod 进入终止(terminating)状态(例如,由于抢占或驱逐)时,Kubernetes 会立即创建一个替换 Pod。因此,两个 Pod 会同时运行。在 API 术语中,当 Pod 具有 deletionTimestamp 并且其阶段(phase)为 PendingRunning 时,该 Pod 就被视为正在终止。

在给定时间点有两个 Pod 正在运行的场景对于一些流行的机器学习框架来说是有问题的,例如 TensorFlow 和 JAX,它们要求对于给定的索引,在同一时间最多只有一个 Pod 在运行。如果某个索引有两个 Pod 在运行,TensorFlow 会给出以下错误。

 /job:worker/task:4: Duplicate task registration with task_name=/job:worker/replica:0/task:4

更多详情请参见 (issue)。

在前一个 Pod 完全终止之前创建替换 Pod,也可能在资源稀缺或预算紧张的集群中引起问题,例如:

  • 对于等待调度的 Pod 来说,集群资源可能很难获取,因为在现有 Pod 完全终止之前,Kubernetes 可能需要很长时间才能找到可用的节点。
  • 如果启用了集群自动扩缩器,替换的 Pod 可能会产生不希望的扩容。

如何使用它?

这是一个 Alpha 特性,你可以通过在集群中启用 JobPodReplacementPolicy 特性门控来开启它。

一旦在你的集群中启用了该特性,你就可以通过创建一个新的 Job 来使用它,其中指定了 podReplacementPolicy 字段,如下所示:

kind: Job
metadata:
  name: new
  ...
spec:
  podReplacementPolicy: Failed
  ...

在该 Job 中,Pod 只有在达到 Failed 阶段时才会被替换,而不是在它们正在终止时。

此外,你可以检查 Job 的 .status.terminating 字段。该字段的值是 Job 拥有的当前正在终止的 Pod 的数量。

kubectl get jobs/myjob -o=jsonpath='{.items[*].status.terminating}'
3 # three Pods are terminating and have not yet reached the Failed phase

这对于外部排队控制器(例如 Kueue)特别有用,它可以跟踪一个 Job 正在运行的 Pod 的配额,直到从当前正在终止的 Job 中回收资源为止。

请注意,当使用自定义的Pod 故障策略时,podReplacementPolicy: Failed 是默认设置。

逐索引的回退限制

默认情况下,Indexed Job 的 Pod 故障会计入全局重试限制,由 .spec.backoffLimit 表示。这意味着,如果有一个持续失败的索引,它会反复重启,直到耗尽限制。一旦达到限制,整个 Job 就会被标记为失败,并且某些索引可能从未启动过。

这对于希望独立处理每个索引的 Pod 故障的用例是有问题的。例如,如果你使用 Indexed Job 来运行集成测试,其中每个索引对应一个测试套件。在这种情况下,你可能希望考虑到可能的不稳定测试,允许每个套件重试 1 或 2 次。可能会有一些有问题的套件,导致相应的索引持续失败。在这种情况下,你可能更倾向于限制有问题的套件的重试次数,同时允许其他套件完成。

该特性允许你:

  • 尽管某些索引失败,但仍能完成所有索引的执行。
  • 通过避免对持续失败的索引进行不必要的重试,更好地利用计算资源。

如何使用它?

这是一个 Alpha 特性,你可以通过在集群中启用 JobBackoffLimitPerIndex 特性门控来开启它。

一旦在你的集群中启用了该特性,你就可以创建一个指定了 .spec.backoffLimitPerIndex 字段的 Indexed Job。

示例

以下示例演示了如何使用此特性来确保 Job 执行所有索引(前提是没有其他原因导致 Job 提前终止,例如达到 activeDeadlineSeconds 超时或被用户手动删除),并且失败次数是按每个索引控制的。

apiVersion: batch/v1
kind: Job
metadata:
  name: job-backoff-limit-per-index-execute-all
spec:
  completions: 8
  parallelism: 2
  completionMode: Indexed
  backoffLimitPerIndex: 1
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: example # this example container returns an error, and fails,
                      # when it is run as the second or third index in any Job
                      # (even after a retry)        
        image: python
        command:
        - python3
        - -c
        - |
          import os, sys, time
          id = int(os.environ.get("JOB_COMPLETION_INDEX"))
          if id == 1 or id == 2:
            sys.exit(1)
          time.sleep(1)          

现在,在 Job 完成后检查 Pod:

kubectl get pods -l job-name=job-backoff-limit-per-index-execute-all

返回类似以下的输出:

NAME                                              READY   STATUS      RESTARTS   AGE
job-backoff-limit-per-index-execute-all-0-b26vc   0/1     Completed   0          49s
job-backoff-limit-per-index-execute-all-1-6j5gd   0/1     Error       0          49s
job-backoff-limit-per-index-execute-all-1-6wd82   0/1     Error       0          37s
job-backoff-limit-per-index-execute-all-2-c66hg   0/1     Error       0          32s
job-backoff-limit-per-index-execute-all-2-nf982   0/1     Error       0          43s
job-backoff-limit-per-index-execute-all-3-cxmhf   0/1     Completed   0          33s
job-backoff-limit-per-index-execute-all-4-9q6kq   0/1     Completed   0          28s
job-backoff-limit-per-index-execute-all-5-z9hqf   0/1     Completed   0          28s
job-backoff-limit-per-index-execute-all-6-tbkr8   0/1     Completed   0          23s
job-backoff-limit-per-index-execute-all-7-hxjsq   0/1     Completed   0          22s

此外,你可以查看该 Job 的状态:

kubectl get jobs job-backoff-limit-per-index-fail-index -o yaml

输出以类似以下的 status 结尾:

  status:
    completedIndexes: 0,3-7
    failedIndexes: 1,2
    succeeded: 6
    failed: 4
    conditions:
    - message: Job has failed indexes
      reason: FailedIndexes
      status: "True"
      type: Failed

在这里,索引 12 都各自重试了一次。在它们各自第二次失败后,指定的 .spec.backoffLimitPerIndex 被超过,因此重试被停止。作为比较,如果禁用了逐索引的回退,那么有问题的索引会一直重试,直到超过全局的 backoffLimit,然后整个 Job 会被标记为失败,而一些更高编号的索引可能还未启动。

如何了解更多?

参与其中

这些特性由 SIG Apps 赞助。在批处理工作组中,我们正积极为 Kubernetes 用户改进批处理用例。工作组是专注于特定目标的相对短期的倡议。WG Batch 的目标是改善批处理工作负载用户的体验,为批处理用例提供支持,并针对常见用例增强 Job API。如果你对此感兴趣,请通过订阅我们的邮件列表或在 Slack 上加入我们的工作组。

致谢

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

如果没有 Aldo Culquicondor (Google) 在整个 Kubernetes 生态系统中提供卓越的领域知识和专业技术,我们无法实现这两项特性。