概述
Job控制器用于调配pod对象运行一次性任务,容器中的进程在正常运行结束后不会对其进行重启,而是将pod对象置于completed状态。若容器中的进程因错误而终止,则需要依据配置确定重启与否,未运行完成的pod对象因其所在的节点故障而意外终止后会被重新调度。
Job example
通过如下Job 示例计算 π 到小数点后 2000 位,并将结果打印出来。 此计算大约需要 10 秒钟完成。
# vim job.yml
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
使用以下命令来创建该实例:
# kubectl apply -f job.yml
job.batch/pi created
使用以下命令查看Job详细状态:
# kubectl describe jobs/pi
Name: pi
Namespace: default
Selector: controller-uid=c4efb802-2e4a-4165-90c8-5e12422f1719
Labels: controller-uid=c4efb802-2e4a-4165-90c8-5e12422f1719
job-name=pi
Annotations: <none>
Parallelism: 1
Completions: 1
Start Time: Wed, 25 Aug 2021 14:34:22 +0800
Pods Statuses: 1 Running / 0 Succeeded / 0 Failed
Pod Template:
Labels: controller-uid=c4efb802-2e4a-4165-90c8-5e12422f1719
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 7s job-controller Created pod: pi-wjssf
要以机器可读的方式列举隶属于某 Job 的全部 Pods,你可以使用类似下面这条命令:
# pods=$(kubectl get pods --selector=job-name=pi --output=jsonpath='{.items[*].metadata.name}')
# echo $pods
pi-wjssf
查看 Pod 的标准输出 :
# kubectl logs $pods
3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989380952572010654858632788659361533818279682303019520353018529689957736225994138912497217752834791315155748572424541506959508295331168617278558890750983817546374649393192550604009277016711390098488240128583616035637076601047101819429555961989467678374494482553797747268471040475346462080466842590694912933136770289891521047521620569660240580381501935112533824300355876402474964732639141992726042699227967823547816360093417216412199245863150302861829745557067498385054945885869269956909272107975093029553211653449872027559602364806654991198818347977535663698074265425278625518184175746728909777727938000816470600161452491921732172147723501414419735685481613611573525521334757418494684385233239073941433345477624168625189835694855620992192221842725502542568876717904946016534668049886272327917860857843838279679766814541009538837863609506800642251252051173929848960841284886269456042419652850222106611863067442786220391949450471237137869609563643719172874677646575739624138908658326459958133904780275901
Job的定义
与Kubernetes中其他资源的配置类似,Job也需要 apiVersion
、kind
和 metadata
字段。 Job 的名字必须是合法的 DNS 子域名。
Job 对象的 YAML 文件,还需要一个 .spec
字段。
Pod 模板
Job 的 .spec
中只有 .spec.template
是必需的字段。
字段 .spec.template
的值是一个 Pod 模版。 其定义规范与 Pod 完全相同,只是其中不再需要 apiVersion
或 kind
字段。
除了作为 Pod 所必需的字段之外,Job 中的 Pod 模版必需设置合适的标签 和合适的重启策略。
Job 中 Pod 的 RestartPolicy
只能设置为 Never
或 OnFailure
之一
Pod 选择算符
字段 .spec.selector
是可选的。在绝大多数场合,你都不需要为其赋值。 参阅设置自己的 Pod 选择算符
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
设置为一个非负整数。
控制并行性
并行性请求(.spec.parallelism
)可以设置为任何非负整数。 如果未设置,则默认为 1。 如果设置为 0,则 Job 相当于启动之后便被暂停,直到此值被增加。
实际并行性(在任意时刻运行状态的 Pods 个数)可能比并行性请求略大或略小, 原因如下:
- 对于 确定完成计数 Job,实际上并行执行的 Pods 个数不会超出剩余的完成数。 如果
.spec.parallelism
值较高,会被忽略。 - 对于 工作队列 Job,有任何 Job 成功结束之后,不会有新的 Pod 启动。 不过,剩下的 Pods 允许执行完毕。
- 如果 Job 没有来得及作出响应,或者
- 如果 Job 控制器因为任何原因(例如,缺少
ResourceQuota
或者没有权限)无法创建 Pods。 Pods 个数可能比请求的数目小。 - Job 控制器可能会因为之前同一 Job 中 Pod 失效次数过多而压制新 Pod 的创建。
- 当 Pod 处于体面终止进程中,需要一定时间才能停止
处理 Pod 和容器失效
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 回退失效策略
为了实现这点,可以将 .spec.backoffLimit
设置为视 Job 为失败之前的重试次数。 失效回退的限制值默认为 6。 与 Job 相关的失效的 Pod 会被 Job 控制器重建,回退重试时间将会按指数增长 (从 10 秒、20 秒到 40 秒)最多至 6 分钟。 当 Job 的 Pod 被删除时,或者 Pod 成功时没有其它 Pod 处于失败状态,失效回退的次数也会被重置(为 0)。
如果你的 Job 的
restartPolicy
被设置为 "OnFailure",就要注意运行该 Job 的容器 会在 Job 到达失效回退次数上限时自动被终止。 这会使得调试 Job 中可执行文件的工作变得非常棘手。 我们建议在调试 Job 时将restartPolicy
设置为 "Never", 或者使用日志系统来确保失效 Jobs 的输出不会意外遗失
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 (状态为 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 控制器自动清除。
Job 模式
Kubernetes Job 对象可以用来支持 Pod 的并发执行,但是:
- Job 对象并非设计为支持需要紧密相互通信的Pod的并发执行,例如科学计算
- Job 对象不支持并发处理一系列相互独立但是又相互关联的工作任务,例如:
- 发送邮件
- 渲染页面
- 转码文件
- 扫描 NoSQL 数据库中的主键
- 其他
在一个复杂的系统中,可能存在多种类型的工作任务,本文只考虑批处理任务(batch job)。
对于批处理任务的并行计算,存在着几种模式,它们各自有自己的优缺点:
- 每个工作任务一个 Job 对象 v.s. 一个 Job 对象负责所有的工作任务
- 当工作任务特别多时,第二种选择(一个 Job 对象负责所有的工作任务)更合适一些
- 第一种选择(每个工作任务一个 Job 对象)将为管理员和系统带来很大的额外开销,因为要管理很多数量的 Job 对象
- Pod的数量与工作任务的数量相等 v.s. 每个Pod可以处理多个工作任务
- 第一种选择(Pod的数量与工作任务的数量相等)通常只需要对现有的代码或容器做少量的修改
- 第二种选择(每个Pod可以处理多个工作任务)更适合工作任务的数量特别多的情况,相较于第一种选择可以降低系统开销
- 使用工作队列,此时:
- 需要运行一个队列服务
- 需要对已有的程序或者容器做修改,以便其可以配合队列工作
- 如果是一个已有的程序,改造时可能存在难度
他们的优缺点归纳如下表所示,其中第二列到第四列罗列了主要考虑的对比因素:
模式 | 单个 Job 对象 | Pods 数少于工作条目数? | 直接使用应用无需修改? |
---|---|---|---|
每工作条目一 Pod 的队列 | ✓ | 有时 | |
Pod 数量可变的队列 | ✓ | ✓ | |
静态任务分派的带索引的 Job | ✓ | ✓ | |
Job 模版扩展 | ✓ |
当你使用 .spec.completions
来设置完成数时,Job 控制器所创建的每个 Pod 使用完全相同的 spec
。 这意味着任务的所有 Pod 都有相同的命令行,都使用相同的镜像和数据卷,甚至连 环境变量都(几乎)相同。 这些模式是让每个 Pod 执行不同工作的几种不同形式。
下表显示的是每种模式下 .spec.parallelism
和 .spec.completions
所需要的设置。 其中,W
表示的是工作条目的个数。
模式 | .spec.completions | .spec.parallelism |
---|---|---|
每工作条目一 Pod 的队列 | W | 任意值 |
Pod 个数可变的队列 | 1 | 任意值 |
静态任务分派的带索引的 Job | W | |
Job 模版扩展 | 1 | 应该为 1 |
Job的特殊操作
在创建 Job 时,系统默认将为其指定一个 .spec.selector
的取值,并确保其不会与任何其他 Job 重叠。
在少数情况下,您仍然可能需要覆盖这个自动设置 .spec.selector
的取值。在做此项操作时,您必须十分小心:
-
如果指定的.spec.selector
不能确定性的标识出该 Job 的 Pod,并可能选中无关的 Pod (例如,selector 可能选中其他控制器创建的 Pod),则:
- 与该 Job 不相关的 Pod 可能被删除
- 该 Job 可能将不相关的 Pod 纳入到
.spec.completions
的计数 - 一个或多个 Job 可能不能够创建 Pod,或者不能够正常结束
-
如果指定的
.spec.selector
不是唯一的,则其他控制器(例如,Deployment、StatefulSet 等)及其 Pod 的行为也将难以预测
在 Kubernetes 中,系统并不能帮助你避免此类型的错误,您需要自己关注所指定的 .spec.selector
的取值是否合理。
让我们来看一个实际使用 .spec.selector
的例子:假设 Job old
已经运行,您希望已经创建的 Pod 继续运行,但是您又想要修改该 Job 的 名字,同时想要使该 Job 新建的 Pod 使用新的 template。此时您不能够修改已有的 Job 对象,因为这些字段都是不可修改的。此时,您可以执行命令 kubectl delete jobs/old --cascade=false
,以删除 Job old
但是保留其创建的 Pod。
- 在删除之前,先记录 Job
old
的 selector,执行命令:
kubectl get job old -o yaml
输出结果:
kind: Job
metadata:
name: old
...
spec:
selector:
matchLabels:
controller-uid: a8f3d00d-c6d2-11e5-9f87-42010af00002
...
创建新的 Job new
,并使用已有的 selector。由于已创建的 Pod 带有标签 controller-uid=a8f3d00d-c6d2-11e5-9f87-42010af00002
,这些 Pod 也将被新的 Job new
所管理。使用类似如下的 yaml 文件创建 Job new
:
kind: Job
metadata:
name: new
...
spec:
manualSelector: true
selector:
matchLabels:
controller-uid: a8f3d00d-c6d2-11e5-9f87-42010af00002
...
- 当您不使用系统自动创建的
.spec.selector
时,需要在 Jobnew
中指定.spec.manualSelector: true
- 新建的 Job
new
其 uid 将不同于a8f3d00d-c6d2-11e5-9f87-42010af00002
。设置.spec.manualSelector: true
意味着,您知道这个设定是有意为之,系统将使用您指定的.spec.selector
,而不是使用 Jobnew
的 uid 作为.spec.selector
的取值
Job 替代方案
裸Pod
当 Pod 运行所在的节点重启或者失败,Pod 会被终止并且不会被重启。 Job 会重新创建新的 Pod 来替代已终止的 Pod。 因为这个原因,我们建议你使用 Job 而不是独立的裸 Pod, 即使你的应用仅需要一个 Pod。
副本控制器
Job 与副本控制器是彼此互补的。 副本控制器管理的是那些不希望被终止的 Pod (例如,Web 服务器), Job 管理的是那些希望被终止的 Pod(例如,批处理作业)。
正如在 Pod 生命期中讨论的, Job
仅适合于 restartPolicy
设置为 OnFailure
或 Never
的 Pod。 注意:如果 restartPolicy
未设置,其默认值是 Always
。
单个 Job 启动控制器 Pod
另一种模式是用唯一的 Job 来创建 Pod,而该 Pod 负责启动其他 Pod,因此扮演了一种 后启动 Pod 的控制器的角色。 这种模式的灵活性更高,但是有时候可能会把事情搞得很复杂,很难入门, 并且与 Kubernetes 的集成度很低。
这种模式的实例之一是用 Job 来启动一个运行脚本的 Pod,脚本负责启动 Spark 主控制器(参见 Spark 示例), 运行 Spark 驱动,之后完成清理工作。
这种方法的优点之一是整个过程得到了 Job 对象的完成保障, 同时维持了对创建哪些 Pod、如何向其分派工作的完全控制能力,
Cron Jobs
你可以使用 CronJob
创建一个在指定时间/日期运行的 Job,类似于 UNIX 系统上的 cron
工具。
参考文章
https://kubernetes.io/zh/docs/concepts/workloads/controllers/job/
评论区