前言
Pod是k8s中最小的运行单元,Pod最常见的控制器就是 Deployment 和 Statefulset, 其他两种 Job/CronJob 、DaemonSet 这样的控制器较少见一些。
一、DaemonSet
1.1 DaemonSet基本属性
顾名思义,DaemonSet 的主要作用,是让你在 Kubernetes 集群里,运行一个 Daemon Pod。 所以,这个 Pod 有如下三个特征:
- 这个 Pod 运行在 Kubernetes 集群里的每一个节点(Node)上,每个节点上只有一个这样的 Pod 实例
- 当有新的节点加入 Kubernetes 集群后,该 Pod 会自动地在新节点上被创建出来
- 而当旧节点被删除后,它上面的 Pod 也相应地会被回收掉。
这个机制听起来很简单,但 Daemon Pod 的意义确实是非常重要的,业务场景包括:
- 各种网络插件的 Agent 组件,都必须运行在每一个节点上,用来处理这个节点上的容器网络
- 各种存储插件的 Agent 组件,也必须运行在每一个节点上,用来在这个节点上挂载远程存储目录,操作容器的 Volume 目录
- 各种监控组件和日志组件,也必须运行在每一个节点上,负责这个节点上的监控信息和日志搜集。
为了弄清楚 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
这个 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
在这个 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
这个 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
在 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
这是因为在默认情况下,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
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) 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
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>
接下来,我们来把这个 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
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
有了版本号,你也就可以像 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>
就会看到,这个 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
这个 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
此时,相信你对 Kubernetes 的 API 对象已经不再陌生了。在这个 Job 的 YAML 文件里,你肯定一眼就会看到一位“老熟人”:Pod 模板,即 spec.template 字段。
在这个 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…。
所以,这其实就是一个计算π值的容器。而通过 scale=10000,我指定了输出的小数点后的位数是 10000。在我的计算机上,这个计算大概用时 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 永远都不应该被重启,否则它们会再重新计算一遍。
事实上,restartPolicy 在 Job 对象里只允许被设置为 Never 和 OnFailure;而在 Deployment 对象里,restartPolicy 则只允许被设置为 Always。
此时,我们通过 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 的含义。
如前所述,当一个 Job 的 Pod 运行结束后,它会进入 Completed 状态。但是,如果这个 Pod 因为某种原因一直不肯结束呢?
在 Job 的 API 对象里,有一个 spec.activeDeadlineSeconds 字段可以设置最长运行时间,比如:
spec:
backoffLimit: 5
activeDeadlineSeconds: 100
一旦运行超过了 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 程序退出了就不再重启了,不管正确还是错误退出
解释:
- batch/v1 是当前 Job 的 apiVersion。
- kind 指明当前资源的类型为 Job。
- 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
kubectl get job 查看 Job 的状态:
[root@k8s-master ~]# kubectl get job
NAME COMPLETIONS DURATION AGE
myjob 1/1 21s 9m58s
可以看到按照预期启动了一个 Pod,并且已经成功执行。(Pod 执行完毕后容器已经退出)
[root@k8s-master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
myjob-q54fk 0/1 Completed 0 9m53s
kubectl logs 可以查看 Pod 的标准输出:
[root@k8s-master ~]# kubectl logs myjob-q54fk
hello k8s job
以上是 Pod 成功执行的情况,如果 Pod 失败了会怎么样呢?
Job执行失败
先删除之前的 Job:
[root@k8s-master ~]# kubectl delete -f myjob.yml
job.batch "myjob" deleted
修改 myjob.yml,故意引入一个错误,只需要修改command。
command: ["error command","hello k8s job"]
运行新的 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
当前 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
可以看到有多个 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
日志显示没有可执行程序,符合我们的预期。
下面解释一个现象:为什么 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
如果将 restartPolicy 设置为 OnFailure 会怎么样?下面我们实践一下,修改 myjob.yml 后重新启动。
[root@k8s-master ~]# kubectl get pod
NAME READY STATUS RESTARTS AGE
myjob-m5h8w 0/1 CrashLoopBackOff 4 3m57s
这里只有一个 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
这里我们将并行的 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
Job 一共启动了两个 Pod,而且 AGE 相同,可见是并行运行的。
我们还可以通过 completions 设置 Job 成功完成 Pod 的总数:
spec:
completions: 6
parallelism: 2
上面配置的含义是:每次运行两个 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
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):文章来源:https://www.toymoban.com/news/detail-573141.html
- Job没有选择器:跟其他控制器不同的是,Job 对象并不要求你定义一个 spec.selector 来描述要控制哪些 Pod。原因是:这个 Job 对象在创建后,它的 Pod 模板,被自动加上了一个 controller-uid=< 一个随机字符串 > 这样的 Label。而这个 Job 对象本身,则被自动加上了这个 Label 对应的 Selector,从而 保证了 Job 与它所管理的 Pod 之间的匹配关系。而 Job Controller 之所以要使用这种携带了 UID 的 Label,就是为了避免不同 Job 对象所管理的 Pod 发生重合。需要注意的是,这种自动生成的 Label 对用户来说并不友好,所以不太适合推广到 Deployment 等长作业编排对象上。
- Job重启策略:在 Deployment 对象里,restartPolicy 则只允许被设置为 Always;restartPolicy 在 Job 对象里只允许被设置为 Never 和 OnFailure。
- 如果在 Pod 模板中定义 restartPolicy=Never ,那么离线作业失败后,Pod 永远都不应该被重启,但是会尝试创建新的 Pod。
- 如果在 Pod 模板中定义 restartPolicy=OnFailure,那么离线作业失败后,Job Controller 就不会去尝试创建新的 Pod,但是会不断地尝试重启 Pod 里的容器,但是受到 spec.backoffLimit 字段限定重试次数。
- Job重试间隔时间与正常执行时间限制:如果Job失败,Job Controller 重新创建 Pod 的间隔是呈指数增加的;即使 Job 正常执行,通过 spec.activeDeadlineSeconds 字段可以设置最长运行时间,一旦超过时间,自动停止
- 无论restartPolicy设置为Never还是OnFailure,Job的成功或失败状态都会通过Job的状态(.status)字段中的条件(.status.conditions)来表示。
- Job可以作为并行执行,通过 completions 和 parallelism 两个属性设置。
参考资料:DaemonSet守护进程
初识Job
Job执行文章来源地址https://www.toymoban.com/news/detail-573141.html
到了这里,关于Kubernetes_Pod_DaemonSet与Job/CronJob的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!