在 Kubernetes 中,常用的工作负载/控制器,有 Deployment、ReplicaSet、StatefulSet、DaemonSet、Job、Cronjob、ReplicationController 等,在前面,我们已经学习了 Deloyment、DaemonSet、ReplicationController,本章将学习 Job 和 Cronjob。
我们知道,Pod 是一种临时性的对象,我们单独创建 Pod,一般用于临时测试调试,在生产中我们使用 Deployment 等对 Pod 进行管理。而 Job、Cronjob 它们主要用于创建一个或多个 Pod,来完成某些任务,它们创建的 Pod 不会长久的运行在节点中。
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 会跟踪成功完成的 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
flowchart LR
Start --> busybox1 --> busybox2 --> busybox3 --> busybox4 --> busybox5 --> Stop
我们查看前面设置 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
时,其启动过程可能如下:
graph TD;
Start-->busybox1
Start-->busybox2;
busybox1-->busybox3;
busybox2-->busybox4;
busybox3-->busybox5;
带工作队列的 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 中的 Pod 都是一样的,因此如果要 Job 处理不同的工作任务,则需要外界帮忙。
举个例子,平台是一个电商系统,消息队列中有评论、订单等五类消息,那么应该设计五种程序去处理这些消息,但是 Pod 只有一种。此时可以设置原子性的任务领取中心,Job 启动 Pod 后,Pod 便向任务中心领取任务类型,领取到后,开始工作。
那么 Job 中的 Pod 可能是这样完成工作的:
gantt
dateFormat YYYY-MM-DD
title Message Queue
section Job
评论处理 :done , 2021-11-01, 1d
问答处理 :done, 2021-11-02, 1d
差评处理 :done, 2021-11-03, 1d
订单处理 :done, 2021-11-04, 1d
退货处理 :active, 2021-11-05, 1d
[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
如果我们不希望 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
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 会每分钟执行一次。