Kubernetes — Job与CronJob

有一类做业显然不知足这样的条件,这就是“离线业务”,或者叫做 Batch Job(计算业务)。shell

这 种业务在计算完成后就直接退出了,而此时若是你依然用 Deployment 来管理这种业务的话,就会 发现 Pod 会在计算结束后退出,而后被 Deployment Controller 不断地重启;而像“滚动更 新”这样的编排功能,更无从谈起了。 因此,早在 Borg 项目中,Google 就已经对做业进行了分类处理,提出了 LRS(Long Running Service)和 Batch Jobs 两种做业形态,对它们进行“分别管理”和“混合调度”。ubuntu

不过,在 2015 年 Borg 论文刚刚发布的时候,Kubernetes 项目并不支持对 Batch Job 的管理。直 到 v1.4 版本以后,社区才逐步设计出了一个用来描述离线业务的 API 对象,它的名字就是:Job。小程序

Job API 对象的定义很是简单,我来举个例子,以下所示:api

 

job.yaml并发

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

 

此时,相信你对 Kubernetes 的 API 对象已经再也不陌生了。在这个 Job 的 YAML 文件里,你确定一 眼就会看到一位“老熟人”:Pod 模板,即 spec.template 字段。app

在这个 Pod 模板中,我定义了一个 Ubuntu 镜像的容器(准确地说,是一个安装了 bc 命令的 Ubuntu 镜像),它运行的程序是: 函数

echo "scale=10000; 4*a(1)" | bc -l 

其中,bc 命令是 Linux 里的“计算器”;-l 表示,我如今要使用标准数学库;而 a(1),则是调用数 学库中的 arctangent 函数,计算 atan(1)。这是什么意思呢?工具

中学知识告诉咱们:tan(π/4) = 1。因此,4*atan(1)正好就是π,也就是 3.1415926…。  ui

 

因此,这其实就是一个计算π值的容器。而经过 scale=10000,我指定了输出的小数点后的位数是 10000。spa

在个人计算机上,这个计算大概用时 1 分 54 秒。 可是,跟其余控制器不一样的是,Job 对象并不要求你定义一个 spec.selector 来描述要控制哪些 Pod。具体缘由,我立刻会讲解到。 如今,咱们就能够建立这个 Job 了:

kubectl create -f job.yaml

  

在成功建立后,咱们来查看一下这个 Job 对象,以下所示:

$ kubectl describe jobs/pi
Name:             pi
Namespace:        default
Selector:         controller-uid=c2db599a-2c9d-11e6-b324-0209dc45a495
Labels:           controller-uid=c2db599a-2c9d-11e6-b324-0209dc45a495
                  job-name=pi
Annotations:      <none>
Parallelism:      1
Completions:      1
..
Pods Statuses:    0 Running / 1 Succeeded / 0 Failed
Pod Template:
  Labels:       controller-uid=c2db599a-2c9d-11e6-b324-0209dc45a495
                job-name=pi
  Containers:
   ...
  Volumes:              <none>
Events:
  FirstSeen    LastSeen    Count    From            SubobjectPath    Type        Reason            Message
  ---------    --------    -----    ----            -------------    --------    ------            -------
  1m           1m          1        {job-controller }                Normal      SuccessfulCreate  Created pod: pi-rq5rl

  

能够看到,这个 Job 对象在建立后,它的 Pod 模板,被自动加上了一个 controller-uid=< 一个随 机字符串 > 这样的 Label。

而这个 Job 对象自己,则被自动加上了这个 Label 对应的 Selector,从 而 保证了 Job 与它所管理的 Pod 之间的匹配关系。

而 Job Controller 之因此要使用这种携带了 UID 的 Label,就是为了不不一样 Job 对象所管理的 Pod 发生重合。须要注意的是,这种自动生成的 Label 对用户来讲并不友好,因此不太适合推广到 Deployment 等长做业编排对象上。 接下来,咱们能够看到这个 Job 建立的 Pod 进入了 Running 状态,这意味着它正在计算 Pi 的值。

kubectl get pods
NAME                                READY     STATUS    RESTARTS   AGE
pi-rq5rl                            1/1       Running   0          10s

  

而几分钟后计算结束,这个 Pod 就会进入 Completed 状态:

kubectl get pods
NAME                                READY     STATUS      RESTARTS   AGE
pi-rq5rl                            0/1       Completed   0          4m

  

这也是咱们须要在 Pod 模板中定义 restartPolicy=Never 的缘由:离线计算的 Pod 永远都不该该 被重启,不然它们会再从新计算一遍。

此时,咱们经过 kubectl logs 查看一下这个 Pod 的日志,就能够看到计算获得的 Pi 值已经被打印 了出来:

kubectl logs pi-rq5rl
3.141592653589793238462643383279...

  

这时候,你必定会想到这样一个问题,若是这个离线做业失败了要怎么办?

好比,咱们在这个例子中定义了 restartPolicy=Never,那么离线做业失败后 Job Controller 就 会不断地尝试建立一个新 Pod,以下所示:

