一、简介
DaemonSet 确保所有(或者某些)节点上运行一个 Pod 的副本。当有节点加入集群时, 也会为他们新增一个 Pod 。当有节点从集群移除时,这些 Pod 也会被回收。删除 DaemonSet 将会删除它建立的全部 Pod。node
DaemonSet 的一些典型用法:python
在每一个节点上运行集群存守护进程。例如 glusterd、cephweb
在每一个节点上运行日志收集守护进程。例如 fluentd、logstashdocker
在每一个节点上运行监控守护进程。例如 Prometheus Node Exporter、Sysdig Agent、collectd、Dynatrace OneAgent、APPDynamics Agent、Datadog agent、New Relic agent、Ganglia gmond、Instana Agent 等shell
一种简单的用法是为每种类型的守护进程在全部的节点上都启动一个 DaemonSet。一个稍微复杂的用法是为同一种守护进程部署多个 DaemonSet;每一个具备不一样的标志, 而且对不一样硬件类型具备不一样的内存、CPU 要求。vim
二、建立DaemonSet
Google Cloud 的 Kubernetes 集群就会在全部的节点上启动 fluentd 和 Prometheus 来收集节点上的日志和监控数据,想要建立用于日志收集的守护进程其实很是简单,咱们可使用以下所示的代码:centos
[root@yygh-de ~]# vim DaemonSet.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd-elasticsearch
namespace: kube-system
spec:
selector:
matchLabels:
name: fluentd-elasticsearch
template:
metadata:
labels:
name: fluentd-elasticsearch
spec:
containers:
- name: fluentd-elasticsearch
image: k8s.gcr.io/fluentd-elasticsearch:1.20
volumeMounts:
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
当咱们使用 kubectl apply -f
建立上述的 DaemonSet 时,它会在 Kubernetes 集群的 kube-system
命名空间中建立 DaemonSet 资源并在全部的节点上建立新的 Pod:api
[root@yygh-de ~]# kubectl get daemonsets.apps fluentd-elasticsearch --namespace kube-system
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
fluentd-elasticsearch 1 1 1 1 1 <none> 19h
[root@yygh-de ~]# kubectl get pods --namespace kube-system --label name=fluentd-elasticsearch
NAME READY STATUS RESTARTS AGE
fluentd-elasticsearch-kvtwj 1/1 Running 0 19h
因为集群中只存在一个 Pod,因此 Kubernetes 只会在该节点上建立一个 Pod,若是咱们向当前的集群中增长新的节点时,Kubernetes 就会建立在新节点上建立新的副本,总的来讲,咱们可以获得如下的拓扑结构:数组
集群中的 Pod 和 Node 一一对应,而 DaemonSet 会管理所有机器上的 Pod 副本,负责对它们进行更新和删除。安全
三、实现原理
全部的 DaemonSet 都是由控制器负责管理的,与其余的资源同样,用于管理 DaemonSet 的控制器是 DaemonSetsController
,该控制器会监听 DaemonSet、ControllerRevision、Pod 和 Node 资源的变更。
大多数的触发事件最终都会将一个待处理的 DaemonSet 资源入栈,下游 DaemonSetsController
持有的多个工做协程就会从队列里面取出资源进行消费和同步。
四、同步
DaemonSetsController
同步 DaemonSet 资源使用的方法就是 syncDaemonSet
,这个方法从队列中拿到 DaemonSet 的名字时,会先从集群中获取最新的 DaemonSet 对象并经过 constructHistory
方法查找当前 DaemonSet 所有的历史版本:
func (dsc *DaemonSetsController) syncDaemonSet(key string) error {
namespace, name, _ := cache.SplitMetaNamespaceKey(key)
ds, _ := dsc.dsLister.DaemonSets(namespace).Get(name)
dsKey, _ := controller.KeyFunc(ds)
cur, old, _ := dsc.constructHistory(ds)
hash := cur.Labels[apps.DefaultDaemonSetUniqueLabelKey]
dsc.manage(ds, hash)
switch ds.Spec.UpdateStrategy.Type {
case apps.OnDeleteDaemonSetStrategyType:
case apps.RollingUpdateDaemonSetStrategyType:
dsc.rollingUpdate(ds, hash)
}
dsc.cleanupHistory(ds, old)
return dsc.updateDaemonSetStatus(ds, hash, true)
}
而后调用的 manage
方法会负责管理 DaemonSet 在节点上 Pod 的调度和运行,rollingUpdate
会负责 DaemonSet 的滚动更新;前者会先找出找出须要运行 Pod 和不须要运行 Pod 的节点,并调用 syncNodes
对这些须要建立和删除的 Pod 进行同步:
func (dsc *DaemonSetsController) syncNodes(ds *apps.DaemonSet, podsToDelete, nodesNeedingDaemonPods []string, hash string) error {
dsKey, _ := controller.KeyFunc(ds)
generation, err := util.GetTemplateGeneration(ds)
template := util.CreatePodTemplate(ds.Spec.Template, generation, hash)
createDiff := len(nodesNeedingDaemonPods)
createWait := sync.WaitGroup{}
createWait.Add(createDiff)
for i := 0; i < createDiff; i++ {
go func(ix int) {
defer createWait.Done()
podTemplate := template.DeepCopy()
if utilfeature.DefaultFeatureGate.Enabled(features.ScheduleDaemonSetPods) {
podTemplate.Spec.Affinity = util.ReplaceDaemonSetPodNodeNameNodeAffinity(podTemplate.Spec.Affinity, nodesNeedingDaemonPods[ix])
dsc.podControl.CreatePodsWithControllerRef(ds.Namespace, podTemplate, ds, metav1.NewControllerRef(ds, controllerKind))
} else {
podTemplate.Spec.SchedulerName = "kubernetes.io/daemonset-controller"
dsc.podControl.CreatePodsOnNode(nodesNeedingDaemonPods[ix], ds.Namespace, podTemplate, ds, metav1.NewControllerRef(ds, controllerKind))
}
}(i)
}
createWait.Wait()
获取了 DaemonSet 中的模板之以后,就会开始并行地为节点建立 Pod 副本,并发建立的过程使用了 for 循环、Goroutine 和 WaitGroup
保证程序运行的正确,然而这里使用了特性开关来对调度新 Pod 的方式进行了控制,咱们会在接下来的调度一节介绍 DaemonSet 调度方式的变迁和具体的执行过程。
当 Kubernetes 建立了须要建立的 Pod 以后,就须要删除全部节点上没必要要的 Pod 了,这里使用一样地方式并发地对 Pod 进行删除:
deleteDiff := len(podsToDelete)
deleteWait := sync.WaitGroup{}
deleteWait.Add(deleteDiff)
for i := 0; i < deleteDiff; i++ {
go func(ix int) {
defer deleteWait.Done()
dsc.podControl.DeletePod(ds.Namespace, podsToDelete[ix], ds)
}(i)
}
deleteWait.Wait()
return nil
}
到了这里咱们就完成了节点上 Pod 的调度和运行,为一些节点建立 Pod 副本的同时删除另外一部分节点上的副本,manage
方法执行完成以后就会调用 rollingUpdate
方法对 DaemonSet 的节点进行滚动更新并对控制器版本进行清理并更新 DaemonSet 的状态,文章后面的部分会介绍滚动更新的过程和实现。
五、调度
在早期的 Kubernetes 版本中,全部 DaemonSet Pod 的建立都是由 DaemonSetsController
负责的,而其余的资源都是由 kube-scheduler 进行调度,这就致使了以下的一些问题:
DaemonSetsController
没有办法在节点资源变动时收到通知 (#46935, #58868);DaemonSetsController
没有办法遵循 Pod 的亲和性和反亲和性设置 (#29276);DaemonSetsController
可能须要二次实现 Pod 调度的重要逻辑,形成了重复的代码逻辑 (#42028);多个组件负责调度会致使 Debug 和抢占等功能的实现很是困难;
设计文档 Schedule DaemonSet Pods by default scheduler, not DaemonSet controller 中包含了使用 DaemonSetsController
调度时遇到的问题以及新设计给出的解决方案。
若是咱们选择使用过去的调度方式,DeamonSetsController
就会负责在节点上建立 Pod,经过这种方式建立的 Pod 的 schedulerName
都会被设置成 kubernetes.io/daemonset-controller
,可是在默认状况下这个字段通常为 default-scheduler
,也就是使用 Kubernetes 默认的调度器 kube-scheduler 进行调度:
func (dsc *DaemonSetsController) syncNodes(ds *apps.DaemonSet, podsToDelete, nodesNeedingDaemonPods []string, hash string) error {
// ...
for i := 0; i < createDiff; i++ {
go func(ix int) {
podTemplate := template.DeepCopy()
if utilfeature.DefaultFeatureGate.Enabled(features.ScheduleDaemonSetPods) {
// ...
} else {
podTemplate.Spec.SchedulerName = "kubernetes.io/daemonset-controller"
dsc.podControl.CreatePodsOnNode(nodesNeedingDaemonPods[ix], ds.Namespace, podTemplate, ds, metav1.NewControllerRef(ds, controllerKind))
}
}(i)
}
// ...
}
DaemonSetsController
在调度 Pod 时都会使用 CreatePodsOnNode
方法,这个方法的实现很是简单,它会先对 Pod 模板进行验证,随后调用 createPods
方法经过 Kubernetes 提供的 API 建立新的副本:
func (r RealPodControl) CreatePodsWithControllerRef(namespace string, template *v1.PodTemplateSpec, controllerObject runtime.Object, controllerRef *metav1.OwnerReference) error {
if err := validateControllerRef(controllerRef); err != nil {
return err
}
return r.createPods("", namespace, template, controllerObject, controllerRef)
}
DaemonSetsController
经过节点选择器和调度器的谓词对节点进行过滤,createPods
会直接为当前的 Pod 设置 spec.NodeName
属性,最后获得的 Pod 就会被目标节点上的 kubelet 建立。
除了这种使用 DaemonSetsController
管理和调度 DaemonSet 的方法以外,咱们还可使用 Kubernetes 默认的方式 kube-scheduler 建立新的 Pod 副本:
func (dsc *DaemonSetsController) syncNodes(ds *apps.DaemonSet, podsToDelete, nodesNeedingDaemonPods []string, hash string) error {
// ...
for i := 0; i < createDiff; i++ {
go func(ix int) {
podTemplate := template.DeepCopy()
if utilfeature.DefaultFeatureGate.Enabled(features.ScheduleDaemonSetPods) {
podTemplate.Spec.Affinity = util.ReplaceDaemonSetPodNodeNameNodeAffinity(podTemplate.Spec.Affinity, nodesNeedingDaemonPods[ix])
dsc.podControl.CreatePodsWithControllerRef(ds.Namespace, podTemplate, ds, metav1.NewControllerRef(ds, controllerKind))
} else {
// ...
}
}(i)
}
// ...
}
这种状况会使用 NodeAffinity 特性来避免发生在 DaemonSetsController
中的调度:
DaemonSetsController
会在podsShouldBeOnNode
方法中根据节点选择器过滤全部的节点;对于每个节点,控制器都会建立一个遵循如下节点亲和的 Pod;
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- nodeSelectorTerms:
matchExpressions:
- key: kubernetes.io/hostname
operator: in
values:
- dest_hostname
当节点进行同步时,DaemonSetsController 会根据节点亲和的设置来验证节点和 Pod 的关系;
若是调度的谓词失败了,DaemonSet 持有的 Pod 就会保持在 Pending 的状态,因此能够经过修改 Pod 的优先级和抢占保证集群在高负载下也能正常运行 DaemonSet 的副本;
Pod 的优先级和抢占功能在 Kubernetes 1.8 版本引入,1.11 时转变成 beta 版本,在目前最新的 1.13 中依然是 beta 版本,感兴趣的读者能够阅读 Pod Priority and Preemption 文档了解相关的内容。
六、滚动更新
DaemonSetsController
对滚动更新的实现其实比较简单,它其实就是根据 DaemonSet 规格中的配置,删除集群中的 Pod 并保证同时不可用的副本数不会超过 spec.updateStrategy.rollingUpdate.maxUnavailable
,这个参数也是 DaemonSet 滚动更新能够配置的惟一参数:
func (dsc *DaemonSetsController) rollingUpdate(ds *apps.DaemonSet, hash string) error {
nodeToDaemonPods, err := dsc.getNodesToDaemonPods(ds)
_, oldPods := dsc.getAllDaemonSetPods(ds, nodeToDaemonPods, hash)
maxUnavailable, numUnavailable, err := dsc.getUnavailableNumbers(ds, nodeToDaemonPods)
oldAvailablePods, oldUnavailablePods := util.SplitByAvailablePods(ds.Spec.MinReadySeconds, oldPods)
var oldPodsToDelete []string
for _, pod := range oldUnavailablePods {
if pod.DeletionTimestamp != nil {
continue
}
oldPodsToDelete = append(oldPodsToDelete, pod.Name)
}
for _, pod := range oldAvailablePods {
if numUnavailable >= maxUnavailable {
break
}
oldPodsToDelete = append(oldPodsToDelete, pod.Name)
numUnavailable++
}
return dsc.syncNodes(ds, oldPodsToDelete, []string{}, hash)
}
删除 Pod 的顺序其实也很是简单而且符合直觉,上述代码会将不可用的 Pod 先加入到待删除的数组中,随后将历史版本的可用 Pod 加入待删除数组 oldPodsToDelete
,最后调用 syncNodes
完成对副本的删除。
七、删除
与 Deployment、ReplicaSet 和 StatefulSet 同样,DaemonSet 的删除也会致使它持有的 Pod 的删除,若是咱们使用以下的命令删除该对象,咱们能观察到以下的现象:
[root@yygh-de ~]# kubectl delete daemonsets.apps fluentd-elasticsearch --namespace kube-system
daemonset.apps "fluentd-elasticsearch" deleted
[root@yygh-de ~]# kubectl get pods --watch --namespace kube-system
fluentd-elasticsearch-wvffx 1/1 Terminating 0 14s
这部分的工做就都是由 Kubernetes 中的垃圾收集器完成的,读者能够阅读 垃圾收集器 了解集群中的不一样对象是如何进行关联的以及在删除单一对象时如何触发级联删除的原理。
八、总结
DaemonSet 其实就是 Kubernetes 中的守护进程,它会在每个节点上建立可以提供服务的副本,不少云服务商都会使用 DaemonSet 在全部的节点上内置一些用于提供日志收集、统计分析和安全策略的服务。
在研究 DaemonSet 的调度策略的过程当中,咱们其实可以经过一些历史的 issue 和 PR 了解到 DaemonSet 调度策略改动的缘由,也能让咱们对于 Kubernetes 的演进过程和设计决策有一个比较清楚的认识。
若是文章有任何错误欢迎不吝赐教,其次你们有任何关于运维的疑难杂问,也欢迎和你们一块儿交流讨论。关于运维学习、分享、交流,笔者开通了微信公众号【运维猫】,感兴趣的朋友能够关注下,欢迎加入,创建属于咱们本身的小圈子,一块儿学运维知识。群主还经营一家Orchis饰品店,喜欢的小伙伴欢迎👏前来下单。
扫描二维码
获取更多精彩
运维猫公众号

有须要技术交流的小伙伴能够加我微信,期待与你们共同成长,本人微信:
扫描二维码
添加私人微信
运维猫博主

扫码加微信
最近有一些星友咨询我知识星球的事,我也想继续在星球上发布更优质的内容供你们学习和探讨。运维猫公众号平台致力于为你们提供免费的学习资源,知识星球主要致力于即将入坑或者已经入坑的运维行业的小伙伴。
点击阅读原文 查看更多精彩内容!!!
本文分享自微信公众号 - 运维猫(centos15)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。