文章目录
  • 前言
  • 一、DaemonSet
  • 1.1 DaemonSet基本属性
  • 1.2 DaemonSet 如何保证每个 Node 上有且只有一个被管理的 Pod
  • 1.3 DaemonSet控制版本
  • 二、Job
  • 2.1 Job引入
  • 2.2 Job执行
  • Job执行成功
  • Job执行失败
  • 并行执行 Job
  • 尾声


前言

Pod是k8s中最小的运行单元,Pod最常见的控制器就是 Deployment 和 Statefulset, 其他两种 Job/CronJob 、DaemonSet 这样的控制器较少见一些。

一、DaemonSet

1.1 DaemonSet基本属性

顾名思义,DaemonSet 的主要作用,是让你在 Kubernetes 集群里,运行一个 Daemon Pod。 所以,这个 Pod 有如下三个特征:

  1. 这个 Pod 运行在 Kubernetes 集群里的每一个节点(Node)上,每个节点上只有一个这样的 Pod 实例
  2. 当有新的节点加入 Kubernetes 集群后,该 Pod 会自动地在新节点上被创建出来
  3. 而当旧节点被删除后,它上面的 Pod 也相应地会被回收掉。

这个机制听起来很简单,但 Daemon Pod 的意义确实是非常重要的,业务场景包括:

  1. 各种网络插件的 Agent 组件,都必须运行在每一个节点上,用来处理这个节点上的容器网络
  2. 各种存储插件的 Agent 组件,也必须运行在每一个节点上,用来在这个节点上挂载远程存储目录,操作容器的 Volume 目录
  3. 各种监控组件和日志组件,也必须运行在每一个节点上,负责这个节点上的监控信息和日志搜集。

为了弄清楚 DaemonSet 的工作原理,我们还是按照老规矩,先从它的 API 对象的定义说起。

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd-elasticsearch
  namespace: kube-system
  labels:
    k8s-app: fluentd-logging
spec:
  selector:
    matchLabels:
      name: fluentd-elasticsearch
  template:
    metadata:
      labels:
        name: fluentd-elasticsearch
    spec:
      tolerations:
      - key: node-role.kubernetes.io/master
        effect: NoSchedule
      containers:
      - name: fluentd-elasticsearch
        image: k8s.gcr.io/fluentd-elasticsearch:1.20
        resources:
          limits:
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 200Mi
        volumeMounts:
        - name: varlog
          mountPath: /var/log
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
      terminationGracePeriodSeconds: 30
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.

这个 DaemonSet,管理的是一个 fluentd-elasticsearch 镜像的 Pod。这个镜像的功能非常实用:通过 fluentd 将 Docker 容器里的日志转发到 ElasticSearch 中。

可以看到,DaemonSet 跟 Deployment 其实非常相似,只不过是没有 replicas 字段;它也使用 selector 选择管理所有携带了 name=fluentd-elasticsearch 标签的 Pod。

而这些 Pod 的模板,也是用 template 字段定义的。在这个字段中,我们定义了一个使用 fluentd-elasticsearch:1.20 镜像的容器,而且这个容器挂载了两个 hostPath 类型的 Volume,分别对应宿主机的 /var/log 目录和 /var/lib/docker/containers 目录。

显然,fluentd 启动之后,它会从这两个目录里搜集日志信息,并转发给 ElasticSearch 保存。这样,我们通过 ElasticSearch 就可以很方便地检索这些日志了。

需要注意的是,Docker 容器里应用的日志,默认会保存在宿主机的 /var/lib/docker/containers/{{. 容器 ID}}/{{. 容器 ID}}-json.log 文件里,所以这个目录正是 fluentd 的搜集目标。

1.2 DaemonSet 如何保证每个 Node 上有且只有一个被管理的 Pod

DaemonSet 是如何保证每个 Node 上有且只有一个被管理的 Pod 呢?

回答:DaemonSet Controller,首先从 Etcd 里获取所有的 Node 列表,然后遍历所有的 Node。这时,它就可以很容易地去检查,当前这个 Node 上是不是有一个携带了 name=fluentd-elasticsearch 标签的 Pod 在运行。 而检查的结果,可能有这么三种情况:

(1) 没有这种 Pod,那么就意味着要在这个 Node 上创建这样一个 Pod [较复杂, 通过 nodeAffinity 实现, 下面详解]
(2) 有这种 Pod,但是数量大于 1,那就说明要把多余的 Pod 从这个 Node 上删除掉 [简单, 直接调用 Kubernetes API 实现]
(3) 正好只有一个这种 Pod,那说明这个节点是正常的。 [不需要处理该Node]

