DaemonSet
主要作用是让Kubernetes集群中运行一个Daemon Pod,这个Pod有如下三个特征:
这个Pod运行在Kubernetes集群里的每一个节点(Node)上
当有新节点加入Kubernetes集群后,该Pod会自动地在新节点上被创建出来;而当旧节点被删除后,它上面的Pod也相应地会被回收掉
例如:
各种网络插件的Agent组件,都必须运行在每一个节点上,用来处理这个节点上的容器网络
各种存储插件的Agent组件,也必须运行在每一个节点上,用来在这个节点上挂载远程存储目录,操作容器的Volume目录
各种监控组件和日志组件,也必须运行在每一个节点上,负责这个节点上的监控信息和日志搜集
更重要的是,与其他编排对象不同,DaemonSet开始运行的时机,很多时候比整个kubernetes集群出现的时机都要早 。例如,这个DaemonSet是网络插件的Agent组件,在整个kubernetes集群中还没有可用的容器网络时,所有的worker节点的状态都是NotReady。这个时候普通的Pod肯定不能运行的,所以DaemonSet要先于其他Pod运行。
复制 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管理一个fluented-elasticsearch镜像的Pod,功能是通过fluented将Docker容器里的日志转发到ElasticSearch。
DaemonSet与Deployment很类似,只是没有 replicas
字段,也是使用selector管理Pod 。
在template
中定义Pod的模板,包含一个镜像,这个镜像挂载了两个hostPath类型的Volume,分别对应宿主机的/var/log
目录和/var/lib/docker/containers
目录。fluented启动后,它会从这两个目录里搜集日志信息,并转发给ElasticSearch保存,这样就可以通过ElasticSearch方便地检索这些日志了。
注意,Docker容器里应用的日志,默认会保存在宿主机的/var/lib/docker/containers/{{.容器ID}}/{{.容器ID}}-json.log
文件里,这个目录就是fluented搜集的目标之一。
如何保证每个Node上有且仅有一个被管理的Pod,DaemonSet Controller首先从Etcd里获取所有的Node列表,遍历所有的Node,遍历的过程中可以检查当前节点上是否有携带了对应标签的Pod在运行。检查结果有三种情况:
有被管理的Pod,但是数量超过1,直接调用kubernetes API这个节点上删除多余的Pod
第一种情况,新建Pod的时候,利用Pod API,通过nodeSelector
选择Node的名字即可。新版本中nodeSelector
将被弃用,使用新的nodeAffinity
字段。如下例子:
复制 apiVersion: v1
kind: Pod
metadata:
name: with-node-affinity
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
# 这个nodeAffinity必须在被调度的时候予以考虑,同时也可以设置在某些情况下不予考虑这个nodeAffinity
nodeSelectorTerms:
- matchExpressions:
- key: metadata.name
# 这个Pod只允许运行在“metadata.name”是“node-1”的节点上
operator: In
values:
- node-1
在这个Pod中,声明一个spec.affinity
字段,然后定义一个nodeAffinity
。其中spec.Affinity
字段是Pod里跟调度相关的一个字段。nodeAffinity的定义支持丰富的语法:
丰富的语法,是其取代前一代的原因之一。其实大多数时候,Operator语义没啥用。所以,DaemonSet Controller会在创建Pod的时候,自动在这个Pod的API对象里加上这个nodeAffinity定义,nodeAffinity中需要绑定的节点名字,正是当前正在遍历的这个节点。
DaemonSet并不修改用户提交的YAML文件里的Pod模板,而是在向kubernetes发起请求之前,直接修改根据模板生成的Pod对象
DaemonSet会给这个Pod自动加上另一个与调度相关的字段的字段tolerations
,这就意味着这个Pod能够容忍(toleration)某些Node上的污点(taint)。会自动加入如下字段:
复制 apiVersion: v1
kind: Pod
metadata:
name: with-toleration
spec:
tolerations:
- key: node.kubernetes.io/unschedulable
operator: Exists
effect: NoSchedule
这个Toleration的含义是:容忍所有被标记为unschedulable
污点的节点,容忍的效果是允许调度。
在正常情况下,被标记了unschedulable
污点(effect:NoSchedule)的节点,是不会有任何Pod被调度上去的。添加了容忍之后就可以忽略这个限制,这样就能保证每个节点都有一个Pod。如果这个节点存在故障,那么Pod可能会启动失败,DaemonSet则会始终尝试直到Pod启动成功 。
更新策略
RollingUpdate:一个一个的更新,先更新第一个 pod,然后老的 pod 被移除,通过健康检查之后再去创建第二个pod,这样对于业务上来说会比较平滑地升级,不会中断;
OnDelete:也是一个很好的更新策略,就是模板更新之后,pod不会有任何变化,需要手动控制。当删除某一个节点对应的 pod,它就会重建,不删除的话它就不会重建,这样的话对于一些需要手动控制的特殊需求也会有特别好的作用。
DaemonSet控制器
watch API Server的状态包括node的状态,这些数据都是通过API Server存储在etcd中的。
当node节点状态发生变化时,通过一个内存消息队列发出消息
DaemonSet Controller会watch到这个状态,然后查看各个节点上是否都有对应的Pod
如果有Pod就会做一个对比,比较一下版本,然后进行滚动更新
Ondelete的时候也会检查一下版本,判断是否更新还是创建Pod
如何比其他Pod运行的早
通过Toleration机制实现。在Kubernetes项目中,当一个节点的网络插件尚未安装时,这个节点就会被自定加上一个“污点”:node.kubernetes.io/network-unavailable
。DaemonSet通过添加容忍的方式就可以跳过这个限制,从而成功的启动一个网络插件的Pod在这个节点:
复制 ...
template:
metadata:
labels:
name: network-plugin-agent
spec:
tolerations:
- key: node.kubernetes.io/network-unavailable
operator: Exists
effect: NoSchedule
这种机制正是在部署kubernetes集群的时候,能够先部署kubernetes本身,再部署网络插件的根本原因。因为网络插件本身就是一个DaemonSet。可以在Pod的模板中添加更多种类的Toleration,从而利用DaemonSet实现自己的目的。比如添加下面的容忍:
复制 tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule
这样的话Pod可以被调度到主节点,默认主节点有“node-role.kubernetes.io/master
”的污点,Pod是不能运行的。一般在DaemonSet上都要加上resource字段,来限制CPU和内存的使用,防止占用过多的宿主机资源。
版本管理(ControllerRevision)
ControllerRevision 其实是一个通用的版本管理对象,这样可以巧妙的避免每种控制器都要维护一套冗余的代码和逻辑。
DaemonSet也可以像Deployment那样进行版本管理:
复制 #查看版本历史
kubectl rollout history daemonset fluentd-elasticsearch -n kube-system
daemonsets "fluentd-elasticsearch"
REVISION CHANGE-CAUSE
1 <none>
# 更新镜像版本
kubectl set image ds/fluentd-elasticsearch fluentd-elasticsearch=k8s.gcr.io/fluentd-elasticsearch:v2.2.0 --record -n=kube-system
# 增加--record参数,升级指令会直接出现在history中
# 查看滚动更新的过程
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那样进行历史版本回滚。Deployment通过每一个版本对应一个ReplicaSet来控制不同的版本,DaemonSet没有ReplicaSet,使用ControllerRevision进行控制。
ControllerRevision专门用来记录某种Controller对象的版本,Kubernetes v1.7之后添加的API对象。
查看对应的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>
# 对DaemonSet进行版本回滚
kubectl rollout undo daemonset fluentd-elasticsearch --to-revision=1 -n kube-system
daemonset.extensions/fluentd-elasticsearch rolled back
# undo操作读取Revision=1的ControllerRevision对象保存的Data字段
注意,执行了上述undo操作后,DaemonSet的Revision并不会从2变回1,而是变成3,每一个操作都是一个新的ControllerRevision对象被创建。
ControllerRevision对象:
在Data
字段保存了该版本对象的完整的DaemonSet的API对象
在Annotation
字段保存了创建这个对象所使用的kubectl命令
Job
Deployment、StatefulSet、DaemonSet这三种编排概念,主要编排的对象是“在线业务 ”(即Long Running Task长作业),比如Nginx、MySQL等。这类应用一旦运行起来,除非出错或者停止,它的容器进程会一直保持在Running状态 。
但是,有一类作业显然不满足这个情况,就是“离线业务”(即Batch Job计算任务),这种任务在计算完成后就直接退出了,而此时如果依然用Deployment来管理这类作业,就会发现Pod计算任务结束后退出,然后Controller不断重启这个任务,向“滚动更新”这样的功能就更不需要了。
复制 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
在这个yaml中包含一个Pod模板,即spec.template
字段。这个Pod定义了一个计算π的容器。注意:这个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
# 处于计算状态
kubectl get pods
NAME READY STATUS RESTARTS AGE
pi-rq5rl 1/1 Running 0 10s
# 任务结束
kubectl get pods
NAME READY STATUS RESTARTS AGE
pi-rq5rl 0/1 Completed 0 4m
# 这就是为什么在Job对象的模板中要定义restartPolicy=Never的原因,离线计算的任务永远不该被重启,再计算一遍毫无意义
# 查看计算结果
kubectl logs pi-rq5rl
3.141592653589793238462643383279...
# 如果计算失败
kubectl get pods
NAME READY STATUS RESTARTS AGE
pi-55h89 0/1 ContainerCreating 0 2s
pi-tqbcz 0/1 Error 0 5s
# 根据restartPolicy的定义,如果为never,则会重新创建新的Pod再次计算,如果为onfailure则restart这个Pod里面的容器
通过describe可以看到,这个Job对象在创建后,它的Pod模板,被自动添加上了一个controller-uid=<一个随机字符串>
这样的label。而这个Job对象本身,则被自动加上了这个Label对应的Selector,从而保证了Job与它所管理的Pod之间的匹配关系。
Job Controller使用这种携带UID的label的方式,是为了避免不同Job对象所管理的Pod发生重合 。这种自动生成的Label对用户来说很不友好,所以不适合推广到Deployment等长作业编排对象上。
restartPolicy
在Job对象中只能被设置为Never或者OnFailure,在Job的对象中添加spec.backoffLimit
字段来定义重试的次数,默认为6次(即backoffLimit=6)。
需要注意,重新创建Pod或者重启Pod的间隔是呈指数增长的,即下一次重新创建Pod的动作会分别发生在10s、20s、40s。。。
当Job正常运行结束后,Pod处于Completed状态,如果Pod因为某种原因一直处于运行状态,则可以设置spec.activeDeadlineSeconds
字段来设置最长运行时间,比如:
复制 spec:
backoffLimit: 5
activeDeadlineSeconds: 100
运行超过100s这个Job的所有Pod都会终止,并且在Pod的状态里看到终止的原因是reason:DeadlineExceeded。
在Job对象中,负责并行控制的参数有两个:
spec.parallelism
:定义的是Job在任意时间最多可以启动多少个Pod同时运行
spec.completions
:定义的是Job至少完成的Pod数目,即Job最小完成数
复制 # 添加最大并行数2,最小完成数4
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
kubectl create -f job.yaml
# Job维护两个状态字段,DESIRED和SUCCESSFUL
kubectl get job
NAME DESIRED SUCCESSFUL AGE
pi 4 0 3s
# DESIRED就是completions定义的最小完成数
# 同时创建两个
kubectl get pods
NAME READY STATUS RESTARTS AGE
pi-5mt88 1/1 Running 0 6s
pi-gmcq5 1/1 Running 0 6s
# 当每个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状态
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
# 所有Pod都成功退出,Job的SUCCESSFUL字段值为4
kubectl get job
NAME DESIRED SUCCESSFUL AGE
pi 4 4 5m
Job Controller的控制对象是Pod,在控制循环中进行的协调(Reconcile)操作,是根据:
parallelism、completions参数的值
共同计算出在这个周期里,应该创建或者删除的Pod数目,然后调用Kubernetes API来执行这个操作。
Job Controller 实际上控制了作业执行的 并行度
和总共需要完成的 任务数
这两个重要的参数 。在实际使用中,需要根据作业的特性,来决定并行度和任务数的合理取值。
Job控制器
controller会watch API Server,每次提交一个Job的YAML文件都是经过API Server写入到etcd中。Job Controller注册三个handler(Add、Update、Delete),有对应的操作的时候,通过内存级消息队列,发送到controller中。
接收到通知事件后,Controller会检查当前是否有运行的Pod
如果没有,通过Scale Up把这个Pod创建出来
如果有或者大于期望值,会执行Scale Down操作
同时检查是并行还是串行的Job,根据配置的并行度、串行度,及时把对应数量的Pod创建出来
最后把Job的整个状态更新到API Server中
常见的Job使用方法
外部管理器+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
在yaml文件中定义了$ITEM
这样的变量,在控制这种Job时,只需要注意两个方面:
所有来自同一个模板的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
# 这样一组yaml文件就生成了,通过create就能执行任务
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
通过这种方式很方便的管理Job作业,只需要类似与for循环这样的外部工具,TensorFlow的KubeFlow就是这样实现的。在这种模式下使用Job对象,completions和parallelism这两个字段都应该使用默认值1,而不需要自行设置,作业的并行控制应该交给外部工具来管理(如KubeFlow )。
拥有固定任务数目的并行Job
这种模式下,只关心最后是否拥有指定数目(spec.completions
)个任务成功退出。至于执行的并行度是多少并不关心。可以使用工作队列(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
在yaml中总共定义了总共有8个任务会被放入工作队列,可以使用RabbitMQ充当工作队列,所以在Pod 的模板中定义BROKER_URL
作为消费者。Pod中的执行逻辑如下:
复制 /* job-wq-1 的伪代码 */
queue := newQueue($BROKER_URL, $QUEUE)
task := queue.Pop()
process(task)
exit
创建这个job后,每组两个Pod,一共八个,每个Pod都会连接BROKER_URL
,从RabbitMQ里读取任务,然后各自处理。每个Pod只要将任务信息读取并完成计算,用户只关心总共有8个任务计算完成并退出,就认为整个job计算完成,对应的就是“任务总数固定”的场景。
指定并行度,但不设定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之间是不是有资源的竞争和协同等。
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对象的控制器 。它创建和删除Job的依据是schedule字段定义的、一个标准UNIX Cron 格式的表达式。
Cron表达式中的五个部分分别代表:分钟、小时、日、月、星期。CronJob对象会记录每次Job执行的时间。
由于定时任务的特殊性,很可能某个Job还没有执行完成,另外一个新job就产生了,这时候可以通过spec.concurrencyPolicy
字段来定义具体的处理策略,如:
concurrencyPolicy=Allow,默认的情况,这些Job可以同时存在
concurrencyPolicy=Forbid,不会创建新的Pod,该创建周期被跳过
concurrencyPolicy=Replace,新产生的Job会替换旧的,没有执行完的Job
如果某一次Job创建失败,就会被标记为“miss”。当在指定的时间窗口(通过字段spec.startingDeadlineSeconds
字段指定,单位为秒)内,miss数目达到100时,那个Cronjob会停止再创建这个Job。