# Job
Job 会创建一个或者多个 Pods,并将继续重试 Pods 的执行,直到指定数量的 Pods 成功终止。 随着 Pods 成功结束,Job 跟踪记录成功完成的 Pods 个数。 当数量达到指定的成功个数阈值时,任务(即 Job)结束。 删除 Job 的操作会清除所创建的全部 Pods。 挂起 Job 的操作会删除 Job 的所有活跃 Pod,直到 Job 被再次恢复执行。
一种简单的使用场景下,你会创建一个 Job 对象以便以一种可靠的方式运行某 Pod 直到完成。 当第一个 Pod 失败或者被删除(比如因为节点硬件失效或者重启)时,Job 对象会启动一个新的 Pod。
你也可以使用 Job 以并行的方式运行多个 Pod。
# 一、Job 使用
下面是一个 Job 配置示例。它负责计算 π 到小数点后 2000 位,并将结果打印出来。 此计算大约需要 10 秒钟完成。
apiVersion: batch/v1
kind: Job
metadata:
name: pi
spec:
template:
spec:
containers:
- name: pi
image: perl
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
restartPolicy: Never
backoffLimit: 4
1. 创建 Job
$ kubectl apply -f job.yaml
job.batch/pi created
2. 检查 Job 状态
$ kubectl describe jobs/pi
Name: pi
Namespace: default
Selector: controller-uid=25e92d1d-2204-47c9-a531-26067bc7b4f6
Labels: controller-uid=25e92d1d-2204-47c9-a531-26067bc7b4f6
job-name=pi
Annotations: <none>
Parallelism: 1
Completions: 1
Start Time: Mon, 28 Jun 2021 11:06:23 +0800
Pods Statuses: 1 Running / 0 Succeeded / 0 Failed
Pod Template:
Labels: controller-uid=25e92d1d-2204-47c9-a531-26067bc7b4f6
job-name=pi
Containers:
pi:
Image: perl
Port: <none>
Host Port: <none>
Command:
perl
-Mbignum=bpi
-wle
print bpi(2000)
Environment: <none>
Mounts: <none>
Volumes: <none>
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulCreate 42s job-controller Created pod: pi-xzbr2
3. 查看 Pod
要查看 Job 对应的已完成的 Pods,可以执行 kubectl get pods
:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
pi-xzbr2 0/1 Completed 0 71s
要以机器可读的方式列举隶属于某 Job 的全部 Pods,可以使用类似下面这条命令:
pods=$(kubectl get pods --selector=job-name=pi --output=jsonpath='{.items[*].metadata.name}')
echo $pods
输出:
pi-xzbr2
这里,选择算符与 Job 的选择算符相同。--output=jsonpath
选项给出了一个表达式, 用来从返回的列表中提取每个 Pod 的 name 字段。
4. 查看结果
$ kubectl logs $pods
3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989380952572010654858632788659361533818279682303019520353018529689957736225994138912497217752834791315155748572424541506959508295331168617278558890750983817546374649393192550604009277016711390098488240128583616035637076601047101819429555961989467678374494482553797747268471040475346462080466842590694912933136770289891521047521620569660240580381501935112533824300355876402474964732639141992726042699227967823547816360093417216412199245863150302861829745557067498385054945885869269956909272107975093029553211653449872027559602364806654991198818347977535663698074265425278625518184175746728909777727938000816470600161452491921732172147723501414419735685481613611573525521334757418494684385233239073941433345477624168625189835694855620992192221842725502542568876717904946016534668049886272327917860857843838279679766814541009538837863609506800642251252051173929848960841284886269456042419652850222106611863067442786220391949450471237137869609563643719172874677646575739624138908658326459958133904780275901
# 二、Job 场景
适合以 Job 形式来运行的任务主要有三种:
- 非并行 Job:
- 通常只启动一个 Pod,除非该 Pod 失败。
- 当 Pod 成功终止时,立即视 Job 为完成状态。
- 具有确定完成计数的并行 Job:
.spec.completions
字段设置为非 0 的正数值。- Job 用来代表整个任务,当成功的 Pod 个数达到
.spec.completions
时,Job 被视为完成。 - 当使用
.spec.completionMode="Indexed"
时,每个 Pod 都会获得一个不同的 索引值,介于 0 和.spec.completions-1
之间。
- 带工作队列的并行 Job:
- 不设置
spec.completions
,默认值为.spec.parallelism
。 - 多个 Pod 之间必须相互协调,或者借助外部服务确定每个 Pod 要处理哪个工作条目。 例如,任一 Pod 都可以从工作队列中取走最多 N 个工作条目。
- 每个 Pod 都可以独立确定是否其它 Pod 都已完成,进而确定 Job 是否完成。
- 当 Job 中 任何 Pod 成功终止,不再创建新 Pod。
- 一旦至少 1 个 Pod 成功完成,并且所有 Pod 都已终止,即可宣告 Job 成功完成。
- 一旦任何 Pod 成功退出,任何其它 Pod 都不应再对此任务执行任何操作或生成任何输出。 所有 Pod 都应启动退出过程。
- 不设置
对于 非并行 的 Job,你可以不设置 spec.completions
和 spec.parallelism
。 这两个属性都不设置时,均取默认值 1。
对于 确定完成计数 类型的 Job,你应该设置 .spec.completions
为所需要的完成个数。 你可以设置 .spec.parallelism
,也可以不设置。其默认值为 1。
对于一个 工作队列 Job,你不可以设置 .spec.completions
,但要将.spec.parallelism
设置为一个非负整数。
关于如何利用不同类型的 Job 的更多信息,请参见 「Job 模式」一节。
控制并行性
并行性请求(.spec.parallelism
)可以设置为任何非负整数。 如果未设置,则默认为 1。 如果设置为 0,则 Job 相当于启动之后便被暂停,直到此值被增加。
实际并行性(在任意时刻运行状态的 Pods 个数)可能比并行性请求略大或略小, 原因如下:
- 对于 确定完成计数 Job,实际上并行执行的 Pods 个数不会超出剩余的完成数。 如果
.spec.parallelism
值较高,会被忽略。 - 对于 工作队列 Job,有任何 Job 成功结束之后,不会有新的 Pod 启动。 不过,剩下的 Pods 允许执行完毕。
- 如果 Job 控制器没有来得及作出响应,或者如果 Job 控制器因为任何原因(例如,缺少
ResourceQuota
或者没有权限)无法创建 Pods。 Pods 个数可能比请求的数目小。 - Job 控制器可能会因为之前同一 Job 中 Pod 失效次数过多而压制新 Pod 的创建。
- 当 Pod 处于体面终止进程中,需要一定时间才能停止。
# 三、Job 模式
Job 对象可以用来支持多个 Pod 的可靠的并发执行。 Job 对象不是设计用来支持相互通信的并行进程的,后者一般在科学计算中应用较多。 Job 的确能够支持对一组相互独立而又有所关联的 工作条目 的并行处理。 这类工作条目可能是要发送的电子邮件、要渲染的视频帧、要编解码的文件、NoSQL 数据库中要扫描的主键范围等等。
在一个复杂系统中,可能存在多个不同的工作条目集合。这里我们仅考虑用户希望一起管理的 工作条目集合之一 — 批处理作业。
并行计算的模式有好多种,每种都有自己的强项和弱点。这里要权衡的因素有:
- 每个工作条目对应一个 Job 或者所有工作条目对应同一 Job 对象。 后者更适合处理大量工作条目的场景; 前者会给用户带来一些额外的负担,而且需要系统管理大量的 Job 对象。
- 创建与工作条目相等的 Pod 或者令每个 Pod 可以处理多个工作条目。 前者通常不需要对现有代码和容器做较大改动; 后者则更适合工作条目数量较大的场合,原因同上。
- 有几种技术都会用到工作队列。这意味着需要运行一个队列服务,并修改现有程序或容器 使之能够利用该工作队列。 与之比较,其他方案在修改现有容器化应用以适应需求方面可能更容易一些。
下面是对这些权衡的汇总,列 2 到 4 对应上面的权衡比较。 模式的名称对应了相关示例和更详细描述的链接。
模式 | 单个 Job 对象 | Pods 数少于工作条目数? | 直接使用应用无需修改? |
---|---|---|---|
每工作条目一 Pod 的队列 (opens new window) | ✓ | 有时 | |
Pod 数量可变的队列 (opens new window) | ✓ | ✓ | |
静态任务分派的带索引的 Job (opens new window) | ✓ | ✓ | |
Job 模版扩展 (opens new window) | ✓ |
当你使用 .spec.completions
来设置完成数时,Job 控制器所创建的每个 Pod 使用完全相同的 spec
(opens new window)。 这意味着任务的所有 Pod 都有相同的命令行,都使用相同的镜像和数据卷,甚至连 环境变量都(几乎)相同。 这些模式是让每个 Pod 执行不同工作的几种不同形式。
下表显示的是每种模式下 .spec.parallelism
和 .spec.completions
所需要的设置。 其中,W
表示的是工作条目的个数。
模式 | .spec.completions | .spec.parallelism |
---|---|---|
每工作条目一 Pod 的队列 (opens new window) | W | 任意值 |
Pod 个数可变的队列 (opens new window) | 1 | 任意值 |
静态任务分派的带索引的 Job (opens new window) | W | |
Job 模版扩展 (opens new window) | 1 | 应该为 1 |
# 四、Job 回退
Pod 中的容器可能因为多种不同原因失效,例如因为其中的进程退出时返回值非零, 或者容器因为超出内存约束而被杀死等等。 如果发生这类事件,并且 .spec.template.spec.restartPolicy = "OnFailure"
, Pod 则继续留在当前节点,但容器会被重新运行。 因此,你的程序需要能够处理在本地被重启的情况,或者要设置 .spec.template.spec.restartPolicy = "Never"
。
整个 Pod 也可能会失败,且原因各不相同。 例如,当 Pod 启动时,节点失效(被升级、被重启、被删除等)或者其中的容器失败而 .spec.template.spec.restartPolicy = "Never"
。 当 Pod 失败时,Job 控制器会启动一个新的 Pod。 这意味着,你的应用需要处理在一个新 Pod 中被重启的情况。 尤其是应用需要处理之前运行所产生的临时文件、锁、不完整的输出等问题。
注意,即使你将 .spec.parallelism
设置为 1,且将 .spec.completions
设置为 1,并且 .spec.template.spec.restartPolicy
设置为 "Never",同一程序仍然有可能被启动两次。
如果你确实将 .spec.parallelism
和 .spec.completions
都设置为比 1 大的值, 那就有可能同时出现多个 Pod 运行的情况。 为此,你的 Pod 也必须能够处理并发性问题。
Pod 回退失效策略:
在有些情形下,你可能希望 Job 在经历若干次重试之后直接进入失败状态,因为这很可能意味着遇到了配置错误。
为了实现这点,可以将 .spec.backoffLimit
设置为视 Job 为失败之前的重试次数。 失效回退的限制值默认为 6。 与 Job 相关的失效的 Pod 会被 Job 控制器重建,回退重试时间将会按指数增长 (从 10 秒、20 秒到 40 秒)最多至 6 分钟。 当 Job 的 Pod 被删除时,或者 Pod 成功时没有其它 Pod 处于失败状态,失效回退的次数也会被重置(为 0)。
# 五、Job 终止和清理
Job 完成时不会再创建新的 Pod,不过已有的 Pod 也不会被删除。 保留这些 Pod 使得你可以查看已完成的 Pod 的日志输出,以便检查错误、警告 或者其它诊断性输出。 Job 完成时 Job 对象也一样被保留下来,这样你就可以查看它的状态。 在查看了 Job 状态之后删除老的 Job 的操作留给了用户自己。 你可以使用 kubectl
来删除 Job。
例如:
kubectl delete jobs/pi
或者
kubectl delete -f ./job.yaml
当使用 kubectl
来删除 Job 时,该 Job 所创建的 Pods 也会被删除。
默认情况下,Job 会持续运行,除非某个 Pod 失败(restartPolicy=Never
) 或者某个容器出错退出(restartPolicy=OnFailure
)。 这时,Job 基于前述的 spec.backoffLimit
来决定是否以及如何重试。 一旦重试次数到达 .spec.backoffLimit
所设的上限,Job 会被标记为失败, 其中运行的 Pods 都会被终止。
终止 Job 的另一种方式是设置一个活跃期限。 你可以为 Job 的 .spec.activeDeadlineSeconds
设置一个秒数值。 该值适用于 Job 的整个生命期,无论 Job 创建了多少个 Pod。 一旦 Job 运行时间达到 activeDeadlineSeconds
秒,其所有运行中的 Pod 都会被终止,并且 Job 的状态更新为 type: Failed
及 reason: DeadlineExceeded
。
注意 Job 的 .spec.activeDeadlineSeconds
优先级高于其 .spec.backoffLimit
设置。 因此,如果一个 Job 正在重试一个或多个失效的 Pod,该 Job 一旦到达 activeDeadlineSeconds
所设的时限即不再部署额外的 Pod,即使其重试次数还未 达到 backoffLimit
所设的限制。
例如:
apiVersion: batch/v1
kind: Job
metadata:
name: pi-with-timeout
spec:
backoffLimit: 5
activeDeadlineSeconds: 100
template:
spec:
containers:
- name: pi
image: perl
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
restartPolicy: Never
还要注意的是,restartPolicy
对应的是 Pod,而不是 Job 本身: 一旦 Job 状态变为 type: Failed
,就不会再发生 Job 重启的动作。 换言之,由 .spec.activeDeadlineSeconds
和 .spec.backoffLimit
所触发的 Job 终结机制都会导致 Job 永久性的失败,而这类状态都需要手工干预才能解决。
自动清理完成的 Job:
完成的 Job 通常不需要留存在系统中。在系统中一直保留它们会给 API 服务器带来额外的压力。 如果 Job 由某种更高级别的控制器来管理,例如 CronJobs, 则 Job 可以被 CronJob 基于特定的根据容量裁定的清理策略清理掉。
已完成 Job 的 TTL 机制
自动清理已完成 Job (状态为 Complete
或 Failed
)的另一种方式是使用由 TTL 控制器所提供 的 TTL 机制。 通过设置 Job 的 .spec.ttlSecondsAfterFinished
字段,可以让该控制器清理掉已结束的资源。
TTL 控制器清理 Job 时,会级联式地删除 Job 对象。 换言之,它会删除所有依赖的对象,包括 Pod 及 Job 本身。 注意,当 Job 被删除时,系统会考虑其生命周期保障,例如其 Finalizers。
apiVersion: batch/v1
kind: Job
metadata:
name: pi-with-ttl
spec:
ttlSecondsAfterFinished: 100
template:
spec:
containers:
- name: pi
image: perl
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
restartPolicy: Never
Job pi-with-ttl
在结束 100 秒之后,可以成为被自动删除的对象。
如果该字段设置为 0
,Job 在结束之后立即成为可被自动删除的对象。 如果该字段没有设置,Job 不会在结束之后被 TTL 控制器自动清除。
注意这种 TTL 机制仍然是一种 Alpha 状态的功能特性,需要配合 TTLAfterFinished
特性门控使用。有关详细信息,可参考 TTL 控制器 (opens new window)的文档。
# 六、Job 挂起
要挂起一个 Job,你可以将 Job 的 .spec.suspend
字段更新为 true。 之后,当你希望恢复其执行时,将其更新为 false。 创建一个 .spec.suspend
被设置为 true 的 Job 本质上会将其创建为被挂起状态。
当 Job 被从挂起状态恢复执行时,其 .status.startTime
字段会被重置为当前的时间。这意味着 .spec.activeDeadlineSeconds
计时器会在 Job 挂起时被停止,并在 Job 恢复执行时复位。
要记住的是,挂起 Job 会删除其所有活跃的 Pod。当 Job 被挂起时,你的 Pod 会 收到 SIGTERM 信号而被终止。 Pod 的体面终止期限会被考虑,不过 Pod 自身也必须在此期限之内处理完信号。 处理逻辑可能包括保存进度以便将来恢复,或者取消已经做出的变更等等。 Pod 以这种形式终止时,不会被记入 Job 的 completions
计数。
处于被挂起状态的 Job 的定义示例可能是这样子:
apiVersion: batch/v1
kind: Job
metadata:
name: myjob
spec:
suspend: true
parallelism: 1
completions: 5
template:
spec:
...
Job 的 status
可以用来确定 Job 是否被挂起,或者曾经被挂起:
apiVersion: batch/v1
kind: Job
# .metadata and .spec omitted
status:
conditions:
- lastProbeTime: "2021-02-05T13:14:33Z"
lastTransitionTime: "2021-02-05T13:14:33Z"
status: "True"
type: Suspended
startTime: "2021-02-05T13:13:48Z"
Job 的 "Suspended" 类型的状况在状态值为 "True" 时意味着 Job 正被 挂起;lastTransitionTime
字段可被用来确定 Job 被挂起的时长。 如果此状况字段的取值为 "False",则 Job 之前被挂起且现在在运行。 如果 "Suspended" 状况在 status
字段中不存在,则意味着 Job 从未 被停止执行。
当 Job 被挂起和恢复执行时,也会生成事件:
Name: myjob
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulCreate 12m job-controller Created pod: myjob-hlrpl
Normal SuccessfulDelete 11m job-controller Deleted pod: myjob-hlrpl
Normal Suspended 11m job-controller Job suspended
Normal SuccessfulCreate 3s job-controller Created pod: myjob-jvb44
Normal Resumed 3s job-controller Job resumed
# 七、CronJob
CronJob 创建基于时隔重复调度的 Job。
一个 CronJob 对象就像 crontab (cron table) 文件中的一行。 它用 Cron 格式进行编写, 并周期性地在给定的调度时间执行 Job。
注意:
所有 CronJob 的
schedule:
时间都是基于 kube-controller-manager (opens new window). 的时区。如果你的控制平面在 Pod 或是裸容器中运行了 kube-controller-manager, 那么为该容器所设置的时区将会决定 Cron Job 的控制器所使用的时区。
为 CronJob 资源创建清单时,请确保所提供的名称是一个合法的 DNS 子域名 (opens new window). 名称不能超过 52 个字符。 这是因为 CronJob 控制器将自动在提供的 Job 名称后附加 11 个字符,并且存在一个限制, 即 Job 名称的最大长度不能超过 63 个字符。
示例:每分钟打印出当前时间和问候消息
apiVersion: batch/v1
kind: CronJob
metadata:
name: hello
spec:
schedule: "*/1 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: hello
image: busybox
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -c
- date; echo Hello from the Kubernetes cluster
restartPolicy: OnFailure
Kubernetes 官网中的 「使用 CronJob 运行自动化任务 (opens new window) 」一文会为你详细讲解此例。
cron 表达式:
# ┌───────────── 分钟 (0 - 59)
# │ ┌───────────── 小时 (0 - 23)
# │ │ ┌───────────── 月的某天 (1 - 31)
# │ │ │ ┌───────────── 月份 (1 - 12)
# │ │ │ │ ┌───────────── 周的某天 (0 - 6) (周日到周一;在某些系统上,7 也是星期日)
# │ │ │ │ │
# │ │ │ │ │
# │ │ │ │ │
# * * * * *
示例:
输入 | 描述 | 相当于 |
---|---|---|
@yearly (or @annually) | 每年 1 月 1 日的午夜运行一次 | 0 0 1 1 * |
@monthly | 每月第一天的午夜运行一次 | 0 0 1 * * |
@weekly | 每周的周日午夜运行一次 | 0 0 * * 0 |
@daily (or @midnight) | 每天午夜运行一次 | 0 0 * * * |
@hourly | 每小时的开始一次 | 0 * * * * |