如何在指定的 Node 上创建新 Pod 呢?

如果你已经熟悉了 Pod API 对象的话,那一定可以立刻说出答案:用 nodeSelector,选择 Node 的名字即可。

不过,在 Kubernetes 项目里,nodeSelector 其实已经是一个将要被废弃的字段了。因为,现在有了一个新的、功能更完善的字段可以代替它,即:nodeAffinity 节点亲和性。我来举个例子:

apiVersion: v1
kind: Pod
metadata:
  name: with-node-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: metadata.name
            operator: In
            values:
            - node-geektime
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

在这个 Pod 里,我声明了一个 spec.affinity 字段,然后定义了一个 nodeAffinity。其中,spec.affinity 字段,是 Pod 里跟调度相关的一个字段。

而在这里,我定义的 nodeAffinity 的含义是:

  • requiredDuringSchedulingIgnoredDuringExecution:它的意思是说,这个 nodeAffinity 必须在每次调度的时候予以考虑。同时,这也意味着你可以设置在某些情况下不考虑这个 nodeAffinity
  • 这个 Pod,将来只允许运行在“metadata.name”是“node-geektime”的节点上。

在这里,你应该注意到 nodeAffinity 的定义,可以支持更加丰富的语法,比如 operator: In(即:部分匹配;如果你定义 operator: Equal,就是完全匹配),这也正是 nodeAffinity 会取代 nodeSelector 的原因之一。

所以,我们的 DaemonSet Controller 会在创建 Pod 的时候,自动在这个 Pod 的 API 对象里,加上这样一个 nodeAffinity 定义。其中,需要绑定的节点名字,正是当前正在遍历的这个 Node。

当然,DaemonSet 并不需要修改用户提交的 YAML 文件里的 Pod 模板,而是在向 Kubernetes 发起请求之前,直接修改根据模板生成的 Pod 对象。这个思路,也正是我在前面讲解 Pod 对象时介绍过的。

此外,DaemonSet 还会给这个 Pod 自动加上另外一个与调度相关的字段,叫作 tolerations。这个字段意味着这个 Pod,会“容忍”(Toleration)某些 Node 的“污点”(Taint)。

而 DaemonSet 自动加上的 tolerations 字段,格式如下所示:

apiVersion: v1
kind: Pod
metadata:
  name: with-toleration
spec:
  tolerations:
  - key: node.kubernetes.io/unschedulable
    operator: Exists
    effect: NoSchedule
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

这个 Toleration 的含义是:“容忍”所有被标记为 unschedulable“污点”的 Node;“容忍”的效果是允许调度。

而在正常情况下,被标记了 unschedulable“污点”的 Node,是不会有任何 Pod 被调度上去的(effect: NoSchedule)。可是,DaemonSet 自动地给被管理的 Pod 加上了这个特殊的 Toleration,就使得这些 Pod 可以忽略这个限制,继而保证每个节点上都会被调度一个 Pod。当然,如果这个节点有故障的话,这个 Pod 可能会启动失败,而 DaemonSet 则会始终尝试下去,直到 Pod 启动成功。

这时,你应该可以猜到,我在前面介绍到的 DaemonSet 的“过人之处”,其实就是依靠 Toleration 实现的。

假如当前 DaemonSet 管理的,是一个网络插件的 Agent Pod,那么你就必须在这个 DaemonSet 的 YAML 文件里,给它的 Pod 模板加上一个能够“容忍”node.kubernetes.io/network-unavailable“污点”的 Toleration。正如下面这个例子所示:

template:
    metadata:
      labels:
        name: network-plugin-agent
    spec:
      tolerations:
      - key: node.kubernetes.io/network-unavailable
        operator: Exists
        effect: NoSchedule
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

在 Kubernetes 项目中,当一个节点的网络插件尚未安装时,这个节点就会被自动加上名为node.kubernetes.io/network-unavailable的“污点”。

而通过这样一个 Toleration,调度器在调度这个 Pod 的时候,就会忽略当前节点上的“污点”,从而成功地将网络插件的 Agent 组件调度到这台机器上启动起来。

至此,通过上面这些内容,你应该能够明白,DaemonSet 其实是一个非常简单的控制器。在它的控制循环中,只需要遍历所有节点,然后根据节点上是否有被管理 Pod 的情况,来决定是否要创建或者删除一个 Pod。

