Job 与 CronJob 控制器
接下来给大家介绍另外一类资源对象:Job,我们在日常的工作中经常都会遇到一些需要进行批量数据处理和分析的需求,当然也会有按时间来进行调度的工作,在我们的 Kubernetes 集群中为我们提供了Job 和 CronJob 两种资源对象来应对我们的这种需求。
Job 负责处理任务,即仅执行一次的任务,它保证批处理任务的一个或多个 Pod 成功结束。而CronJob 则就是在 Job上加上了时间调度。
Job
我们用 Job 这个资源对象来创建一个如下所示的任务,该任务负责计算 π 到小数点后 2000 位,并将结果打印出来,此计算大约需要 10 秒钟完成。对应的资源清单如下所示:
# job-pi.yaml
# 这是一个 Kubernetes Job 示例,用来计算圆周率的前 2000 位
apiVersion: batch/v1 # API 版本,Job 属于 batch/v1
kind: Job # 资源类型:Job
metadata:name: pi
spec:template:spec:containers:- name: piimage: dockerpull.pw/perl:5.34.0command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]# command:# perl → 调用 Perl 解释器# -Mbignum=bpi → 加载 bignum 模块,支持大数计算,提供 bpi() 函数# -wle → Perl 参数,w=警告,l=自动换行,e=执行代码# "print bpi(2000)" → 打印圆周率 π 的前 2000 位restartPolicy: Never # Pod 失败时不会自动重启(Job 会根据策略重试)backoffLimit: 4 # Job 失败时的最大重试次数,超过就标记为失败
我们可以看到 Job 中也是一个 Pod 模板,和之前的 Deployment、StatefulSet 之类的是一致的,只是 Pod 中的容器要求是一个任务,而不是一个常驻前台的进程了,因为需要退出,另外值得注意的是 Job 的 RestartPolicy 仅支持 Never 和 OnFailure 两种,不支持 Always,我们知道 Job 就相当于来执行一个批处理任务,执行完就结束了,如果支持 Always 的话是不是就陷入了死循环了?
验证:
ubuntu@ubuntu:~$ kubectl get pod -l job-name=pi -w
NAME READY STATUS RESTARTS AGE
pi-mt84b 0/1 Pending 0 0s
pi-mt84b 0/1 Pending 0 0s
pi-mt84b 0/1 ContainerCreating 0 0s
pi-mt84b 1/1 Running 0 25s
pi-mt84b 0/1 Completed 0 27s
pi-mt84b 0/1 Completed 0 28s
pi-mt84b 0/1 Completed 0 29s
ubuntu@ubuntu:~$ kubectl describe job pi
Name: pi
Namespace: default
Selector: batch.kubernetes.io/controller-uid=a0fc2d20-ac95-4512-bba2-28e19ba34ff5
Labels: name=pi
Annotations: <none>
Parallelism: 1
Completions: 1
Completion Mode: NonIndexed
Suspend: false
Backoff Limit: 4
Start Time: Fri, 12 Sep 2025 03:00:07 +0000
Completed At: Fri, 12 Sep 2025 03:00:12 +0000
Duration: 5s
Pods Statuses: 0 Active (0 Ready) / 1 Succeeded / 0 Failed
Pod Template:Labels: batch.kubernetes.io/controller-uid=a0fc2d20-ac95-4512-bba2-28e19ba34ff5batch.kubernetes.io/job-name=picontroller-uid=a0fc2d20-ac95-4512-bba2-28e19ba34ff5job-name=piContainers:pi:Image: dockerpull.pw/perl:5.34.0Port: <none>Host Port: <none>Command:perl-Mbignum=bpi-wleprint bpi(2000)Environment: <none>Mounts: <none>Volumes: <none>Node-Selectors: <none>Tolerations: <none>
Events:Type Reason Age From Message---- ------ ---- ---- -------Normal SuccessfulCreate 15s job-controller Created pod: pi-t9sc5Normal Completed 10s job-controller Job completed
ubuntu@ubuntu:~$ kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
default pi-t9sc5 0/1 Completed 0 80s
ubuntu@ubuntu:~$ kubectl logs pi-t9sc5
3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989380952572010654858632788659361533818279682303019520353018529689957736225994138912497217752834791315155748572424541506959508295331168617278558890750983817546374649393192550604009277016711390098488240128583616035637076601047101819429555961989467678374494482553797747268471040475346462080466842590694912933136770289891521047521620569660240580381501935112533824300355876402474964732639141992726042699227967823547816360093417216412199245863150302861829745557067498385054945885869269956909272107975093029553211653449872027559602364806654991198818347977535663698074265425278625518184175746728909777727938000816470600161452491921732172147723501414419735685481613611573525521334757418494684385233239073941433345477624168625189835694855620992192221842725502542568876717904946016534668049886272327917860857843838279679766814541009538837863609506800642251252051173929848960841284886269456042419652850222106611863067442786220391949450471237137869609563643719172874677646575739624138908658326459958133904780275901
ubuntu@ubuntu:~$
可以看到,Job 对象在创建后,它的 Pod 模板,被自动加上了一个 controller-uid=< 一个随机字符串 > 这样的Label 标签,而这个 Job 对象本身,则被自动加上了这个 Label 对应的 Selector,从而保证了 Job 与它所管理的Pod 之间的匹配关系。而 Job 控制器之所以要使用这种携带了 UID 的 Label,就是为了避免不同 Job 对象所管理的Pod 发生重合。
上面我们这里的 Job 任务对应的 Pod 在运行结束后,会变成 Completed 状态,但是如果执行任务的 Pod 因为某种原因一直没有结束怎么办呢?同样我们可以在 Job 对象中通过设置字段 spec.activeDeadlineSeconds 来限制任务运行的最长时间,比如:
spec:activeDeadlineSeconds: 100
那么当我们的任务 Pod 运行超过了 100s 后,这个 Job 的所有 Pod 都会被终止,并且的终止原因会变成DeadlineExceeded。
如果的任务执行失败了,会怎么处理呢,这个和定义的restartPolicy 有关系,比如定义如下所示的 Job 任务,定义restartPolicy: Never 的重启策略:
# job-failed-demo.yaml
apiVersion: batch/v1
kind: Job
metadata:name: job-failed-demo
spec:template:spec:containers:- name: test-jobimage: dockerpull.pw/busybox# 故意输错命令command: ["echo123", "test failed job!"]restartPolicy: Never
验证:
ubuntu@ubuntu:~$ kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
default job-failed-demo-2cmr7 0/1 StartError 0 55s
default job-failed-demo-mh47x 0/1 StartError 0 31s
default job-failed-demo-v4s4c 0/1 StartError 0 68s
kube-flannel kube-flannel-ds-67vbh 1/1 Running 9 (91m ago) 8d
kube-flannel kube-flannel-ds-ghhhg 1/1 Running 8 (91m ago) 8d
kube-flannel kube-flannel-ds-w666x 1/1 Running 8 (91m ago) 8d
kube-system coredns-674b8bbfcf-6rjbh 1/1 Running 9 (91m ago) 8d
kube-system coredns-674b8bbfcf-s85b7 1/1 Running 9 (91m ago) 8d
kube-system etcd-master 1/1 Running 12 (91m ago) 8d
kube-system kube-apiserver-master 1/1 Running 11 (91m ago) 8d
kube-system kube-controller-manager-master 1/1 Running 11 (91m ago) 8d
kube-system kube-proxy-6bgf7 1/1 Running 9 (91m ago) 8d
kube-system kube-proxy-ddms4 1/1 Running 10 (91m ago) 8d
kube-system kube-proxy-hrqpb 1/1 Running 9 (91m ago) 8d
kube-system kube-scheduler-master 1/1 Running 12 (91m ago) 8d
ubuntu@ubuntu:~$ kubectl describe pod job-failed-demo-v4s4c
...
Events:Type Reason Age From Message---- ------ ---- ---- -------Normal Scheduled 113s default-scheduler Successfully assigned default/job-failed-demo-v4s4c to node2Normal Pulling 113s kubelet Pulling image "busybox"Normal Pulled 110s kubelet Successfully pulled image "busybox" in 2.938s (2.938s including waiting). Image size: 2223685 bytes.Normal Created 110s kubelet Created container: test-jobWarning Failed 110s kubelet Error: failed to create containerd task: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "echo123": executable file not found in $PATH: unknown
...
可以看到当我们设置成 Never 重启策略的时候,Job 任务执行失败后会不断创建新的 Pod,但是不会一直创建下去,会根据 spec.backoffLimit 参数进行限制,通过该字段可以定义重建 Pod 的次数。
但是如果我们设置的 restartPolicy: OnFailure 重启策略,则当 Job 任务执行失败后不会创建新的 Pod 出来,只会不断重启 Pod,比如将上面的 Job 任务 restartPolicy 更改为 OnFailure 后查看 Pod:
验证:
ubuntu@ubuntu:~/example/job_cron_job$ kubectl get pod -l job-name=job-failed-demo -w
NAME READY STATUS RESTARTS AGE
job-failed-demo-vg87j 0/1 RunContainerError 3 (8s ago) 65s
job-failed-demo-vg87j 0/1 CrashLoopBackOff 3 (14s ago) 71s
job-failed-demo-vg87j 0/1 RunContainerError 4 (1s ago) 100s
job-failed-demo-vg87j 0/1 CrashLoopBackOff 4 (13s ago) 112s
除此之外,我们还可以通过设置 spec.parallelism 参数来进行并行控制,该参数定义了一个 Job 在任意时间最多可以有多少个 Pod 同时运行。并行性请求(.spec.parallelism)可以设置为任何非负整数,如果未设置,则默认为 1,如果设置为 0,则 Job 相当于启动之后便被暂停,直到此值被增加。
spec.completions 参数可以定义 Job 至少要完成的 Pod 数目。如下所示创建一个新的 Job 任务,设置允许并行数为 2,至少要完成的 Pod 数为 8:
# job-para-demo.yaml
# 这是一个并行 Job 示例,运行 8 个任务,每次并行执行 2 个 PodapiVersion: batch/v1 # API 版本,Job 属于 batch/v1
kind: Job # 资源类型:Job
metadata:name: job-para-test # Job 的名字,唯一标识
spec:parallelism: 2 # 并行 Pod 数量,Job 会同时运行 2 个 Podcompletions: 8 # Job 总共完成 8 个 Pod 任务,完成后 Job 状态为 Completetemplate: # Pod 模板,定义 Job 创建的 Podspec:containers: # Pod 内的容器列表- name: test-job # 容器名称image: busybox # 使用的镜像command: ["echo", "test parallel job!"] # 容器启动时执行的命令restartPolicy: Never # Pod 容器失败时不重启,由 Job 控制重试
验证:
ubuntu@ubuntu:~$ kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
default job-para-test-2jsts 0/1 Completed 0 109s
default job-para-test-2w68f 0/1 Completed 0 104s
default job-para-test-jlg2j 0/1 Completed 0 114s
default job-para-test-p522l 0/1 Completed 0 2m5s
default job-para-test-rggv6 0/1 Completed 0 2m
default job-para-test-rpkqc 0/1 Completed 0 93s
default job-para-test-trkqh 0/1 Completed 0 99s
default job-para-test-wg2tv 0/1 Completed 0 4s
可以看到一次可以有 2 个 Pod 同时运行,需要 8 个 Pod 执行成功,如果不是 8 个成功,那么会根据 restartPolicy 的策略进行处理,可以认为是一种检查机制。
此外带有确定完成计数的 Job,即 .spec.completions 不为 null 的 Job, 都可以在其.spec.completionMode 中设置完成模式:
- NonIndexed(默认值):当成功完成的 Pod 个数达到 .spec.completions 所设值时认为 Job 已经完成。换言之,每个 Job 完成事件都是独立无关且同质的。要注意的是,当 .spec.completions 取值为 null 时,Job被默认处理为 NonIndexed 模式。
- Indexed:Job 的 Pod 会获得对应的完成索引,取值为 0 到 .spec.completions-1,该索引可以通过三种方式获取:
- Pod 的注解 batch.kubernetes.io/job-completion-index。
- 作为 Pod 主机名的一部分,遵循模式 $(job-name)-$(index)。当你同时使用带索引的 Job(IndexedJob)与服务(Service),Job 中的 Pod 可以通过 DNS 使用确切的主机名互相寻址。
- 对于容器化的任务,在环境变量 JOB_COMPLETION_INDEX 中可以获得。
当每个索引都对应一个成功完成的 Pod 时,Job 被认为是已完成的。
索引完成模式
下面我们将运行一个使用多个并行工作进程的 Kubernetes Job,每个 worker 都是在自己的 Pod 中运行的不同容器。Pod 具有控制平面自动设置的索引编号(index number), 这些编号使得每个 Pod 能识别出要处理整个任务的哪个部分。
Pod 索引在注解 **batch.kubernetes.io/job-completion-index **中呈现,具体表示为一个十进制值字符串。为了让容器化的任务进程获得此索引,我们可以使用 downward API 机制来获取注解的值。而且控制平面会自动设置 DownwardAPI 在 JOB_COMPLETION_INDEX 环境变量中暴露索引。
配置清单
# job-indexed.yaml
# 这是一个 Indexed Job 示例,每个 Pod 会有一个唯一的索引 $JOB_COMPLETION_INDEX
apiVersion: batch/v1
kind: Job
metadata:name: indexed-job # Job 名称
spec:completions: 5 # Job 总共需要完成 5 个 Podparallelism: 3 # Job 并行运行的 Pod 数量为 3 改成0就是串行了completionMode: Indexed # 启用 Indexed 模式,每个 Pod 会有唯一索引 $JOB_COMPLETION_INDEXtemplate:spec:restartPolicy: Never # Pod 容器失败时不重启,由 Job 控制重试# 初始化容器,用于生成每个 Pod 对应的数据initContainers:- name: "input"image: "bash" # 用 bash 镜像执行脚本command:- "bash"- "-c"- |# 根据 Pod 索引生成不同的数据items=(foo bar baz qux xyz)echo ${items[$JOB_COMPLETION_INDEX]} > /input/data.txtvolumeMounts:- mountPath: /input # 挂载共享目录name: input# 主容器,用于处理 initContainer 生成的数据containers:- name: "worker"image: "busybox"command:- "rev"- "/input/data.txt" # 将 initContainer 文件内容反转输出!!!volumeMounts:- mountPath: /inputname: input# Pod 使用的卷volumes:- name: inputemptyDir: {} # 临时目录,Pod 生命周期结束即销毁
验证:
ubuntu@ubuntu:~$ kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
default indexed-job-0-ddfbq 0/1 Completed 0 81s
default indexed-job-1-kvwpr 0/1 Completed 0 81s
default indexed-job-2-frnwp 0/1 Completed 0 6s
default indexed-job-3-q86zs 0/1 Completed 0 62s
default indexed-job-4-4r77m 0/1 Completed 0 59s
ubuntu@ubuntu:~$ kubectl get job
NAME STATUS COMPLETIONS DURATION AGE
indexed-job Complete 5/5 83s 100s
ubuntu@ubuntu:~$ ubuntu@ubuntu:~$ kubectl logs indexed-job-1-kvwpr
Defaulted container "worker" out of: worker, input (init)
rab
ubuntu@ubuntu:~$
当 JOB_COMPLETION_INDEX=3 的时候表示我们将 items[3] 的 qux 值写入到了 /input/data.txt 文件中,然后通过 volume 共享,在主容器中我们通过 rev 命令将其反转,所以输出结果就位 xuq 了。
上面我们这个示例中每个 Pod 只做一小部分工作(反转一个字符串)。 在实际工作中肯定比这复杂,比如你可能会创建一个基于场景数据制作 60 秒视频任务的 Job,此视频渲染 Job 中的每个工作项都将渲染该视频剪辑的特定帧,索引完成模式意味着 Job 中的每个 Pod 都知道通过从剪辑开始计算帧数,来确定渲染和发布哪一帧,这样就可以大大提高工作任务的效率。
CronJob
CronJob 其实就是在 Job 的基础上加上了时间调度,我们可以在给定的时间点运行一个任务,也可以周期性地在给定时间点运行。这个实际上和我们 Linux 中的 crontab 就非常类似了。
一个 CronJob 对象其实就对应中 crontab 文件中的一行,它根据配置的时间格式周期性地运行一个 Job,格式和crontab 也是一样的。
所有 CronJob 的 schedule 时间都是基于 kube-controller-manager 的时区,如果你的控制平面在 Pod 中运行了 kube-controller-manager, 那么为该容器所设置的时区将会决定 CronJob 的控制器所使用的时区。官方并不支持设置如 CRON_TZ 或者 TZ 等变量,这两个变量是用于解析和计算下一个 Job 创建时间所使用的内部库中一个实现细节,不建议在生产集群中使用它。
但是如果在 kube-controller-manager 中启用了 CronJobTimeZone 这个 Feature Gates,那么我们就可以为CronJob 指定一个时区(如果你没有启用该特性门控,或者你使用的是不支持试验性时区功能的 Kubernetes 版本,集群中所有 CronJob 的时区都是未指定的)。启用该特性后,你可以将 spec.timeZone 设置为有效时区名称。
配置清单:
# cronjob-demo.yaml
# 这是一个 CronJob 示例,每分钟执行一次倒计时任务apiVersion: batch/v1
kind: CronJob
metadata:name: cronjob-demo # CronJob 名称
spec:schedule: "*/1 * * * *" # cron 表达式:每分钟执行jobTemplate: # Job 模板spec:template:spec:restartPolicy: OnFailure # Pod 失败时自动重启,成功则不重启containers:- name: hello # 容器名称image: busybox # 使用 busybox 镜像args:- "bin/sh"- "-c"- "for i in 9 8 7 6 5 4 3 2 1; do echo $i; done"# 执行循环命令:从 9 倒数到 1,每个数字打印一行
这里的 Kind 变成了 **CronJob **了,要注意的是 .spec.schedule 字段是必须填写的,用来指定任务运行的周期,格式就和 crontab 一样,另外一个字段是 .spec.jobTemplate , 用来指定需要运行的任务,格式当然和 Job 是一致的。还有一些值得我们关注的字段 .spec.successfulJobsHistoryLimit (默认为 3) 和
.spec.failedJobsHistoryLimit (默认为 1),表示历史限制,是可选的字段,指定可以保留多少完成和失败的Job。然而,当运行一个 CronJob 时,Job 可以很快就堆积很多,所以一般推荐设置这两个字段的值,如果设置限制的值为 0,那么相关类型的 Job 完成后将不会被保留。
验证:
ubuntu@ubuntu:~/example/job_cron_job$ kubectl get jobs
NAME STATUS COMPLETIONS DURATION AGE
cronjob-demo-29294269 Running 0/1 0s 0s
ubuntu@ubuntu:~/example/job_cron_job$ kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE
default cronjob-demo-29294283-zkvff 0/1 Completed 0 10s
ubuntu@ubuntu:~/example/job_cron_job$ kubectl logs -f cronjob-demo-29294283-zkvff
9
8
7
6
5
4
3
2
1
ubuntu@ubuntu:~/example/job_cron_job$
不过需要注意的是delete -f xxx.yaml这将会终止正在创建的 Job,但是运行中的 Job 将不会被终止,不会删除 Job 或 它们的 Pod。
那如果我们想要在每个节点上去执行一个 Job 或者 Cronjob 又该怎么来实现呢?
apiVersion: batch/v1
kind: Job
metadata:name: per-node-job # Job 名称
spec:completions: 3 # 总共需要完成 3 个 Pod 执行成功(对应节点数量)parallelism: 3 # 同时允许 3 个 Pod 并行执行template:spec:containers:- name: taskimage: busybox # 使用 busybox 镜像执行简单任务command: ["sh", "-c", "echo Hello from $(hostname)"]# 输出当前 Pod 所在节点的主机名restartPolicy: Neveraffinity:nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution:nodeSelectorTerms:- matchExpressions:- key: kubernetes.io/hostname # 节点标签 keyoperator: In # 节点名在下面的列表中values:- node1 # 节点1- node2 # 节点2- node3 # 节点3
# 解释:
# 1. 通过 nodeAffinity 将 Pod 调度到指定节点
# 2. completions:3 + parallelism:3 保证每个节点一个 Pod 并行运行
# 3. restartPolicy:Never 表示 Pod 执行失败不重启