一. 简介

Deployment、StatefulSet,以及 DaemonSet 这三个主要编排的对象,都是“在线业务”,都属于Long Running Task(长作业)
但是有一类作业显然不满足这样的条件,这就是“离线业务”,叫作Batch Job(计算业务)。针对这种在计算完成后就直接退出的业务,可以使用我们今天的重点:Job来解决。

关于本文的项目的代码,都放于链接:GitHub资源

二. Job

2.1 案例

如下,我们创建一个计算pi后面100位的案例,需要执行10个这样的任务。
demo-job.yaml文件如下:

apiVersion: batch/v1
kind: Job
metadata:
  name: demo-job
spec:
  parallelism: 3
  completions: 10
  template:
    spec:
      containers:
      - name: pi
        image: resouer/ubuntu-bc
        command: ["sh", "-c", "echo 'scale=100; 4*a(1)' | bc -l "]
      restartPolicy: Never
  backoffLimit: 4
  activeDeadlineSeconds: 300

2.2 检查Selector

运行完毕后,我们将看到如下的状态:

kubectl describe jobs/demo-job
# result
Name:                     demo-job
Namespace:                default
Selector:                 controller-uid=bde35f54-a1f8-4f1b-845f-d42e9c570f23
Labels:                   controller-uid=bde35f54-a1f8-4f1b-845f-d42e9c570f23
                          job-name=demo-job
Annotations:              <none>
Parallelism:              3
Completions:              10
Start Time:               Sat, 27 Mar 2021 23:58:51 +0000
Completed At:             Sat, 27 Mar 2021 23:59:11 +0000
Duration:                 20s
Active Deadline Seconds:  300s
Pods Statuses:            0 Running / 10 Succeeded / 0 Failed

可以看到,这个 Job 对象在创建后,它的 Pod 模板,被自动加上了一个 controller-uid=< 一个随机字符串 > 这样的 Label。而这个 Job 对象本身,则被自动加上了这个 Label 对应的 Selector,从而 保证了 Job 与它所管理的 Pod 之间的匹配关系。之所以要使用这种携带了 UID 的 Label,就是为了避免不同 Job 对象所管理的 Pod 发生重合。

2.3 参数

关于上面的YAML参数,详细分析如下:

  • spec.backoffLimit
    指定在标记此作业失败之前重试的次数,默认为6。并且,Job Controller 重新创建 Pod 的间隔是呈指数增加的,即下一次重新创建 Pod 的动作会分别发生在 10 s、20 s、40 s …后。
  • spec.activeDeadlineSeconds
    指定相对于startTime的持续时间(以秒为单位),在系统尝试终止该作业之前,该作业可能处于活动状态;值必须为正整数。例如,一旦运行超过了 300 s,这个 Job 的所有 Pod 都会被终止。并且,可以在 Pod 的状态里看到终止的原因是reason: DeadlineExceeded
  • restartPolicy
    restartPolicy 在 Job 对象里只允许被设置为 Never 和 OnFailure,关于restartPolicy有如下俩种选项:
    • Never
      配置为当前参数后,那么离线作业失败后Job Controller 就会不断地尝试创建一个新 Pod。
      当然,这个尝试肯定不能无限进行下去,这个与spec.backoffLimit参数是配合使用的。
    • OnFailure
      配置为当前参数后,那么离线作业失败后,Job Controller就不会去创建新的 Pod,而是会不断地尝试重启 Pod 里的容器。

三. Job Controller

3.1 原理

Job Controller 控制的对象就是 Pod。
其次,Job Controller 在控制循环中进行的调谐(Reconcile)操作,是根据实际在 Running 状态 Pod 的数目、已经成功退出的 Pod 的数目,以及 parallelism、completions参数的值共同计算出在这个周期里,应该创建或者删除的 Pod 数目,然后调用 Kubernetes API 来执行这个操作。

如果在这次调谐周期里,Job Controller 发现实际在 Running 状态的 Pod 数目,比parallelism还大,那么它就会删除一些 Pod,使两者相等。

Job Controller 实际上控制了作业执行的并行度,以及总共需要完成的任务数这两个重要参数。而在实际使用时,我们需要根据作业的特性,来决定并行度(parallelism)和任务数(completions)的合理取值。

3.2 参数

在 Job 对象中,负责并行控制的参数有两个:

