前面我们学习了 Deployment、Statefulset、Daemonset 这些工作负载,它们可以帮助我们在不同的场景下运行长伺型(Long Running)的服务。

但是有一类业务(一次性作业和定时任务)运行完就结束了,不需要长期运行,如果使用上述的那些工作负载就无法满足我们的要求。比如 Pod 运行结束后,会被 Deployment、Statefulset 控制器重启或者创建新的副本替换掉,而这并不是我们期望的行为。

所以说,对于这类作业任务,我们需要新的工作负载类型来描述。在 Kubernetes 中,我们分别用 Job 和 Cronjob 来描述一次性任务和定时任务。

我们先来看看 Job。

Job

我通过一个官方的例子来带你了解这个工作负载类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: batch/v1beta1
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

这个 Job 负责计算 π 到小数点后的 2000 位,并将结果打印出来。

我们可以通过 kubectl create 命令将该 Job 创建出来:

1
2
$ kubectl create -f https://kubernetes.io/examples/controllers/job.yaml
job.batch/pi created

创建好了以后,我们来看下这个 Job:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kubectl describe jobs/pi
Name:           pi
Namespace:      default
Selector:       controller-uid=4f8027d0-cac1-42ea-b5f8-dbb4d9c9f67a
Labels:         controller-uid=4f8027d0-cac1-42ea-b5f8-dbb4d9c9f67a
                job-name=pi
Annotations:    <none>
Parallelism:    1
Completions:    1
Start Time:     Mon, 02 Dec 2020 15:04:52 +0200
Completed At:   Mon, 02 Dec 2020 15:06:39 +0200
Duration:       65s
Pods Statuses:  0 Running / 1 Succeeded / 0 Failed
Pod Template:
  Labels:  controller-uid=c9948307-e56d-4b5d-8302-ae2d7b7da67c
           job-name=pi
  Containers:
   pi:
    ...
Events:
  Type    Reason            Age   From            Message
  ----    ------            ----  ----            -------
  Normal  SuccessfulCreate  4m    job-controller  Created pod: pi-jk2k7

在这段代码中,有几点需要特别注意下。

系统自动给 Job 添加了 Selector,即 controller-uid=4f8027d0-cac1-42ea-b5f8-dbb4d9c9f67a,后面的这个 uid 就是指该 Job 自己的 uid。

Job 上会自动被加上了 Label,即 controller-uid=4f8027d0-cac1-42ea-b5f8-dbb4d9c9f67a 和 job-name=pi。

Job 中 spec.podTemplate 中也被加上了 Label,即 controller-uid=4f8027d0-cac1-42ea-b5f8-dbb4d9c9f67a 和 job-name=pi。这样 Job 就可以通过这些 Label 和 Pod 关联起来,从而控制 Pod 的创建、删除等操作。

我们可以通过这些 Label 来找到对应的 Pod。你可以直接使用 Job 的名字,这个最简洁最方便:

1
kubectl get pods --selector=job-name=pi

或者你也可以选择使用 Job 的 uid:

1
kubectl get pods --selector=controller-uid=4f8027d0-cac1-42ea-b5f8-dbb4d9c9f67a

我们可以看到,由 Job 创建出来的 Pod 已经运行结束,为 Completed 状态。

1
2
NAME       READY    STATUS       RESTARTS    AGE
pi-jk2k7   0/1      Completed    0           2m

如果说 Pod 运行过程中异常退出了,那就会根据的 Job 中 PodTemplate 定义的重启策略(restart policy)来操作。对于 Job 来说,我们当然不希望一直重启,因此这里的 restartPolicy 只能为 Never 或者 OnFailure。

如果说创建出来的 Pod 一直由于某些原因,导致运行不成功,怎么办呢?这个时候Job 控制器会根据 spec.backoffLimit 中定义的数值来限制 Pod 失败的次数。默认值是 6,我们在例子中设置为 4。达到这个次数以后,Job controller 便不会再新建 Pod,并直接停止运行这个 Job,将其标记为 Failure。

Job 还支持创建多个 Pod 并发地运行,我们来看另一个官方的例子“使用工作队列进行精细的并行处理”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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
      restartPolicy: OnFailure

在 Job 的定义中通过 spec.parallelism 字段,我们可以指定并发运行的 Pod 数目。我们这里指定为 2,也就是会创建 2 个同时运行的 Pod。

例子中的 2 个 Pod 都会不停地从队列中获取数据进行处理,直到队列为空后退出,运行结束。