实现方式是:在创建每个 Pod 的时候,DaemonSet 会自动给这个 Pod 加上一个 nodeAffinity,从而保证这个 Pod 只会在指定节点上启动。同时,它还会自动给这个 Pod 加上一个 Toleration,从而忽略节点的 unschedulable“污点”。

当然,你也可以在 Pod 模板里加上更多种类的 Toleration,从而利用 DaemonSet 达到自己的目的。比如,在这个 fluentd-elasticsearch DaemonSet 里,我就给它加上了这样的 Toleration:

tolerations:
- key: node-role.kubernetes.io/master
  effect: NoSchedule
  • 1.
  • 2.
  • 3.

这是因为在默认情况下,Kubernetes 集群不允许用户在 Master 节点部署 Pod。因为,Master 节点默认携带了一个叫作node-role.kubernetes.io/master的“污点”。所以,为了能在 Master 节点上部署 DaemonSet 的 Pod,我就必须让这个 Pod“容忍”这个“污点”。

在理解了 DaemonSet 的工作原理之后,接下来我就通过一个具体的实践来帮你更深入地掌握 DaemonSet 的使用方法。

1.3 DaemonSet控制版本

首先,创建这个 DaemonSet 对象:

# 新建一个daemonset
$ kubectl create -f fluentd-elasticsearch.yaml
  • 1.
  • 2.

DaemonSet Pod资源占用:在 DaemonSet 上,我们一般都应该加上 resources 字段,来限制它的 CPU 和内存使用,防止它占用过多的宿主机资源。 在实际的使用中,强烈建议将 DaemonSet 的 Pod 都设置为 Guaranteed 的 QoS 类型。如果不这样做,一旦 DaemonSet 的 Pod 被回收,它又会立即在原宿主机上被重建出来,这就使得前面资源回收的动作,完全没有意义了。[ 使用 Guaranteed 服务质量,Pod 因为资源不足被删除,就不会又重新新建起来,因为 Guaranteed 服务质量会先检查 Node 上的资源是否满足 Pod ]

QoS(Quality of Service,服务质量)是通过为不同类型的 Pod 分配资源来控制和优化 Kubernetes 集群中的服务性能和可用性。Kubernetes(K8s)中有四种类型的Pod QoS(Quality of Service)级别,分别是:
(1) BestEffort(最低保证):这是最低级别的QoS,表示对Pod的资源使用没有特定的需求,可以与其他Pod共享节点的资源。这些Pod不会被调度程序主动杀死以释放资源,也不会受到其他Pod的限制。
(2) Burstable(可突发):这是介于BestEffort和Guaranteed之间的QoS级别。Pod可以请求并使用特定数量的资源,但若资源不足时,它们仍然可以被调度到节点上,并与其他Pod共享资源。
(3) Guaranteed(保证):这是最高级别的QoS级别,表示Pod对资源的需求是固定且不可削减的。这些Pod具有优先权,并且会优先获得可用资源。如果节点资源不足,调度程序可以选择杀死其他QoS级别较低的Pod来保证Guaranteed级别的Pod的资源需求得到满足。[特点: Pod 的请求值和限制值相等,即request下限和limit上限相同,使得 Kubernetes 可根据此进行资源管理和调度。]
(4) Not to be evicted(禁止驱逐):这是一种特殊的QoS级别,用于在Pod不能被主动杀死以释放资源的情况下标识一个Pod。这可能是由于Pod的configuration属性设置为"do not evict"或者因为正使用弹性分布式存储、系统暂停或其他原因导致Pod无法驱逐。
综上,Guaranteed 是一种可靠性较高的 QoS 类型,Pod 被设置为 Guaranteed 类型时,Kubernetes 会分配足够的资源来满足 Pod 的需求,以确保它们始终可用。

kubectl drain <NodeName> --force --ignore-daemonsets
  • 1.

解释:(1) kubectl drain 是一个 Kubernetes 命令,drain 译为迁移,用于将一个节点上的 Pod 迁移至其他节点。当需要从集群中删除一个节点或维护一个节点时,可以使用该命令确保节点上的 Pod 不会丢失。

(2) 指定 <NodeName> 参数表示要进行 Pod 迁移操作的节点名称,表示这个节点上的 Pod 都迁走。

(3) --force 参数表示强制进行 Pod 迁移操作。使用该参数,即使 Pod 处于未就绪或无法删除的状态,也会强制进行迁移。

