主要作用是让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字段。如下例子:
在这个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)。会自动加入如下字段:
这个Toleration的含义是:容忍所有被标记为unschedulable污点的节点,容忍的效果是允许调度。
在正常情况下,被标记了unschedulable污点(effect:NoSchedule)的节点,是不会有任何Pod被调度上去的。添加了容忍之后就可以忽略这个限制,这样就能保证每个节点都有一个Pod。如果这个节点存在故障,那么Pod可能会启动失败,DaemonSet则会始终尝试直到Pod启动成功 。
RollingUpdate:一个一个的更新,先更新第一个 pod,然后老的 pod 被移除,通过健康检查之后再去创建第二个pod,这样对于业务上来说会比较平滑地升级,不会中断;
OnDelete:也是一个很好的更新策略,就是模板更新之后,pod不会有任何变化,需要手动控制。当删除某一个节点对应的 pod,它就会重建,不删除的话它就不会重建,这样的话对于一些需要手动控制的特殊需求也会有特别好的作用。
watch API Server的状态包括node的状态,这些数据都是通过API Server存储在etcd中的。
当node节点状态发生变化时,通过一个内存消息队列发出消息
DaemonSet Controller会watch到这个状态,然后查看各个节点上是否都有对应的Pod
如果有Pod就会做一个对比,比较一下版本,然后进行滚动更新
Ondelete的时候也会检查一下版本,判断是否更新还是创建Pod
通过Toleration机制实现。在Kubernetes项目中,当一个节点的网络插件尚未安装时,这个节点就会被自定加上一个“污点”:node.kubernetes.io/network-unavailable。DaemonSet通过添加容忍的方式就可以跳过这个限制,从而成功的启动一个网络插件的Pod在这个节点:
这种机制正是在部署kubernetes集群的时候,能够先部署kubernetes本身,再部署网络插件的根本原因。因为网络插件本身就是一个DaemonSet。可以在Pod的模板中添加更多种类的Toleration,从而利用DaemonSet实现自己的目的。比如添加下面的容忍:
这样的话Pod可以被调度到主节点,默认主节点有“node-role.kubernetes.io/master”的污点,Pod是不能运行的。一般在DaemonSet上都要加上resource字段,来限制CPU和内存的使用,防止占用过多的宿主机资源。
版本管理(ControllerRevision)
ControllerRevision 其实是一个通用的版本管理对象,这样可以巧妙的避免每种控制器都要维护一套冗余的代码和逻辑。
DaemonSet也可以像Deployment那样进行版本管理:
有了版本号,就可以像Deployment那样进行历史版本回滚。Deployment通过每一个版本对应一个ReplicaSet来控制不同的版本,DaemonSet没有ReplicaSet,使用ControllerRevision进行控制。
ControllerRevision专门用来记录某种Controller对象的版本,Kubernetes v1.7之后添加的API对象。
查看对应的ControllerRevision:
注意,执行了上述undo操作后,DaemonSet的Revision并不会从2变回1,而是变成3,每一个操作都是一个新的ControllerRevision对象被创建。
ControllerRevision对象:
在Data字段保存了该版本对象的完整的DaemonSet的API对象
在Annotation字段保存了创建这个对象所使用的kubectl命令
Deployment、StatefulSet、DaemonSet这三种编排概念,主要编排的对象是“在线业务 ”(即Long Running Task长作业),比如Nginx、MySQL等。这类应用一旦运行起来,除非出错或者停止,它的容器进程会一直保持在Running状态 。
但是,有一类作业显然不满足这个情况,就是“离线业务”(即Batch Job计算任务),这种任务在计算完成后就直接退出了,而此时如果依然用Deployment来管理这类作业,就会发现Pod计算任务结束后退出,然后Controller不断重启这个任务,向“滚动更新”这样的功能就更不需要了。
在这个yaml中包含一个Pod模板,即spec.template字段。这个Pod定义了一个计算π的容器。注意:这个Job对象并没有定义一个`spec.selector来描述要控制哪些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字段来设置最长运行时间,比如:
运行超过100s这个Job的所有Pod都会终止,并且在Pod的状态里看到终止的原因是reason:DeadlineExceeded。
在Job对象中,负责并行控制的参数有两个:
spec.parallelism:定义的是Job在任意时间最多可以启动多少个Pod同时运行
spec.completions:定义的是Job至少完成的Pod数目,即Job最小完成数
Job Controller的控制对象是Pod,在控制循环中进行的协调(Reconcile)操作,是根据:
parallelism、completions参数的值
共同计算出在这个周期里,应该创建或者删除的Pod数目,然后调用Kubernetes API来执行这个操作。
Job Controller 实际上控制了作业执行的 并行度 和总共需要完成的 任务数 这两个重要的参数 。在实际使用中,需要根据作业的特性,来决定并行度和任务数的合理取值。
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的yaml文件定义为一个模板,然后用一个外部工具控制这些模板来生成Job,如下所示:
在yaml文件中定义了$ITEM这样的变量,在控制这种Job时,只需要注意两个方面:
所有来自同一个模板的Job,都有一个jobgroup:jobexample标签,这一组Job使用这样一个相同的标识
通过这种方式很方便的管理Job作业,只需要类似与for循环这样的外部工具,TensorFlow的KubeFlow就是这样实现的。在这种模式下使用Job对象,completions和parallelism这两个字段都应该使用默认值1,而不需要自行设置,作业的并行控制应该交给外部工具来管理(如KubeFlowarrow-up-right )。
这种模式下,只关心最后是否拥有指定数目(spec.completions)个任务成功退出。至于执行的并行度是多少并不关心。可以使用工作队列(Work Queue)进行任务分发,job的yaml定义如下:
在yaml中总共定义了总共有8个任务会被放入工作队列,可以使用RabbitMQ充当工作队列,所以在Pod 的模板中定义BROKER_URL作为消费者。Pod中的执行逻辑如下:
创建这个job后,每组两个Pod,一共八个,每个Pod都会连接BROKER_URL,从RabbitMQ里读取任务,然后各自处理。每个Pod只要将任务信息读取并完成计算,用户只关心总共有8个任务计算完成并退出,就认为整个job计算完成,对应的就是“任务总数固定”的场景。
指定并行度,但不设定completions
此时,需要自己想办法决定什么时候启动新的Pod,什么时候Job才算完成。这种情况下,任务的总数未知,所以需要工作队列来分发任务,并且判断队列是否为空(即任务已经完成)。Job的定义如下,只是不设置completions的值:
Pod 的执行逻辑如下:
由于任务数目的总数不固定,所以每一个Pod必须能够知道,自己什么时候可以退出。比如队列为空,所以这种用法对应的是“任务总数不固定”的场景。在实际使用中,需要处理的条件非常复杂,任务完成后的输出,每个任务Pod之间是不是有资源的竞争和协同等。
定时任务,API对象如下:
在这个yaml文件中,最重要的是jobTemplate,CronJob是一个Job对象的控制器 。它创建和删除Job的依据是schedule字段定义的、一个标准UNIX Cronarrow-up-right 格式的表达式。
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。