spec.parallelism
指定作业在任何给定时间应运行的Pod的最大期望数目。
((.spec.completions-.status.successful)<.spec.parallelism),即当要做的工作小于最大并行度时,稳定状态下运行的Pod的实际数量将小于此数量。
spec.completion
指定与作业一起运行所需的成功完成的Pod数量。
设置为nil表示任何Pod的成功都表示所有Pod的成功,并允许并行性具有任何正值。设置为1意味着并行度被限制为1,并且pod的成功标志着工作的成功。

3.3 场景

3.3.1 parallelism和completions都确定

在这种模式下使用 Job 对象,completionsparallelism这两个字段都应该使用默认值 1,而不应该由我们自行设置。
一般实际落地方案就是:外部管理器 +Job 模板。作业 Pod 的并行控制,应该完全交由外部工具来进行管理(比如,KubeFlow)。

3.3.2 parallelism不确定,completions确定

当只关心最后是否有指定数目(spec.completions)个任务成功退出,而不关系任务并行度时。
一般采用:外部MQ+Job。利用确定的Pod数,主动去消费MQ里面的消息达到目标数量即可。
例如,每个 Pod 只需要将任务信息读取出来,处理完成,然后退出即可。而作为用户,只关心最终一共有 10 个计算任务启动并且退出,只要这个目标达到,就认为整个 Job 处理完成了。

3.3.3 parallelism确定,completions不确定

由于任务数目的总数不固定,所以每一个 Pod 必须能够知道,什么时候可以退出。比如,借助上面例子,简单地以“队列为空”,作为任务全部完成的标志。
在这种情况下,难点在于任务的总数是未知的,所以不仅需要一个工作队列来负责任务分发,还需要能够判断工作队列已经为空。

四. Cron Job

4.1 原理

此处先看demo-cronjob.yaml文件:

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: demo-cronjob
spec:
  schedule: "*/5 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox
            args:
            - /bin/sh
            - -c
            - date; echo Hello World
          restartPolicy: OnFailure
  concurrencyPolicy: Replace
  startingDeadlineSeconds: 500

在这个 YAML 文件中,最重要的关键词就是jobTemplateCronJob是一个 Job 对象的控制器。CronJob 与 Job 的关系,正如同 Deployment 与 ReplicaSet 的关系一样。

4.2 Unix Cron

CronJob 是一个专门用来管理 Job 对象的控制器。只不过,它创建和删除 Job 的依据,是 schedule 字段定义的、一个标准的Unix Cron格式的表达式。
Cron 表达式中的五个部分分别代表:分钟、小时、日、月、星期。

*/5 * * * * 这个 Cron 表达式里 */5 中的 * 表示从 0 开始,/ 表示“每”,1 表示偏移量。含义就是:从 0 开始,每 5 个时间单位执行一次。

关于上面的任务,执行如下俩条指令可以查看结果(等待5min):

kubectl get jobs
kubectl get cronjob demo-cronjob

结果如下:

4.3 并发策略

由于定时任务的特殊性,很可能某个 Job 还没有执行完,另外一个新 Job 就产生了。这时候,你可以通过 spec.concurrencyPolicy字段来定义具体的处理策略。
concurrencyPolicy 有如下三种配置参数:

  • Allow
    允许CronJobs并发运行,默认值。
  • Forbid
    禁止同时运行,如果前一个运行尚未完成,则跳过下一个运行。
  • Replace
    取消当前正在运行的作业,并将其替换为新作业。

如果由于任何原因错过了计划的时间,则以秒为单位的可选截止期限,用于开始工作。当在指定的时间窗口内,miss 的数目达到 100 时,那么 CronJob 会停止再创建这个 Job。这个时间窗口,可以由spec.startingDeadlineSeconds字段指定。
在上面例子中,startingDeadlineSeconds=500,意味着在过去 500 s 里,如果 miss 的数目达到了 100 次,那么这个 Job 就不会被创建执行了。

五. 总结

关于Job,Cron Job 和 Job Controller 这三者,都是采用了控制器模式设计,利用对象管理对象,是一种树状的层级管理。这点与Deployment,ReplicaSet和Deployment Controller之间的设计理念一致。
用一个对象控制另一个对象,是 Kubernetes 编排的精髓所在!

转自:https://blog.wyatt.plus/archives/152