(4) --ignore-daemonsets 参数表示在进行 Pod 迁移时忽略守护进程集(DaemonSet)。守护进程集是一种类型的 Pod,在每个节点上都会运行,并且不能被驱逐。

整体解释:使用该命令时,Kubernetes 将调度调动一个控制器(Cordon Controller),将节点设置为不可调度状态,然后将节点上的 Pod 逐个迁移到其他节点。一旦所有 Pod 都被迁移到其他节点,节点将被标记为SchedulingDisabled,表示该节点不可用。另外注意,在使用该命令之前,请确保已经使用适当的方法和策略将负载移到其他可用节点。

通过 kubectl get 查看一下 Kubernetes 集群里的 DaemonSet 对象,如下:

# 查看DaemonSet
$ kubectl get ds -n kube-system fluentd-elasticsearch
NAME                    DESIRED   CURRENT   READY     UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
fluentd-elasticsearch   2         2         2         2            2           <none>          1h

# 查看DaemonSet的Pod
$ kubectl get pod -n kube-system -l name=fluentd-elasticsearch
NAME                          READY     STATUS    RESTARTS   AGE
fluentd-elasticsearch-dqfv9   1/1       Running   0          53m
fluentd-elasticsearch-pf9z5   1/1       Running   0          53m
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

Kubernetes 里比较长的 API 对象都有短名字,比如 DaemonSet 对应的是 ds,Deployment 对应的是 deploy。DaemonSet 和 Deployment 一样,也有 DESIRED、CURRENT 等多个状态字段。这也就意味着,DaemonSet 可以像 Deployment 那样,进行版本管理。这个版本,可以使用 kubectl rollout history 看到:

$ kubectl rollout history daemonset fluentd-elasticsearch -n kube-system
daemonsets "fluentd-elasticsearch"
REVISION  CHANGE-CAUSE
1         <none>
  • 1.
  • 2.
  • 3.
  • 4.

接下来,我们来把这个 DaemonSet 的容器镜像版本到 v2.2.0,生成 DaemonSet 版本2 (后面用来做版本回滚的)

$ kubectl set image ds/fluentd-elasticsearch fluentd-elasticsearch=k8s.gcr.io/fluentd-elasticsearch:v2.2.0 --record -n=kube-system
  • 1.

Kubernetes 命令,用于更新一个指定守护进程集(DaemonSet)中的容器镜像。它将一个容器镜像的新版本指定给一个特定的守护进程集,该守护进程集属于 kube-system 命名空间。

解释命令中的参数:

  • set image 表示要设置容器镜像的命令。
  • ds/fluentd-elasticsearch 指定了要更新镜像的守护进程集的名称。ds 表示 DaemonSet。
  • fluentd-elasticsearch 是要更新的容器的名称。
  • fluentd-elasticsearch=k8s.gcr.io/fluentd-elasticsearch:v2.2.0 指定了新的容器镜像。fluentd-elasticsearch 是容器的名称,k8s.gcr.io/fluentd-elasticsearch:v2.2.0 是要更新的容器镜像的新版本。
  • --record 参数表示将此操作记录到事件日志中。
  • -n=kube-system 参数指定了 Kubernetes 命名空间,kube-system 是一个特殊的命名空间,用于运行 Kubernetes 系统组件和 DaemonSet。

运行该命令后,Kubernetes 将更新 kube-system 命名空间中 fluentd-elasticsearch 守护进程集中的容器镜像为指定版本。

接下来,我们可以使用 kubectl rollout status 命令看到这个“滚动更新”的过程,如下所示:

$ kubectl rollout status ds/fluentd-elasticsearch -n kube-system
Waiting for daemon set "fluentd-elasticsearch" rollout to finish: 0 out of 2 new pods have been updated...
Waiting for daemon set "fluentd-elasticsearch" rollout to finish: 0 out of 2 new pods have been updated...
Waiting for daemon set "fluentd-elasticsearch" rollout to finish: 1 of 2 updated pods are available...
daemon set "fluentd-elasticsearch" successfully rolled out
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

有了版本号,你也就可以像 Deployment 一样,将 DaemonSet 回滚到某个指定的历史版本了。

Deployment 管理这些版本,靠的是“一个版本对应一个 ReplicaSet 对象”。可是,DaemonSet 控制器操作的直接就是 Pod,不可能有 ReplicaSet 这样的对象参与其中。实际上,DaemonSet 的这些版本是通过一种 kind: ControllerRevision 的资源来管理的。

在 Kubernetes 项目中,任何你觉得需要记录下来的状态,都可以被用 API 对象的方式实现。当然,“版本”也不例外。