kubectl get pods
NAME                                READY     STATUS              RESTARTS   AGE
pi-55h89                            0/1       ContainerCreating   0          2s
pi-tqbcz                            0/1       Error               0          5s

  

能够看到,这时候会不断地有新 Pod 被建立出来。 固然,这个尝试确定不能无限进行下去。

因此,咱们就在 Job 对象的 spec.backoffLimit 字段里定 义了重试次数为 4(即,backoffLimit=4),而这个字段的默认值是 6。 须要注意的是,Job Controller 从新建立 Pod 的间隔是呈指数增长的,即下一次从新建立 Pod 的 动做会分别发生在 10 s、20 s、40 s …后。 而若是你定义的 restartPolicy=OnFailure,那么离线做业失败后,Job Controller 就不会去尝试 建立新的 Pod。

可是,它会不断地尝试重启 Pod 里的容器。这也正好对应了 restartPolicy 的含义 (你也能够借此机会再回顾一下第 15 篇文章《深刻解析 Pod 对象(二):使用进阶》中的相关内 容)。 如前所述,当一个 Job 的 Pod 运行结束后,它会进入 Completed 状态。可是,若是这个 Pod 因 为某种缘由一直不愿结束呢? 在 Job 的 API 对象里,有一个 spec.activeDeadlineSeconds 字段能够设置最长运行时间,好比:

spec:
 backoffLimit: 5
 activeDeadlineSeconds: 100

  

一旦运行超过了 100 s,这个 Job 的全部 Pod 都会被终止。而且,你能够在 Pod 的状态里看到终 止的缘由是 reason: DeadlineExceeded。

以上,就是一个 Job API 对象最主要的概念和用法了。不过,离线业务之因此被称为 Batch Job, 固然是由于它们能够以“Batch”,也就是并行的方式去运行。

接下来,我就来为你讲解一下Job Controller 对并行做业的控制方法。 在 Job 对象中,负责并行控制的参数有两个: 1. spec.parallelism,它定义的是一个 Job 在任意时间最多能够启动多少个 Pod 同时运行; 2. spec.completions,它定义的是 Job 至少要完成的 Pod 数目,即 Job 的最小完成数。

这两个参数听起来有点儿抽象,因此我准备了一个例子来帮助你理解。 如今,我在以前计算 Pi 值的 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。 接下来,咱们来建立这个 Job 对象:

kubectl create -f job.yaml

  

能够看到,这个 Job 其实也维护了两个状态字段,即 DESIRED 和 SUCCESSFUL,以下所示:

kubectl get job
NAME      DESIRED   SUCCESSFUL   AGE
pi        4         0            3s

其中,DESIRED 的值,正是 completions 定义的最小完成数。 而后,咱们能够看到,这个 Job 首先建立了两个并行运行的 Pod 来计算 Pi:  

kubectl get pods
NAME       READY     STATUS    RESTARTS   AGE
pi-5mt88   1/1       Running   0          6s
pi-gmcq5   1/1       Running   0          6s

  

而在 40 s 后,这两个 Pod 相继完成计算。 这时咱们能够看到,每当有一个 Pod 完成计算进入 Completed 状态时,就会有一个新的 Pod 被自 动建立出来,而且快速地从 Pending 状态进入到 ContainerCreating 状态:

kubectl get pods
NAME       READY     STATUS    RESTARTS   AGE
pi-gmcq5   0/1       Completed   0         40s
pi-84ww8   0/1       Pending   0         0s
pi-5mt88   0/1       Completed   0         41s
pi-62rbt   0/1       Pending   0         0s

$ kubectl get pods
NAME       READY     STATUS    RESTARTS   AGE
pi-gmcq5   0/1       Completed   0         40s
pi-84ww8   0/1       ContainerCreating   0         0s
pi-5mt88   0/1       Completed   0         41s
pi-62rbt   0/1       ContainerCreating   0         0s

  

紧接着,Job Controller 第二次建立出来的两个并行的 Pod 也进入了 Running 状态:

kubectl get pods 
NAME       READY     STATUS      RESTARTS   AGE
pi-5mt88   0/1       Completed   0          54s
pi-62rbt   1/1       Running     0          13s
pi-84ww8   1/1       Running     0          14s
pi-gmcq5   0/1       Completed   0          54s

最终,后面建立的这两个 Pod 也完成了计算,进入了 Completed 状态。 这时,因为全部的 Pod 均已经成功退出,这个 Job 也就执行完了,因此你会看到它的 SUCCESSFUL 字段的值变成了 4:  

 

kubectl get pods 
NAME       READY     STATUS      RESTARTS   AGE
pi-5mt88   0/1       Completed   0          5m
pi-62rbt   0/1       Completed   0          4m
pi-84ww8   0/1       Completed   0          4m
pi-gmcq5   0/1       Completed   0          5m

