前面讲述的无论是Deployment,StatefulSet还是DeamonSet管理的都是在线业务,也就是:Long Running Task(长作业)。这些应用一直保持运行状态。

而这节讲述的就是只会运行一次的离线任务,也就是执行完之后就会退出。不需要进行重启与滚动更新。

Job

这是v1.4版本之后,社区设计出的一个用来描述离线业务的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

启动了一个Ubuntu镜像的容器,然后运行echo "scale=10000; 4*a(1)" | bc -l 命令。这就是一个计算Π值小数点后10000的计算任务。不会重启,即restartPolicy: Nevwer。 Job对象并不要求定义一个spec.selector来描述要控制哪些Pod。

创建这个Job

$ kubectl create -f job.yaml

查看创建的Job对象可以看到,其Pod模板中自动添加了一个 controller-uid=< 一个随机字符串 > 这样的 Label。这个Job对象本身自动被加上了这个Label对应的Selector,保证了Job换个它所管理的Pod之间的匹配关系。所以前面提到过不需要定义spec.selector字段,因为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

Pod计算结束,就会进入Completed状态。重启策略为Never,所以离线计算的Pod永远都不会被重启。

Note

事实上,restartPolicy 在 Job 对象里只允许被设置为 Never 和 OnFailure;而在 Deployment 对象里,restartPolicy 则只允许被设置为 Always。

虽然Job不会重启,但是如果离线任务失败是会进行重试的,Job Controller会不断重试创建一个新Pod直到计算成功。为了避免无限重试,sepc.backoffLimit定义能够重试的次数,默认是6。重试之之间的时间间隔是呈指数增加的,10s, 20s, 40s…。

一个Job的Pod运行结束进入Completed状态,但是为了避免一个Pod因为某种原因无法退出,通过sepc.activeDeadlineSeconds字段可以设置Pod的最长运行时间,一旦超过了这个时间,这个Job的所有Pod都会被终止

Job Controller并行作业控制

负责并行控制的参数有两个:

  • spec.parallelism:定义一个Pod在任意时间最多可以启动多少个Pod同时运行
  • spec.completions:定义Job至少要完成的Pod数目,即Job的最小完成数

简单来说就是一次最多运行的任务数和最终需要达到的完成数。

给Job的YAML配置添加这两个参数:

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

这里表示一次最多运行2个Pod,最终需要完成4个Pod。最初启动两个Pod完成计算任务,每完成一个就启动一个新的Pod继续计算,直到达到4个最小完成数。

JobController控制的对象就是Pod,根据前面提到的两个参数和集群中的状态,即实际Running状态Pod的数目、已经成功退出的Pod的数目共同计算出这个周期里,应该创建或者删除的Pod的数目,然后调用k8s的API来执行操作。

需要创建的 Pod 数目 = 最终需要的 Pod 数目 - 实际在 Running 状态 Pod 数目 - 已经成功退出的 Pod 数目

Job对象使用方法

1. 外部管理器+Job模板

这是最简单粗暴的用法。把Job的YAML文件定义为一个模板,然后用一个外部工具来控制这些模板生成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

在模板中引入了类似$ITEM这样的变量,在创建Job是将这些变量进行替换就生成了Job的YAML文件,例如通过shell脚本进行替换:

$ mkdir ./jobs
$ for i in apple banana cherry
do
  cat job-tmpl.yaml | sed "s/\$ITEM/$i/" > ./jobs/job-$i.yaml
done

然后即可运行这些Job。

2. 拥有固定任务数目的Job

就是只关心是否有指定数目(spec.completions)的任务成功退出,并不关心执行时的并行度。

比如下面的例子中,指定了8个最小完成数目,然后还使用了工作队列进行任务分发,这里的生产者是外部程序,Pod作为消费者来从工作队列中获取任务并执行:

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

只关心最终一共有 8 个计算任务启动并且退出,只要这个目标达到,我就认为整个 Job 处理完成了。所以说,这种用法,对应的就是“任务总数固定”的场景

3.指定并行度,不设置固定的completions值

任务的总数是未知的,所以需要想办法判断Job什么时候才算执行结束。 所以需要一个工作队列来负责分发任务,并判断工作队列是否为空

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

上一步可以由Job来决定什么时候任务结束,但是现在需要Pod进行处理,工作队列为空则结束任务:

/* job-wq-2的伪代码 */
for !queue.IsEmpty($BROKER_URL, $QUEUE) {
  task := queue.Pop()
  process(task)
}
print("Queue empty, exiting")
exit

CronJob

这是一个定时任务的Job。 其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

重点是这个字段:jobTemplate,立马就能够明白,CronJob是Job的控制器。通过schedule字段定义的unix cron格式的表达式来决定何时创建或删除Job对象。

这两者的关系与Deployment和ReplicaSet的关系很像。

定时任务很特殊,一个Job可能还没有完成,另一个新的Job可能就需要创建了。通过spec.concurrencyPolicy字段来定义具体的处理策略。

  • Allow,默认情况。表示这些Job可以同时存在。
  • Forbid,不会创建新的Pod,创建周期被跳过。
  • Replace:新产生的Job会替代旧的、没有执行完的Job。

一个Job如果创建失败,那么这次创建就会被标记为miss,在指定时间窗口内达到100次miss,那么这个CronJob就会停止创建这个Job。这个时间窗口可以由spec.startingDeadlineSeconds来指定。


tags: 容器编排 控制器