Job与CronJob
创建一个Job API对象
apiVersion: batch/v1 kind: Job metadata: name: pi spec: template: spec: containers: - name: pi image: resouer/ubuntu-bc command: ["sh", "-c", "echo 'scale=10000; 4*a(1)' | bc -l "] restartPolicy: Never backoffLimit: 4
kubectl create -f job.yaml
查看这个Job
[root@master zztK8s]# kubectl describe jobs pi Name: pi Namespace: default Selector: controller-uid=6d16e85e-9b7c-474a-af93-d85ddbbda5be Labels: controller-uid=6d16e85e-9b7c-474a-af93-d85ddbbda5be job-name=pi Annotations: <none> Parallelism: 1 Completions: 1 Start Time: Mon, 07 Mar 2022 22:09:14 0800 Pods Statuses: 1 Running / 0 Succeeded / 0 Failed Pod Template: Labels: controller-uid=6d16e85e-9b7c-474a-af93-d85ddbbda5be job-name=pi Containers: pi: Image: resouer/ubuntu-bc Port: <none> Host Port: <none> Command: sh -c echo 'scale=10000; 4*a(1)' | bc -l Environment: <none> Mounts: <none> Volumes: <none> Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal SuccessfulCreate 35s job-controller Created pod: pi-cdccb
查看这个Pod:
Name: pi-cdccb Namespace: default Priority: 0 Node: node1/192.168.88.101 Start Time: Mon, 07 Mar 2022 22:09:14 0800 Labels: controller-uid=6d16e85e-9b7c-474a-af93-d85ddbbda5be job-name=pi Annotations: <none> Status: Succeeded IP: 10.244.2.55 IPs: IP: 10.244.2.55 Controlled By: Job/pi Containers: pi: Container ID: docker://29a089e89234d2ef9266e75c05541b30e2b77d4b663d67763dfcffcec88a7942 Image: resouer/ubuntu-bc Image ID: docker-pullable://resouer/ubuntu-bc@sha256:3aff2cb1513375dc4ec42b80e8694cd1f9a8970fa5a55ebff98e1b85fe241d7f Port: <none> Host Port: <none> Command: sh -c echo 'scale=10000; 4*a(1)' | bc -l State: Terminated Reason: Completed Exit Code: 0 Started: Mon, 07 Mar 2022 22:09:32 0800 Finished: Mon, 07 Mar 2022 22:10:48 0800 Ready: False Restart Count: 0 Environment: <none> Mounts: /var/run/secrets/kubernetes.io/serviceaccount from default-token-z8n96 (ro) Conditions: Type Status Initialized True Ready False ContainersReady False PodScheduled True Volumes: default-token-z8n96: Type: Secret (a volume populated by a Secret) SecretName: default-token-z8n96 Optional: false QoS Class: BestEffort Node-Selectors: <none> Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s node.kubernetes.io/unreachable:NoExecute for 300s Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled <unknown> default-scheduler Successfully assigned default/pi-cdccb to node1 Normal Pulling 4m20s kubelet, node1 Pulling image "resouer/ubuntu-bc" Normal Pulled 4m3s kubelet, node1 Successfully pulled image "resouer/ubuntu-bc" Normal Created 4m3s kubelet, node1 Created container pi Normal Started 4m3s kubelet, node1 Started container pi
这个 Job 对象创建后,它的 Pod 自动添加模板 controller-uid=< 随机字符串 > 这样的 Label。而这个 Job 对象本身就是自动添加的 Label 对应的 Selector,从而 保证了 Job 它管理的 Pod 匹配关系。(Job Controller 之所以要使用这种携带了 UID 的 Label,避免不同 Job 对象管理 Pod 发生重合。)
[root@master zztK8s]# kubectl get pods pi-cdccb NAME READY STATUS RESTARTS AGE pi-cdccb 0/1 Completed 0 6m35s
[root@master zztK8s]# kubectl logs pi-cdccb 3.141592653589793238462643383279502884197169399375105820974944592307\ 81640628620899862803482534211706798214808651328230664709384460955058\ 22317253594081284811174502841027019385211055596446229489549303819644\ 28810975665933446128475648233786783165271201909145648566923460348610\
离线作业失败
Job 对象的 spec.backoffLimit 字段定义了重试次数 4(即,backoffLimit=4),这个字段的默认值是 6。
如果指定为
:在任何情况下,只要容器不在运行状态,就自动重启容器;
最长运行时间
在 Job 的 API 对象里,有一个 spec.activeDeadlineSeconds 字段可以设置最长运行时间,比如:
spec:
backoffLimit: 5
activeDeadlineSeconds: 100
一旦运行超过了 100 s,这个 Job 的所有 Pod 都会被终止。并且,你可以在 Pod 的状态里看到终止的原因是 reason: DeadlineExceeded。
并行作业
在 Job 对象中,负责并行控制的参数有两个:
- spec.parallelism,它定义的是一个 Job 在任意时间最多可以启动多少个 Pod 同时运行;
- spec.completions,它定义的是 Job 至少要完成的 Pod 数目,即 Job 的最小完成数。
小例子
apiVersion: batch/v1
kind: Job
metadata:
name: pi
spec:
parallelism: 2
completions: 4
template:
spec:
containers:
- name: pi
image: resouer/ubuntu-bc
command: ["sh", "-c", "echo 'scale=5000; 4*a(1)' | bc -l "]
restartPolicy: Never
backoffLimit: 4
指定了这个 Job 最大的并行数是 2,而最小的完成数是 4。
创建这个对象,查看Pod对象
[root@master zztK8s]# kubectl get pods pi-qlqc5
NAME READY STATUS RESTARTS AGE
pi-qlqc5 0/1 Completed 0 33s
[root@master zztK8s]# kubectl get pods
pi-bvv9j 0/1 Completed 0 63s
pi-qlqc5 0/1 Completed 0 111s
pi-sp6kq 0/1 Completed 0 111s
pi-v2j8v 0/1 Completed 0 80s
每当有一个 Pod 完成计算进入 Completed 状态时,就会有一个新的 Pod 被自动创建出来,并且快速地从 Pending 状态进入到 ContainerCreating 状态
[root@master zztK8s]# kubectl get jobs
NAME COMPLETIONS DURATION AGE
pi 4/4 101s 3m8s
三种常用的、使用 Job 对象的方法。
第一种:
这种模式的特定用法是:把 Job 的 YAML 文件定义为一个“模板”,然后用一个外部工具控制这些“模板”来生成 Job。这时,Job 的定义方式如下所示:
apiVersion: batch/v1
kind: Job
metadata:
name: process-item-$ITEM
labels:
jobgroup: jobexample
spec:
template:
metadata:
name: jobexample
labels:
jobgroup: jobexample
spec:
containers:
- name: c
image: busybox
command: ["sh", "-c", "echo Processing item $ITEM && sleep 5"]
restartPolicy: Never
在这个 Job 的 YAML 里,定义了 $ITEM 这样的“变量”。
所以,在控制这种 Job 时,我们只要注意如下两个方面即可:
- 创建 Job 时,替换掉 $ITEM 这样的变量;
- 所有来自于同一个模板的 Job,都有一个 jobgroup: jobexample 标签,也就是说这一组 Job 使用这样一个相同的标识。
而做到第一点非常简单。比如,你可以通过这样一句 shell 把 $ITEM 替换掉:
$ mkdir ./jobs
$ for i in apple banana cherry
do
cat job-tmpl.yaml | sed "s/\$ITEM/$i/" > ./jobs/job-$i.yaml
done
一组来自于同一个模板的不同 Job 的 yaml 就生成了。接下来,就可以通过一句 kubectl create 指令创建这些 Job 了:
$ kubectl create -f ./jobs
$ kubectl get pods -l jobgroup=jobexample
NAME READY STATUS RESTARTS AGE
process-item-apple-kixwv 0/1 Completed 0 4m
process-item-banana-wrsf7 0/1 Completed 0 4m
process-item-cherry-dnfu9 0/1 Completed 0 4m
大多数用户在需要管理 Batch Job 的时候,都已经有了一套自己的方案,需要做的往往就是集成工作。这时候,Kubernetes 项目对这些方案来说最有价值的,就是 Job 这个 API 对象。所以,你只需要编写一个外部工具(等同于我们这里的 for 循环)来管理这些 Job 即可。
这种模式最典型的应用,就是 TensorFlow 社区的 KubeFlow 项目。
很容易理解,在这种模式下使用 Job 对象,completions 和 parallelism 这两个字段都应该使用默认值 1,而不应该由我们自行设置。而作业 Pod 的并行控制,应该完全交由外部工具来进行管理(比如,KubeFlow)。
第二种:拥有固定任务数目的并行 Job
这种模式下,只关心最后是否有指定数目(spec.completions)个任务成功退出。至于执行时的并行度是多少,并不关心。
比如,计算 Pi 值的例子,就是这样一个典型的、拥有固定任务数目(completions=4)的应用场景。 它的 parallelism 值是 2;或者,你可以干脆不指定 parallelism,直接使用默认的并行度(即:1)。
此外,你还可以使用一个工作队列(Work Queue)进行任务分发。这时,Job 的 YAML 文件定义如下所示:
apiVersion: batch/v1
kind: Job
metadata:
name: job-wq-1
spec:
completions: 8
parallelism: 2
template:
metadata:
name: job-wq-1
spec:
containers:
- name: c
image: myrepo/job-wq-1
env:
- name: BROKER_URL
value: amqp://guest:guest@rabbitmq-service:5672
- name: QUEUE
value: job1
restartPolicy: OnFailure
可以看到,它的 completions 的值是:8,这意味着我们总共要处理的任务数目是 8 个。也就是说,总共会有 8 个任务会被逐一放入工作队列里(你可以运行一个外部小程序作为生产者,来提交任务)。
在这个实例中,我选择充当工作队列的是一个运行在 Kubernetes 里的 RabbitMQ。所以,我们需要在 Pod 模板里定义 BROKER_URL,来作为消费者。
所以,一旦你用 kubectl create 创建了这个 Job,它就会以并发度为 2 的方式,每两个 Pod 一组,创建出 8 个 Pod。每个 Pod 都会去连接 BROKER_URL,从 RabbitMQ 里读取任务,然后各自进行处理。这个 Pod 里的执行逻辑,我们可以用这样一段伪代码来表示:
/* job-wq-1 的伪代码 */
queue := newQueue($BROKER_URL, $QUEUE)
task := queue.Pop()
process(task)
exit
可以看到,每个 Pod 只需要将任务信息读取出来,处理完成,然后退出即可。而作为用户,我只关心最终一共有 8 个计算任务启动并且退出,只要这个目标达到,我就认为整个 Job 处理完成了。所以说,这种用法,对应的就是“任务总数固定”的场景。
决定什么时候启动新 Pod,什么时候 Job 才算执行完成。在这种情况下,任务的总数是未知的,所以你不仅需要一个工作队列来负责任务分发,还需要能够判断工作队列已经为空(即:所有的工作已经结束了)
Job 的定义基本上没变化,只不过是不再需要定义 completions 的值了而已:
apiVersion: batch/v1
kind: Job
metadata:
name: job-wq-2
spec:
parallelism: 2
template:
metadata:
name: job-wq-2
spec:
containers:
- name: c
image: gcr.io/myproject/job-wq-2
env:
- name: BROKER_URL
value: amqp://guest:guest@rabbitmq-service:5672
- name: QUEUE
value: job2
restartPolicy: OnFailure
对应的 Pod 的逻辑会稍微复杂一些,可以用这样一段伪代码来描述
/* job-wq-2 的伪代码 */
for !queue.IsEmpty($BROKER_URL, $QUEUE) {
task := queue.Pop()
process(task)
}
print("Queue empty, exiting")
exit
由于任务数目的总数不固定,所以每一个 Pod 必须能够知道,自己什么时候可以退出。比如,在这个例子中,我简单地以“队列为空”,作为任务全部完成的标志。所以说,这种用法,对应的是“任务总数不固定”的场景。
不过,在实际的应用中,你需要处理的条件往往会非常复杂。比如,任务完成后的输出、每个任务 Pod 之间是不是有资源的竞争和协同等等。
CronJob。
CronJob 描述的,正是定时任务。它的 API 对象,如下所示:
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: hello
spec:
schedule: "*/1 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: hello
image: busybox
args:
- /bin/sh
- -c
- date; echo Hello from the Kubernetes cluster
restartPolicy: OnFailure
最重要的关键词就是。看到它,你一定恍然大悟,原来 CronJob 是一个 Job 对象的控制器(Controller)!
没错,CronJob 与 Job 的关系,正如同 Deployment 与 Pod 的关系一样。CronJob 是一个专门用来管理 Job 对象的控制器。只不过,它创建和删除 Job 的依据,是 schedule 字段定义的、一个标准的Unix Cron格式的表达式。
比如,"*/1 * * * *"。
这个 Cron 表达式里 */1 中的 * 表示从 0 开始,/ 表示“每”,1 表示偏移量。所以,它的意思就是:从 0 开始,每 1 个时间单位执行一次。
那么,时间单位又是什么呢?
Cron 表达式中的五个部分分别代表:分钟、小时、日、月、星期。
所以,上面这句 Cron 表达式的意思是:从当前开始,每分钟执行一次。
而这里要执行的内容,就是 jobTemplate 定义的 Job 了。
所以,这个 CronJob 对象在创建 1 分钟后,就会有一个 Job 产生了,如下所示:
$ kubectl create -f ./cronjob.yaml
cronjob "hello" created
# 一分钟后
$ kubectl get jobs
NAME DESIRED SUCCESSFUL AGE
hello-4111706356 1 1 2s
此时,CronJob 对象会记录下这次 Job 执行的时间:
$ kubectl get cronjob hello
NAME SCHEDULE SUSPEND ACTIVE LAST-SCHEDULE
hello */1 * * * * False 0 Thu, 6 Sep 2018 14:34:00 -070
需要注意的是,由于定时任务的特殊性,很可能某个 Job 还没有执行完,另外一个新 Job 就产生了。这时候,你可以通过 spec.concurrencyPolicy 字段来定义具体的处理策略。比如:
- concurrencyPolicy=Allow,这也是默认情况,这意味着这些 Job 可以同时存在;
- concurrencyPolicy=Forbid,这意味着不会创建新的 Pod,该创建周期被跳过;
- concurrencyPolicy=Replace,这意味着新产生的 Job 会替换旧的、没有执行完的 Job。
而如果某一次 Job 创建失败,这次创建就会被标记为“miss”。当在指定的时间窗口内,miss 的数目达到 100 时,那么 CronJob 会停止再创建这个 Job。
这个时间窗口,可以由 spec.startingDeadlineSeconds 字段指定。比如 startingDeadlineSeconds=200,意味着在过去 200 s 里,如果 miss 的数目达到了 100 次,那么这个 Job 就不会被创建执行了。