Kubernetes v1.7 之后添加了一个 API 对象,名叫 ControllerRevision,专门用来记录某种 Controller 对象的版本。比如,你可以通过如下命令查看 fluentd-elasticsearch 对应的 ControllerRevision:

# 查看 controllerrevision 控制版本资源
$ kubectl get controllerrevision -n kube-system -l name=fluentd-elasticsearch
NAME                               CONTROLLER                             REVISION   AGE
fluentd-elasticsearch-64dc6799c9   daemonset.apps/fluentd-elasticsearch   2          1h


$ kubectl describe controllerrevision fluentd-elasticsearch-64dc6799c9 -n kube-system
Name:         fluentd-elasticsearch-64dc6799c9
Namespace:    kube-system
Labels:       controller-revision-hash=2087235575
              name=fluentd-elasticsearch
Annotations:  deprecated.daemonset.template.generation=2
              kubernetes.io/change-cause=kubectl set image ds/fluentd-elasticsearch fluentd-elasticsearch=k8s.gcr.io/fluentd-elasticsearch:v2.2.0 --record=true --namespace=kube-system
API Version:  apps/v1
Data:
  Spec:
    Template:
      $ Patch:  replace
      Metadata:
        Creation Timestamp:  <nil>
        Labels:
          Name:  fluentd-elasticsearch
      Spec:
        Containers:
          Image:              k8s.gcr.io/fluentd-elasticsearch:v2.2.0
          Image Pull Policy:  IfNotPresent
          Name:               fluentd-elasticsearch
...
Revision:                  2   # 这就是版本资源
Events:                    <none>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.

就会看到,这个 ControllerRevision 对象,实际上是在 Data 字段保存了该版本对应的完整的 DaemonSet 的 API 对象。并且,在 Annotation 字段保存了创建这个对象所使用的 kubectl 命令。

接下来,我们可以尝试将这个 DaemonSet 回滚到 Revision=1 时的状态:

#  DaemonSet 回滚到 Revision=1 时的状态
$ kubectl rollout undo daemonset fluentd-elasticsearch --to-revision=1 -n kube-system
daemonset.extensions/fluentd-elasticsearch rolled back
  • 1.
  • 2.
  • 3.

这个 kubectl rollout undo 操作,实际上相当于读取到了 Revision=1 的 ControllerRevision 对象保存的 Data 字段。而这个 Data 字段里保存的信息,就是 Revision=1 时这个 DaemonSet 的完整 API 对象。

所以,现在 DaemonSet Controller 就可以使用这个历史 API 对象,对现有的 DaemonSet 做一次 PATCH 操作(等价于执行一次 kubectl apply -f “旧的 DaemonSet 对象”),从而把这个 DaemonSet“更新”到一个旧版本。

这也是为什么,在执行完这次回滚完成后,你会发现,DaemonSet 的 Revision 并不会从 Revision=2 退回到 1,而是会增加成 Revision=3。这是因为,一个新的 ControllerRevision 被创建了出来。

二、Job

容器按照持续运行的时间可分为两类:服务类容器和工作类容器。

服务类容器通常持续提供服务,需要一直运行,比如 http server,daemon 等。 [Deployment、ReplicaSet 和 DaemonSet管理]
工作类容器则是一次性任务,比如批处理程序,完成后容器就退出。 [Job/CronJob管理]

Job分类:普通任务(Job)和定时任务(CronJob) 一次性执行。
Job应用场景:离线数据处理,视频解码等业务

小结:服务类使用Deployment、ReplicaSet 和 DaemonSet管理,作业类使用Job/CronJob管理

2.1 Job引入

无论是 Deployment、StatefulSet,以及 DaemonSet 这三个编排概念,它们主要编排的对象,都是“在线业务”,即:Long Running Task(长作业)。比如,我在前面举例时常用的 Nginx、Tomcat,以及 MySQL 等等。这些应用一旦运行起来,除非出错或者停止,它的容器进程会一直保持在 Running 状态。

但是,有一类作业显然不满足这样的条件,这就是“离线业务”,或者叫作 Batch Job(计算业务)。这种业务在计算完成后就直接退出了,而此时如果你依然用 Deployment 来管理这种业务的话,就会发现 Pod 会在计算结束后退出,然后被 Deployment Controller 不断地重启;而像“滚动更新”这样的编排功能,更无从谈起了。

