运行 Kubernetes Job 和 CronJob

KubernetesBeginner
立即练习

简介

Deployment 的职责是让服务持续运行,而批处理任务的成功条件有所不同:执行至完成、记录结果,然后停止。Kubernetes 使用 Job 表示一次性任务,使用 CronJob 表示按计划重复执行的任务。

你将运行一个 Job,检查它的 Pod 和日志,配置受控并行任务,创建一个 CronJob,并了解如何通过暂停和历史记录限制来管理周期性任务。

可以把本实验看成对三种「成功条件」的比较。Deployment 通过持续保持副本可用来实现成功;Job 则要求指定数量的 Pod 以退出码 0 完成;CronJob 的成功体现在按预定时间创建 Job。分清这三个层次后,YAML 的结构会更容易理解。

运行一次性 Job

在本步骤中,你将创建一个 Job,让它的 Pod 输出一份报告并成功退出。使用 here-document 创建清单;带引号的 EOF 可以防止 Shell 展开 YAML 中的字符。

在输入 YAML 之前,先了解它的结构:kind: Job 用于选择控制器,spec.template 描述控制器要创建的 Pod,而 Pod 中包含一个短生命周期容器。与 Deployment 模板不同,Job Pod 必须使用 restartPolicy: Never 或 OnFailure,因为任务成功后就应该停止。

cd /home/labex/project/batch-workloads
cat <<'EOF' > report-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: report-job
  namespace: practice-batch
spec:
  backoffLimit: 2
  template:
    metadata:
      labels:
        app: report-job
    spec:
      restartPolicy: Never
      containers:
        - name: reporter
          image: busybox:1.36
          imagePullPolicy: IfNotPresent
          command: ["sh", "-c", "echo report-complete"]
EOF

先让 Kubernetes 在本地验证文件。--dry-run=client 会检查对象结构,但不会真正创建资源。每次编写新清单时,都建议养成这个习惯:

kubectl apply --dry-run=client -f report-job.yaml

如果结果为 job.batch/report-job created (dry run),说明文件结构有效。接下来正式应用它,然后等待 Job 的 Complete 条件,而不是猜测它需要运行多久:

kubectl apply -f report-job.yaml
kubectl wait --for=condition=Complete job/report-job -n practice-batch --timeout=60s

restartPolicy: Never 让 Job 控制器决定是否需要创建其他 Pod。backoffLimit: 2 将失败重试次数限制为 2 次。通过控制器读取已完成 Pod 的输出:

kubectl logs job/report-job -n practice-batch

你应该会看到 report-complete。指定 job/report-job 后,kubectl 会自动找到该 Job 对应的 Pod,因此不需要复制 Pod 自动生成的名称。

这三个命令展示了一个简洁、清晰的生命周期:

job.batch/report-job created
job.batch/report-job condition met
report-complete

condition met 表示 Job 控制器已经记录任务完成;下一行日志则是应用程序输出。在刚启动的本地集群中,即使这样一个很小的 Job,也可能因为网络组件正在稳定而耗时数秒。等待条件满足可以避免依赖固定的休眠时长,让流程更加可靠。

追踪 Job 的所有关系和完成状态

在本步骤中,你将比较已完成的 Job 和持续运行的 Deployment。已完成的 Job 会保留其 Pod,因此你仍然可以检查日志和终止状态。

先查看常规表格。Job 应显示一次成功完成,而它的 Pod 在不同列中可能显示为 Completed 或 Succeeded:

kubectl get jobs,pods -n practice-batch
kubectl get pods -n practice-batch -l job-name=report-job -o custom-columns='POD:.metadata.name,PHASE:.status.phase,OWNER:.metadata.ownerReferences[0].name,EXIT_CODE:.status.containerStatuses[0].state.terminated.exitCode'
kubectl describe job report-job -n practice-batch

Pod 阶段应为 Succeeded,其所有者应为 report-job,退出码应为 0。当所需数量的 Pod 成功完成后,Job 就算完成;它不会为了保持运行而重新启动已经成功退出的容器。

自定义列查询将四个嵌套字段整理成一张简洁的证据表。当你只想回答「这个 Pod 由谁管理,以及其中的进程是否成功?」时,这比阅读完整的 Pod YAML 更实用。

测试结果如下:

POD                PHASE       OWNER        EXIT_CODE
report-job-cd54c   Succeeded   report-job   0

自动生成的后缀会有所不同。按行查看这些字段:OWNER=report-job 表明该 Pod 由对应控制器管理,而 PHASE=Succeeded 加上 EXIT_CODE=0 则证明进程已成功完成。

并行运行多个完成任务

在本步骤中,你将创建一个 Job,要求 4 个 Pod 成功完成,同时最多允许其中 2 个并行运行。

在编写清单之前,需要先理解两个相互独立的数值:

  • completions: 4 表示总共需要完成 4 个成功的工作单元。
  • parallelism: 2 表示 Kubernetes 同时最多运行其中 2 个工作单元。

completionMode: Indexed 会为工作单元分配 0 到 3 的索引。每个 Pod 都会通过 JOB_COMPLETION_INDEX 环境变量获得自己的索引,这样实际的批处理程序就可以划分数据集,避免所有 Pod 重复处理同一分区。

cat <<'EOF' > indexed-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: indexed-job
  namespace: practice-batch
