本文已发表超过一年。较旧的文章可能包含过时内容。请检查页面中的信息自发布以来是否已发生变化。

Kubernetes 1.28:改进的 Job 故障处理

本文讨论了 Kubernetes 1.28 中用于改进批处理用户 Jobs 的两项新功能:Pod 替换策略(Pod replacement policy)每索引回退限制(Backoff limit per index)

这些功能延续了Pod 故障策略(Pod failure policy)开始的努力,以改进 Job 中 Pod 故障的处理。

Pod 替换策略

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

两个 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 也可能在资源匮乏或预算紧张的集群中引发问题,例如:

  • 对于待调度的 Pods 来说,集群资源可能难以获得,因为 Kubernetes 可能需要很长时间才能找到可用节点,直到现有 Pods 完全终止。
  • 如果启用了集群自动伸缩器(cluster autoscaler),替换 Pods 可能会导致不期望的扩容。

如何使用?

这是一项 Alpha 功能,您可以通过在集群中启用 JobPodReplacementPolicy 功能门(feature gate)来开启。

在集群中启用该功能后,您可以通过创建一个新的 Job 并指定 podReplacementPolicy 字段来使用它,示例如下:

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

在该 Job 中,Pods 只会在达到 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 中正在运行的 Pods 的配额,直到从当前终止的 Job 中回收资源。

请注意,当使用自定义Pod 故障策略(Pod failure policy)时,podReplacementPolicy: Failed 是默认值。

每索引回退限制(Backoff limit per index)

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

这对于希望独立处理每个索引的 Pod 故障的用例来说是有问题的。例如,如果您使用 Indexed Jobs 运行集成测试,其中每个索引对应一个测试套件。

在这种情况下,您可能希望考虑到可能的“不稳定测试”(flake tests),允许每个套件重试 1 或 2 次。可能存在一些有缺陷的套件,导致相应的索引持续失败。在这种情况下,您可能更倾向于限制有缺陷套件的重试次数,同时允许其他套件完成。

  • 该功能使您能够:
  • 完成所有索引的执行,即使某些索引失败。

如何使用?

通过避免对持续失败索引的不必要重试来更好地利用计算资源。

这是一项 Alpha 功能,您可以通过在集群中启用 JobBackoffLimitPerIndex 功能门(feature gate)来开启。

在集群中启用该功能后,您可以创建一个指定 .spec.backoffLimitPerIndex 字段的 Indexed Job。

示例

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 执行所有索引(前提是没有其他原因导致 Job 提前终止,例如达到 activeDeadlineSeconds 超时或被用户手动删除),并且每次失败都按索引进行控制。

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

现在,Job 完成后检查 Pods:

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

返回类似以下输出:

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

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

  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 将被标记为失败,而不会启动某些更高的索引。

阅读Pod Replacement PolicyBackoff limit per indexPod failure policy 的 KEP。

参与贡献

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

致谢

与任何 Kubernetes 功能一样,多个人为此付出了贡献,从测试和提交错误到代码评审。