所以,早在 Borg 项目中,Google 就已经对作业进行了分类处理,提出了 LRS(Long Running Service)和 Batch Jobs 两种作业形态,对它们进行“分别管理”和“混合调度”。

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

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
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

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

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

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

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

中学知识告诉我们:tan(π/4) = 1。所以,4*atan(1)正好就是π,也就是 3.1415926…。

所以,这其实就是一个计算π值的容器。而通过 scale=10000,我指定了输出的小数点后的位数是 10000。在我的计算机上,这个计算大概用时 1 分 54 秒。

但是,跟其他控制器不同的是,Job 对象并不要求你定义一个 spec.selector 来描述要控制哪些 Pod。具体原因,我马上会讲解到。

现在,我们就可以创建这个 Job 了:

$ kubectl create -f job.yaml
  • 1.

在成功创建后,我们来查看一下这个 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
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

可以看到,这个 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
  • 1.
  • 2.
  • 3.

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

$ kubectl get pods
NAME                                READY     STATUS      RESTARTS   AGE
pi-rq5rl                            0/1       Completed   0          4m
  • 1.
  • 2.
  • 3.

这也是我们需要在 Pod 模板中定义 restartPolicy=Never 的原因:离线计算的 Pod 永远都不应该被重启,否则它们会再重新计算一遍。

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

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

$ kubectl logs pi-rq5rl
3.141592653589793238462643383279...
  • 1.
  • 2.

这时候,你一定会想到这样一个问题,如果这个离线作业失败了要怎么办?

比如,我们在这个例子中定义了 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
  • 1.
  • 2.
  • 3.
  • 4.

可以看到,这时候会不断地有新 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 的含义。

如前所述,当一个 Job 的 Pod 运行结束后,它会进入 Completed 状态。但是,如果这个 Pod 因为某种原因一直不肯结束呢?

在 Job 的 API 对象里,有一个 spec.activeDeadlineSeconds 字段可以设置最长运行时间,比如:

spec:
 backoffLimit: 5
 activeDeadlineSeconds: 100
  • 1.
  • 2.
  • 3.

一旦运行超过了 100 s,这个 Job 的所有 Pod 都会被终止。并且,你可以在 Pod 的状态里看到终止的原因是 reason: DeadlineExceeded。

2.2 Job执行

Job执行成功

先看一个简单的 Job 配置文件 myjob.yml:

[root@k8s-master ~]# cat myjob.yml 
apiVersion: batch/v1
kind: Job
metadata:  
  name: myjob
spec:  
  template:    
    metadata:      
      name: myjob    
    spec:      
      containers:      
      - name: hello      
        image: busybox    
        command: ["echo","hello k8s job"]      
      restartPolicy: Never  #Never  程序退出了就不再重启了,不管正确还是错误退出
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

解释:

  1. batch/v1 是当前 Job 的 apiVersion。
  2. kind 指明当前资源的类型为 Job。
  3. restartPolicy 指定什么情况下需要重启容器。对于 Job,只能设置为 Never 或者 OnFailure。对于其他 controller(比如 Deployment)可以设置为 Always 。

通过 kubectl apply -f myjob.yml 启动 Job。

[root@k8s-master ~]# kubectl apply -f myjob.yml 
job.batch/myjob created
  • 1.
  • 2.

kubectl get job 查看 Job 的状态:

[root@k8s-master ~]# kubectl get job
NAME    COMPLETIONS   DURATION   AGE
myjob   1/1           21s        9m58s
  • 1.
  • 2.
  • 3.

可以看到按照预期启动了一个 Pod,并且已经成功执行。(Pod 执行完毕后容器已经退出)

[root@k8s-master ~]# kubectl get pod
NAME          READY   STATUS      RESTARTS   AGE
myjob-q54fk   0/1     Completed   0          9m53s
  • 1.
  • 2.
  • 3.

kubectl logs 可以查看 Pod 的标准输出:

[root@k8s-master ~]# kubectl logs  myjob-q54fk
hello k8s job
  • 1.
  • 2.

以上是 Pod 成功执行的情况,如果 Pod 失败了会怎么样呢?

Job执行失败

先删除之前的 Job:

[root@k8s-master ~]# kubectl delete -f myjob.yml 
job.batch "myjob" deleted
  • 1.
  • 2.

修改 myjob.yml,故意引入一个错误,只需要修改command。

command: ["error command","hello k8s job"]
  • 1.

运行新的 Job 并查看状态

[root@k8s-master ~]# kubectl apply -f myjob.yml 
job.batch/myjob created
[root@k8s-master ~]# kubectl get job
NAME    COMPLETIONS   DURATION   AGE
myjob   0/1           9s         9s
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