spec:
  completionMode: Indexed
  completions: 4
  parallelism: 2
  backoffLimitPerIndex: 1
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: worker
          image: busybox:1.36
          imagePullPolicy: IfNotPresent
          command: ["sh", "-c", "echo worker-index=${JOB_COMPLETION_INDEX}; sleep 2"]
EOF

应用清单,并等待全部 4 个索引完成:

最后一条命令使用 -L,将 batch.kubernetes.io/job-completion-index 标签的值作为额外的表格列。这样无需打开完整 YAML,就能看到每个自动生成的 Pod 对应哪个工作索引。

kubectl apply -f indexed-job.yaml
kubectl wait --for=condition=Complete job/indexed-job -n practice-batch --timeout=90s
kubectl get pods -n practice-batch -l job-name=indexed-job -L batch.kubernetes.io/job-completion-index

completions 表示成功完成的工作总数,parallelism 表示可同时运行的 Pod 最大数量。索引式完成会通过 JOB_COMPLETION_INDEX 为每个 Pod 提供稳定的索引。

最终表格应包含 4 个已完成的 Pod,并且索引标签各不相同。它们不一定按数字顺序显示,因为任务是并发完成的,完成顺序并不固定。

例如:

NAME                  STATUS      JOB-COMPLETION-INDEX
indexed-job-0-gw5wq   Completed   0
indexed-job-1-vw4qk   Completed   1
indexed-job-2-mpvnr   Completed   2
indexed-job-3-8z5j8   Completed   3

看到 4 行并不意味着 4 个 Pod 同时运行。4 个索引证明总工作量已经完成,而 parallelism: 2 则限制了 Kubernetes 同时运行的 Pod 数量。

调度 CronJob

在本步骤中,你将创建一个 CronJob,每分钟启动一个 Job。Cron 语法包含 5 个字段:分钟、小时、日、月和星期。

调度表达式 */1 * * * * 表示「每分钟执行一次」。这里特意设置得比较频繁,方便你在短时间内观察效果;生产环境中的调度通常不会如此频繁。清单中还包含嵌套的 jobTemplate,因为 CronJob 会创建 Job,而每个 Job 随后又会创建 Pod。

cat <<'EOF' > heartbeat-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: heartbeat
  namespace: practice-batch
spec:
  schedule: "*/1 * * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 2
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: heartbeat
              image: busybox:1.36
              imagePullPolicy: IfNotPresent
              command: ["sh", "-c", "date -u; echo heartbeat-complete"]
EOF

应用调度配置并查看其摘要:

kubectl apply -f heartbeat-cronjob.yaml
kubectl get cronjob heartbeat -n practice-batch

concurrencyPolicy: Forbid 可以防止新的运行任务与之前尚未完成的任务重叠。你可以根据 CronJob 模板立即创建一个 Job,无需等待下一分钟:

kubectl create job --from=cronjob/heartbeat heartbeat-manual -n practice-batch
kubectl wait --for=condition=Complete job/heartbeat-manual -n practice-batch --timeout=60s
kubectl logs job/heartbeat-manual -n practice-batch

日志中会包含一个 UTC 时间戳,后面跟着 heartbeat-complete。手动创建的 Job 使用相同的模板,但不会改变 CronJob 未来的调度计划。

典型输出如下:

job.batch/heartbeat-manual condition met
Thu Aug 13 07:48:29 UTC 2026
heartbeat-complete

你的时间戳会有所不同。注意,容器使用的是 UTC,因为命令中指定了 date -u;这样可以避免将集群调度时间与终端所在的本地时区混淆。

暂停和恢复计划任务

在本步骤中,你将暂停 CronJob,而不会删除它的定义。在维护或处理故障时,暂停功能非常有用。

kubectl patch 只会修改 JSON 负载中指定的字段。这里将 spec.suspend 设置为 true;CronJob 对象、调度配置以及已经存在的 Job 都会保持不变。

kubectl patch cronjob heartbeat -n practice-batch --type=merge -p '{"spec":{"suspend":true}}'
kubectl get cronjob heartbeat -n practice-batch -o custom-columns='NAME:.metadata.name,SCHEDULE:.spec.schedule,SUSPEND:.spec.suspend,LAST_SCHEDULE:.status.lastScheduleTime'

已经存在的 Job 会继续运行;暂停只会阻止新的计划任务创建。检查完成后恢复 CronJob:

kubectl patch cronjob heartbeat -n practice-batch --type=merge -p '{"spec":{"suspend":false}}'
kubectl get jobs -n practice-batch --sort-by=.metadata.creationTimestamp

现在你可以清楚地区分三个层次:CronJob 定义调度计划,每次计划运行都会创建一个 Job,而每个 Job 又会创建一个或多个 Pod。

由于整个练习可能在下一个整分钟到来之前就结束,LAST_SCHEDULE 仍可能显示为 <none>。这不是错误:heartbeat-manual 是通过 --from 按需创建的,而不是由 CronJob 调度器创建的。最后需要关注的是 SUSPEND=false,它表示未来的计划运行已经恢复。

总结

你使用 Job 执行一次性任务,使用索引式完成运行并行任务,并使用 CronJob 创建周期性调度。你还检查了控制器所有关系、日志、完成状态、并发策略、历史记录限制和暂停功能。理解这些生命周期差异,是选择正确 Kubernetes 工作负载控制器的关键。