Job 还支持其他的工作模式,比如通过模板渲染 Job 来支持批量任务的处理等等。你可以参照官方文档来解锁 Job 更多地使用场景,并动手学习实践。

通常来说,Job 运行结束,即状态为 Completed 或 Failure 时,我们并不需要在系统中继续保留该对象,尤其是这种对象较多的时候,会给 kube-apiserver 的 cache 以及系统访问带来很大的压力。

这个时候我们就可以使用TTL 控制器提供 的 TTL 能力了。

我们只需要在 Job 的 spec.ttlSecondsAfterFinished 字段设置一下,就可以让该控制器帮我们自动清理掉已经结束的资源,包括 Job 本身及其关联的 Pod 对象。

我们来看下面这个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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 在运行结束 100 秒之后就被自动清理删除了,包括创建出来的 Pod。

目前这种 TTL 的能力还处于 Alpha 阶段,如果你要使用的话,需要手动开启 TTLAfterFinished 这个 feature gate,具体可以参考 TTL 控制器的文档学习如何打开和使用这个功能。

我们再来看看 CronJob。

CronJob

从名字就可以看出来,这个工作负载是用于定时任务的,比如每隔 1 分钟执行 1 次任务。

我们来看一个官方的 CronJob 的例子,这个例子比较简单明了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: hello # cronjob 名字
spec:
  schedule: "*/1 * * * *" # job执行的周期,通过 cron 格式来标明
  jobTemplate: # job模板
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox
            imagePullPolicy: IfNotPresent
            args:
            - /bin/sh
            - -c
            - date; echo Hello from the Kubernetes cluster
          restartPolicy: OnFailure

CronJob 通过 spec.schedule 字段来标明 Job 被创建和执行的周期,该字段段用Cron格式编写。Cron 的基本格式为:

1
<分钟> <小时> <日> <月> <星期>

其中分钟的值从 0 到 59,小时的值从 0 到 23,日的值从 1 到 31,月的值从 1 到 12,星期的值从 0 到 6,0 表示星期日。

Cron 还支持“*,-/”等字符,其中 * 是个通配符,可以匹配任何值;/ 则表示起始时间触发,然后每隔一个固定时间触发一次。例如我们如果在分钟中设置 10/20,则表示第一次触发在第 10 分钟,接下来每隔 20 分钟触发一次,也就是第 30 分钟、第 50 分钟等依次往后的时间点触发一次。

所以我们例子中的"*/1 * * * *“表示每隔一分钟触发一次新 Job 的执行。

例子中的 spec.jobTemplate 指定了 Job 的模板,CronJob 控制器会根据 Cron 设置的时间触发新的 Job 创建。因此,我们修改 CronJob 的 spec,只会影响新 Job 的spec 配置,对于已经创建的 Job spec 不会有任何影响。

CronJob 会帮助我们管理 Job,比如自动清理运行完的 Job;也就是说,由 CronJob 管理的 Job,我们不需要去配置上文提到的 TTL 自动清理,CronJob 控制器会自动帮我们清理。CronJob 通过 spec.successfulJobsHistoryLimit 和 spec.failedJobsHistoryLimit 来限制保留已完成的 Job 数量,确保不会有大量的 Job 残留在系统中。默认值分别为 3 和 1。

当然在 CronJob 被触发,创建新的 Job 的时候,还会出现一种情形:上一次触发的 Job 还未执行完成。如果这个时候触发了另一个新 Job 的创建,势必会导致任务重叠。此时就需要你结合自己的业务来考虑这种行为对业务的影响了。

你可以在 spec.concurrentPolicy 中配置:

设置为 Allow,这也是默认的值,允许并发任务的执行;

设置为 Forbid,不允许并发任务执行;

设置为 Replace,用新的 Job 来替换当前正在运行的老的 Job。

写在最后

这一讲,我带你了解了如何在 Kubernetes 中设置定时任务。所有 Cronjob 中的 schedule 字段中的时间都是基于 kube-controller-manager 的时区。我们在搭建环境的时候,最好将各个节点上的时间进行同步,这样可以避免很多奇奇怪怪的问题。

如果你对本节课有什么想法或者疑问,欢迎你在留言区留言,我们一起讨论。

-– ### 精选评论 ##### **5661: > 老师,k8s弃用docker,操作层面会有哪些影响,应该不会影响部署和拉起docker制作的镜像吧