当前 SUCCESSFUL 的 Pod 数量为 0,查看 Pod 的状态:

[root@k8s-master ~]# kubectl get pod 
NAME          READY   STATUS               RESTARTS   AGE
myjob-5fjdh   0/1     ContainerCannotRun   0          2m36s
myjob-7wfjz   0/1     ContainerCannotRun   0          2m
myjob-9w96k   0/1     ContainerCannotRun   0          59s
myjob-chlxz   0/1     ContainerCannotRun   0          100s
myjob-gdqbg   0/1     ContainerCannotRun   0          2m23s
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

可以看到有多个 Pod,状态均不正常。kubectl describe pod 查看某个 Pod 的启动日志:

[root@k8s-master ~]# kubectl describe pod  myjob-5fjdh
Events:
  Type     Reason     Age        From                 Message
  ----     ------     ----       ----                 -------
  Normal   Scheduled  <unknown>  default-scheduler    Successfully assigned default/myjob-5fjdh to k8s-master
  Normal   Pulling    3m22s      kubelet, k8s-master  Pulling image "busybox"
  Normal   Pulled     3m13s      kubelet, k8s-master  Successfully pulled image "busybox"
  Normal   Created    3m12s      kubelet, k8s-master  Created container hello
  Warning  Failed     3m12s      kubelet, k8s-master  Error: failed to start container "hello": Error response from daemon: OCI runtime create failed: container_linux.go:349: starting container process caused "exec: \"error command\": executable file not found in $PATH": unknown
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

日志显示没有可执行程序,符合我们的预期。

下面解释一个现象:为什么 kubectl get pod 会看到这么多个失败的 Pod?

原因是:当第一个 Pod 启动时,容器失败退出,根据 restartPolicy: Never,此失败容器不会被重启,但 Job DESIRED 的 Pod 是 1 (DESIRED 是期望的意思,即期望的Pod是1),目前 SUCCESSFUL 为 0,不满足,所以 Job controller 会启动新的 Pod,直到 SUCCESSFUL 为 1。对于我们这个例子,SUCCESSFUL 永远也到不了 1,所以 Job controller 会一直创建新的 Pod。为了终止这个行为,只能删除 Job。

[root@k8s-master ~]# kubectl delete -f myjob.yml 
job.batch "myjob" deleted
  • 1.
  • 2.

如果将 restartPolicy 设置为 OnFailure 会怎么样?下面我们实践一下,修改 myjob.yml 后重新启动。

[root@k8s-master ~]# kubectl get pod
NAME          READY   STATUS             RESTARTS   AGE
myjob-m5h8w   0/1     CrashLoopBackOff   4          3m57s
  • 1.
  • 2.
  • 3.

这里只有一个 Pod,不过 RESTARTS 为 4,而且不断增加,说明 OnFailure 生效,容器失败后会自动重启。

Job 两种重启策略
(1) 如果在 Pod 模板中定义 restartPolicy=Never ,那么离线作业失败后,Pod 永远都不应该被重启,但是会尝试创建新的 Pod。
(2) 如果在 Pod 模板中定义 restartPolicy=OnFailure,那么离线作业失败后,Job Controller 就不会去尝试创建新的 Pod,但是会不断地尝试重启 Pod 里的容器,但是受到 spec.backoffLimit 字段限定重试次数。

并行执行 Job

有时,我们希望能同时运行多个 Pod,提高 Job 的执行效率。这个可以通过 parallelism 设置。

[root@k8s-master ~]# cat job.yml 
apiVersion: batch/v1
kind: Job
metadata:  
  name: myjob
spec:  
  parallelism: 2   
  template:    
    metadata:      
      name: myjob    
    spec:      
      containers:      
      - name: hello      
        image: busybox    
        command: ["echo","hello k8s job"]      
      restartPolicy: OnFailure
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

这里我们将并行的 Pod 数量设置为 2,实践一下:

[root@k8s-master ~]# kubectl apply -f job.yml 
job.batch/myjob created
 
[root@k8s-master ~]# kubectl get job
NAME    COMPLETIONS   DURATION   AGE
myjob   0/1 of 2      18s        18s
[root@k8s-master ~]# kubectl get pod
NAME          READY   STATUS      RESTARTS   AGE
myjob-5fjdh   0/1     Completed   0          21s
myjob-tdhxz   0/1     Completed   0          21s
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

