3.9 Jobs、CronJobs
在 Kubernetes 中,常用的工作负载/控制器,有 Deployment、ReplicaSet、StatefulSet、DaemonSet、Job、Cronjob、ReplicationController 等,在前面,我们已经学习了 Deloyment、DaemonSet、ReplicationController,本章将学习 Job 和 Cronjob。
我们知道,Pod 是一种临时性的对象,我们单独创建 Pod,一般用于临时测试调试,在生产中我们使用 Deployment 等对 Pod 进行管理。而 Job、Cronjob 它们主要用于创建一个或多个 Pod,来完成某些任务,它们创建的 Pod 不会长久的运行在节点中。
Job
Job 是用来只运行一次任务的对象,Job 对象以一种可靠的方式运行某 Pod 直到完成,适合用于批处理,例如编译程序、执行运算任务。Job 适合一次到位或者一次完整的流程,完成后即可抛弃的任务。
控制器异同点
前面我们已经学习到了 Deployment 、ReplicaSet,Job 跟它们一些异同点,这里列举一些它们各自的特点。
Pod 生命期
和一个个独立的应用容器一样,Pod 也被认为是相对临时性(而不是长期存在)的实体。 Pod 会被创建、赋予一个唯一的 ID, 并被调度到节点,并在终止(根据重启策略)或删除之前一直运行在该节点。
Pod 是临时的
Pod 自身不具有自愈能力。如果 Pod 被调度到某节点 而该节点之后失效,或者调度操作本身失效,Pod 会被删除;与此类似,Pod 无法在节点资源耗尽或者节点维护期间继续存活。Deployment、ReplicaSet、Job 都会重新创建新的 Pod 来替代已终止的 Pod。
Deployment/ReplicaSet、Job
Deployment/ReplicaSet 控制器管理的是那些不希望被终止的 Pod (例如,Web 服务器), Job 管理的是那些希望被预期终止的 Pod( 例如,批处理作业)。
为什么不用 Pod
Pod 是临时性的,有 Always、OnFailure、Never 等选项,设置在容器无法启动时的重试机制;而 Job 只有 OnFailure 、Never ,Job 最多只能重试一次。Pod 在自身没有保障,挂了就挂了,而且 Pod 本身只有一个。Job 会创建一个或者多个 Pods,在失败时将继续重试 Pods 的执行,或者创建新的 Pod,直到指定数量的 Pods 成功终止。
Job
当 Job 启动时,Job 会跟踪成功完成的 Pod 的个数,当成功数量达到某个阈值时,Job 会被终结。当 Job 运行过程中,我们 暂停/挂起 Job,Job 会删除正在运行的 Pod,保留已完成的 Pod 数量,当恢复 Job 时,会创建新的 Pod,继续完成任务。
Job 的结构很简单,下面是一个示例,这个 Job 只有一个镜像,启动后执行 sleep 3
命令,容器会在3秒后自动退出,Job 标记其已经完成。
apiVersion: batch/v1
kind: Job
metadata:
name: busybox
spec:
template:
spec:
containers:
- name: busybox
image: busybox
command: ["/bin/sleep"]
args: ["3"]
restartPolicy: Never
对于一个简单的 Job,其关键在于设定一个可结束的命令,容器执行命令后会退出。
此时这个容器被标记为完成状态。
command: ["/bin/sleep"] args: ["3"] restartPolicy: Never
restartPolicy
需要标记为 Never 或 OnFailure。
使用 kubectl apply -f job.yaml
命令启动此 Job,3 秒后查看 Pod:
root@instance-1:~# kubectl get pods
NAME READY STATUS RESTARTS AGE
busybox-k4wkn 0/1 Completed 0 16s
可以发现此 Pod 处于完成状态(Completed),再查看 job 列表:
root@instance-1:~# kubectl get jobs
NAME COMPLETIONS DURATION AGE
busybox 1/1 11s 101s
完成后我们可以直接删除它:
kubectl delete job busybox
对于这种简单的 Job,称为非并行的,它的特点有:
- 只启动一个 Pod,除非该 Pod 失败;
- 当 Pod 成功终止时,立即视 Job 为完成状态;
这里再列举一个 Job 示例,这个 Job 用于计算圆周率,计算完毕后打印圆周率并结束容器。
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
查看 Pod 列表,找到 pi-
开头的 Pod,查看日志:
root@instance-1:~# kubectl logs pi-7htxl
3.1415926535897932384626433... ...
.spec.backoffLimit
可视为 Job 在失败之前的重试次数。如果我们设置了
restartPolicy: OnFailure
,那么此 Pod 会被重试一次;如果
restartPolicy: Never
,并且设置了backoffLimit
,Pod 会被重试多次。
完成数
使用.spec.completions
来设置完成数时,Job 控制器所创建的每个 Pod 使用完全相同的 spec
模板。 这意味着任务的所有 Pod 都有相同的命令行,都使用相同的镜像和数据卷,甚至连 环境变量都(几乎)相同。
如下 YAML,template
中的 spec
,是每个 Pod 都带有的相关属性,使得每个 Pod 都保持一致。
spec:
completions: 5
template:
spec:
Job 会创建一个或者多个 Pods,并将继续重试 Pods 的执行,直到指定数量的 Pods 成功终止。 随着 Pods 成功结束,Job 跟踪记录成功完成的 Pods 个数。 当数量达到指定的成功个数阈值时,任务(即 Job)结束。 删除 Job 的操作会清除所创建的全部 Pods。
我们继续使用上次的 Job 模板,这里增加一个 completions
,完整 YAML 内容如下:
apiVersion: batch/v1
kind: Job
metadata:
name: busybox
spec:
completions: 5
template:
spec:
containers:
- name: busybox
image: busybox
command: ["/bin/sleep"]
args: ["3"]
restartPolicy: Never
使用 kubectl apply -f job.yaml
启动此 Job。
查看 Job 和 Pod:
root@instance-1:~# kubectl get jobs
NAME COMPLETIONS DURATION AGE
busybox 5/5 36s 38s
root@instance-2:~# kubectl get pods
NAME READY STATUS RESTARTS AGE
busybox-rfhcj 0/1 Completed 0 9s
busybox-stkbg 0/1 Completed 0 23s
busybox-xk6sb 0/1 Completed 0 30s
busybox-z6h9x 0/1 Completed 0 40s
busybox-zqgcb 0/1 Completed 0 16s
Pod 的创建是串行的,每次只运行一个 Pod,当一个 Pod 处于 Completed 状态时,创建下一个 Pod。当有 5 个 Pod 处于 Completed 状态时,此 Job 标记完成。
前面学习到了 completions
,串行启动 Pod,直到 Pod 完成数量为 5 。
如果经常查看 Job 的状态,可以观察到:
NAME COMPLETIONS DURATION AGE
busybox 1/5 30s 10s
NAME COMPLETIONS DURATION AGE
busybox 2/5 28s 18s
NAME COMPLETIONS DURATION AGE
busybox 3/5 28s 25s
root@instance-1:~# kubectl get jobs
NAME COMPLETIONS DURATION AGE
busybox 4/5 30s 30s
root@instance-1:~# kubectl get jobs
NAME COMPLETIONS DURATION AGE
busybox 5/5 36s 37s
并行
我们查看前面设置 Job 的 YAML :
kubectl get job busybox -o yaml
... ...
spec:
backoffLimit: 6
completions: 5
parallelism: 1
selector:
... ...
可以看到 parallelism=1
,这个 parallelism
正是控制并行度的字段,由于这个字段默认为 1,所以这个 Job 每次只能运行一个 Pod。
spec.completions
和spec.parallelism
,这两个属性都不设置时,均取默认值 1。
当我们修改这个 parallelism
值大于 1 时,多个 Pod 可以同时执行。
.spec.parallelism
可以设置为任何非负整数。 如果设置为 0,则 Job 相当于启动之后便被暂停,因为一直没有 Pod 完成,当然我们可以使用 kubectl edit
命令修改其值。
另外,可以 Job 运行时,使用 kubecl scale job xxxx --replicas=n
修改 Job 的并行数量 parallelism
字段的值。
当 parallelism=2
时,其启动过程可能如下:
带工作队列的 Job
带工作队列的 Job,指设置了 .spec.parallelism
,可以不设置 .spec.completions
的 Job。
此时 Job 需要等待所有的 Pod 完成任务,Job 才能终结。
下面创建一个 Job,这里我们启动 5 个 Pod,每个 Pod 不断向 RabbitMQ 请求消费一条消息,当 RabbitMQ 已经没有可消费的消息时,Pod 结束。
apiVersion: batch/v1
kind: Job
metadata:
name: job-wq-1
spec:
parallelism: 5
template:
metadata:
name: job-wq-1
spec:
containers:
- name: c
image: gcr.io/<project>/job-wq-1
env:
- name: BROKER_URL
value: amqp://guest:guest@rabbitmq-service:5672
- name: QUEUE
value: job1
restartPolicy: OnFailure
[Success] 提示
此处使用的 RabbitMQ 是 Kubernetes 官方供大家练习用的,不需要自己部署一个来测试。
此次容器的镜像不是真实存在的,要进行实验,可参考 https://kubernetes.io/docs/tasks/job/_print/#create-an-image 构建镜像测试。
对于没有设置 .spec.completions
的 Job,情况复杂的多,这种情况下任务的总数是未知的。
不设置 .spec.completions
的带工作队列的 Job 会同时启动多个 Pod,只要有任意一个 Pod 成功退出,Job 控制器就知道任务已经完成了,所有的 Pod 将很快会退出。 当所有 Pod 结束,此 Job 才会标记为成功完成,虽然只需要有一个 Pod 完成即可,但 Job 控制器还是会等待其它 Pod 结束。
笔者摘录 Kubernetes 官方文档关于这种 Job 的描述:
- 不设置
spec.completions
。 - 多个 Pod 之间必须相互协调,或者借助外部服务确定每个 Pod 要处理哪个工作条目。
- 每个 Pod 都可以独立确定是否其它 Pod 都已完成,进而确定 Job 是否完成
- 当 Job 中 任何 Pod 成功终止,不再创建新 Pod
- 一旦至少 1 个 Pod 成功完成,并且所有 Pod 都已终止,即可宣告 Job 成功完成
- 一旦任何 Pod 成功退出,任何其它 Pod 都不应再对此任务执行任何操作或生成任何输出。 所有 Pod 都应启动退出过程。
对于这种 Job ,使用起来比较复杂,这种 Job 一般需要结合外部服务来完成任务,例如前面提到了 RabbitMQ。
以前面的 RabbitMQ 为例,Job 启动了 5 个 Pod 消费消息,当 RabbitMQ 没有消息了,此时 Pod1 把任务完成了,然后请求 RabbitMQ ,发现没有消息可处理,Pod1 便结束运行。此时 Job 已经算完成了,因为 Job 的任务是处理完 RabbitMQ,RabbitMQ 没有消息了,此 Job 自然算完成了,但是此时还有四个 Pod 取到了消息但是就没有计算完成,这四个 Pod 继续处理完成任务。当 5 个 Pod 都结束时,此 Job 便功德圆满了。
带类型的 Job
Job 中的 Pod 都是一样的,因此如果要 Job 处理不同的工作任务,则需要外界帮忙。
举个例子,平台是一个电商系统,消息队列中有评论、订单等五类消息,那么应该设计五种程序去处理这些消息,但是 Pod 只有一种。此时可以设置原子性的任务领取中心,Job 启动 Pod 后,Pod 便向任务中心领取任务类型,领取到后,开始工作。
那么 Job 中的 Pod 可能是这样完成工作的:
[Info] 提示
限于条件,这里就不编写具有这种功能的程序了,读者只需要了解即可。
在 Job 创建的 Pod 中,会有个名为 JOB_COMPLETION_INDEX
的环境变量,此环境变量标识了 Pod 的索引,Pod 可以通过此索引标识自己的身份。
示例 YAML 如下:
apiVersion: batch/v1
kind: Job
metadata:
name: busybox
spec:
parallelism: 1
completions: 5
completionMode: Indexed
template:
spec:
containers:
- name: busybox
image: busybox
command: ["env"]
restartPolicy: Never
[Info] 提示
completionMode: Indexed
表明当前 Pod 是带索引的,如果completionMode: NonIndexed
则不带索引。索引会按照 0,1,2,3 这样递增。
执行 kubectl apply -f job.yaml
启动此 Job,会发现:
root@master:~# kubectl get pods
NAME READY STATUS RESTARTS AGE
busybox-0-j8gvz 0/1 Completed 0 29s
busybox-1-k4kfx 0/1 Completed 0 25s
busybox-2-zplxl 0/1 Completed 0 14s
busybox-3-pj4jk 0/1 Completed 0 10s
busybox-4-q5fq9 0/1 Completed 0 6s
如果我们查看 Pod 的环境变量,会发现:
root@master:~# kubectl logs busybox-0-j8gvz
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=busybox-0
JOB_COMPLETION_INDEX=0
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
HOME=/root
处理 Pod 和容器失效
Job 终止和清理
如果我们不希望 Job 运行太长时间,可以为 Job 的 .spec.activeDeadlineSeconds
设置一个秒数值。 在 Job 的整个生命期,无论 Job 创建了多少个 Pod。 一旦 Job 运行时间达到 activeDeadlineSeconds
秒,其所有运行中的 Pod 都会被终止,并且 Job 的状态更新为 type: Failed
及 reason: DeadlineExceeded
。
YAML 示例:
apiVersion: batch/v1
kind: Job
metadata:
name: busybox
spec:
completions: 5
activeDeadlineSeconds: 2
template:
spec:
containers:
- name: busybox
image: busybox
command: ["/bin/sleep"]
args: ["3"]
restartPolicy: Never
CronJob
CronJobs 对于创建周期性的、反复重复的任务很有用,例如执行数据备份或者发送邮件。 CronJobs 也可以用来计划在指定时间时来执行的独立任务,例如计划当集群看起来很空闲时 执行某个 Job。
CronJob 有 个 schedule 字段,用来配置合适启动此 CronJob,其使用五个时间单位,每一位表示一个时刻表示,如 "*/1 * * * *"
。
# ┌───────────── 分钟 (0 - 59)
# │ ┌───────────── 小时 (0 - 23)
# │ │ ┌───────────── 月的某天 (1 - 31)
# │ │ │ ┌───────────── 月份 (1 - 12)
# │ │ │ │ ┌───────────── 周的某天 (0 - 6) (周日到周一;在某些系统上,7 也是星期日)
# │ │ │ │ │
# │ │ │ │ │
# │ │ │ │ │
# * * * * *
每个小时运行一次,在 0 分时开始运行:0 * * * *
,或使用 @hourly
表示。
每天运行一次,在半夜 0 时 0 分运行: 0 0 * * *
,或使用 @daily
表示。
在每周的周日 0 点运行一次,时间在半夜: 0 0 * * 0
,或使用 @weekly
表示。
在每月的 1 号运行一次: 0 0 1 * *
,或使用 @monthly
表示。
每年的 1 月 1 日 执行一次:0 0 1 1 *
,或使用 @yearly
表示。
CronJob 有四种符合,分别是 *
、,
、-
、/
。
符号 /
,使用格式 */{值}
,表示每间隔 1 个 时间执行一次,例如 */1 * * * *
表示间隔一分钟执行一次。
符号 -
,使用格式 {值1}-{值2}
,表示范围中的每一个时间,例如 0-2 * * * *
表示 0 到 20 分钟的每一分钟执行一次,一共 20 次。
符号 ,
,使用格式 */{值1},{值2}...
,表示碰到这里的时间都执行一次,例如 1,5,6 * * * *
,表示 每次在 1,5,6 分钟时都执行一次。
组合示例,23 0-20/2 * * *
,表示在 0-20
点时,每间隔 2
个小时执行一次,其包括的时间:
00:23:00
02:23:00
04:23:00
... ...
20:23:00
请参考:
可供实验的 YAML 示例如下:
apiVersion: batch/v1beta1
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
此 CronJob 会每分钟执行一次。