$ kubectl get job
NAME      DESIRED   SUCCESSFUL   AGE
pi        4         4            5m

  

经过上述 Job 的 DESIRED 和 SUCCESSFUL 字段的关系,咱们就能够很容易地理解Job Controller 的工做原理了。

首先,Job Controller 控制的对象,直接就是 Pod。 其次,Job Controller 在控制循环中进行的调谐(Reconcile)操做,是根据实际在 Running 状态 Pod 的数目、已经成功退出的 Pod 的数目,以及 parallelism、completions 参数的值共同计算出 在这个周期里,应该建立或者删除的 Pod 数目,而后调用 Kubernetes API 来执行这个操做。 以建立 Pod 为例。在上面计算 Pi 值的这个例子中,当 Job 一开始建立出来时,实际处于 Running 状态的 Pod 数目 =0,已经成功退出的 Pod 数目 =0,而用户定义的 completions,也就是最终用 户须要的 Pod 数目 =4。

因此,在这个时刻,须要建立的 Pod 数目 = 最终须要的 Pod 数目 - 实际在 Running 状态 Pod 数 目 - 已经成功退出的 Pod 数目 = 4 - 0 - 0= 4。也就是说,Job Controller 须要建立 4 个 Pod 来 纠正这个不一致状态。 但是,咱们又定义了这个 Job 的 parallelism=2。也就是说,咱们规定了每次并发建立的 Pod 个数 不能超过 2 个。

因此,Job Controller 会对前面的计算结果作一个修正,修正后的指望建立的 Pod 数目应该是:2 个。

这时候,Job Controller 就会并发地向 kube-apiserver 发起两个建立 Pod 的请求。 相似地,若是在此次调谐周期里,Job Controller 发现实际在 Running 状态的 Pod 数目,比 parallelism 还大,那么它就会删除一些 Pod,使二者相等。 综上所述,Job Controller 实际上控制了,做业执行的并行度,以及总共须要完成的任务数这两个 重要参数。而在实际使用时,你须要根据做业的特性,来决定并行度(parallelism)和任务数 (completions)的合理取值。

接下来,我再和你分享三种经常使用的、使用 Job 对象的方法。

外部管理器 +Job 模板

 

第一种用法,也是最简单粗暴的用法:外部管理器 +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 时,咱们只要注意以下两个方面便可:

  • 1. 建立 Job 时,替换掉 $ITEM 这样的变量;
  • 2. 全部来自于同一个模板的 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

  

这个模式看起来虽然很“傻”,但倒是 Kubernetes 社区里使用 Job 的一个很广泛的模式。

缘由很简单:大多数用户在须要管理 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 处理完成了。

所 以说,这种用法,对应的就是“任务总数固定”的场景。

指定并行度

 

第三种用法,也是很经常使用的一个用法:指定并行度(parallelism),但不设置固定的 completions 的值。 此时,你就必须本身想办法,来决定何时启动新 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 之间是否是有资源的竞争和协同等等。 因此,在今天这篇文章中,我就再也不展开 Job 的用法了。

由于,在实际场景里,要么干脆就用第一 种用法来本身管理做业;要么,这些任务 Pod 之间的关系就不那么“单纯”,甚至仍是“有状态应 用”(好比,任务的输入 / 输出是在持久化数据卷里)。在这种状况下,我在后面要重点讲解的 Operator,加上 Job 对象一块儿,可能才能更好的知足实际离线任务的编排需求。 最后,我再来和你分享一个很是有用的 Job 对象,叫做: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

  

在这个 YAML 文件中,最重要的关键词就是jobTemplate。看到它,你必定恍然大悟,原来 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 字段来定义具体的处理策略。

好比:

  • 1. concurrencyPolicy=Allow,这也是默认状况,这意味着这些 Job 能够同时存在;
  • 2. concurrencyPolicy=Forbid,这意味着不会建立新的 Pod,该建立周期被跳过;
  • 3. concurrencyPolicy=Replace,这意味着新产生的 Job 会替换旧的、没有执行完的 Job。

而若是某一次 Job 建立失败,此次建立就会被标记为“miss”。当在指定的时间窗口内,miss 的数 目达到 100 时,那么 CronJob 会中止再建立这个 Job。

这个时间窗口,能够由 spec.startingDeadlineSeconds 字段指定。好比 startingDeadlineSeconds=200,意味着在过去 200 s 里,若是 miss 的数目达到了 100 次,那么 这个 Job 就不会被建立执行了。 总结 在今天这篇文章中,我主要和你分享了 Job 这个离线业务的编排方法,讲解了 completions 和 parallelism 字段的含义,以及 Job Controller 的执行原理。

紧接着,我经过实例和你分享了 Job 对象三种常见的使用方法。可是,根据我在社区和生产环境中 的经验,大多数状况下用户仍是更倾向于本身控制 Job 对象。因此,相比于这些固定的“模式”, 掌握 Job 的 API 对象,和它各个字段的准确含义会更加剧要。

相关文章
相关标签/搜索