Job 一共启动了两个 Pod,而且 AGE 相同,可见是并行运行的。

我们还可以通过 completions 设置 Job 成功完成 Pod 的总数:

spec: 
  completions: 6 
  parallelism: 2
  • 1.
  • 2.
  • 3.

上面配置的含义是:每次运行两个 Pod,直到总共有 6 个 Pod 成功完成。实践一下:

[root@k8s-master ~]# kubectl get job
NAME    COMPLETIONS   DURATION   AGE
myjob   2/6           22s        22s
[root@k8s-master ~]# kubectl get job
NAME    COMPLETIONS   DURATION   AGE
myjob   5/6           33s        33s
[root@k8s-master ~]# kubectl get job
NAME    COMPLETIONS   DURATION   AGE
myjob   6/6           35s        42s
 
[root@k8s-master ~]# kubectl get pod
NAME          READY   STATUS      RESTARTS   AGE
myjob-7wfjz   0/1     Completed   0          49s
myjob-9w96k   0/1     Completed   0          29s
myjob-chlxz   0/1     Completed   0          44s
myjob-cqgd2   0/1     Completed   0          25s
myjob-gdqbg   0/1     Completed   0          49s
myjob-m5h8w   0/1     Completed   0          22s
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

DESIRED 和 SUCCESSFUL 均为 6,符合预期。如果不指定 completions 和 parallelism,默认值均为 1。

上面的例子只是为了演示 Job 的并行特性,实际用途不大。不过现实中确实存在很多需要并行处理的场景。比如批处理程序,每个副本(Pod)都会从任务池中读取任务并执行,副本越多,执行时间就越短,效率就越高。这种类似的场景都可以用 Job 来实现。

尾声

本文知识点(DaemonSet):
(1) DaemonSet 通过 nodeAffinity 和 Toleration 这两个调度器的小功能,保证了每个节点上有且只有一个 Pod。
(2) DaemonSet 使用 ControllerRevision,来保存和管理自己对应的“版本”。这种“面向 API 对象”的设计思路,大大简化了控制器本身的逻辑,也正是 Kubernetes 项目“声明式 API”的优势所在。

StatefulSet 也是直接控制 Pod 对象的,也是使用 ControllerRevision 进行版本管理 [ControllerRevision 其实是 k8s 中一个通用的版本管理对象]

四种控制器管理Pod
Deployment - ReplicaSet - Pod
Statefulset - Pod
Job - Pod
Statefulset - Pod
只有Deployment是间接管理Pod,其他三种都是直接管理Pod


本文知识点(Job):

  1. Job没有选择器:跟其他控制器不同的是,Job 对象并不要求你定义一个 spec.selector 来描述要控制哪些 Pod。原因是:这个 Job 对象在创建后,它的 Pod 模板,被自动加上了一个 controller-uid=< 一个随机字符串 > 这样的 Label。而这个 Job 对象本身,则被自动加上了这个 Label 对应的 Selector,从而 保证了 Job 与它所管理的 Pod 之间的匹配关系。而 Job Controller 之所以要使用这种携带了 UID 的 Label,就是为了避免不同 Job 对象所管理的 Pod 发生重合。需要注意的是,这种自动生成的 Label 对用户来说并不友好,所以不太适合推广到 Deployment 等长作业编排对象上。
  2. Job重启策略:在 Deployment 对象里,restartPolicy 则只允许被设置为 Always;restartPolicy 在 Job 对象里只允许被设置为 Never 和 OnFailure。
  • 如果在 Pod 模板中定义 restartPolicy=Never ,那么离线作业失败后,Pod 永远都不应该被重启,但是会尝试创建新的 Pod。
  • 如果在 Pod 模板中定义 restartPolicy=OnFailure,那么离线作业失败后,Job Controller 就不会去尝试创建新的 Pod,但是会不断地尝试重启 Pod 里的容器,但是受到 spec.backoffLimit 字段限定重试次数。
  1. Job重试间隔时间与正常执行时间限制:如果Job失败,Job Controller 重新创建 Pod 的间隔是呈指数增加的;即使 Job 正常执行,通过 spec.activeDeadlineSeconds 字段可以设置最长运行时间,一旦超过时间,自动停止
  2. 无论restartPolicy设置为Never还是OnFailure,Job的成功或失败状态都会通过Job的状态(.status)字段中的条件(.status.conditions)来表示。
  3. Job可以作为并行执行,通过 completions 和 parallelism 两个属性设置。

参考资料:DaemonSet守护进程初识JobJob执行