在前面的文章中已经分析过 deployment、statefulset 两个重要对象了,本文会继续分析 kubernetes 中另一个重要的对象 daemonset,在 kubernetes 中 daemonset 类似于 linux 上的守护进程会运行在每一个 node 上,在实际场景中,一般会将日志采集或者网络插件采用 daemonset 的方式部署。
DaemonSet 的基本操作
创建
daemonset 在创建后会在每个 node 上都启动一个 pod。
1  | $ kubectl create -f nginx-ds.yaml  | 
扩缩容
由于 daemonset 是在每个 node 上启动一个 pod,其不存在扩缩容操作,副本数量跟 node 数量保持一致。
更新
daemonset 有两种更新策略 OnDelete 和 RollingUpdate,默认为 RollingUpdate。滚动更新时,需要指定 .spec.updateStrategy.rollingUpdate.maxUnavailable(默认为1)和 .spec.minReadySeconds(默认为 0)。
1  | // 更新镜像  | 
回滚
在 statefulset 源码分析一节已经提到过 controllerRevision 这个对象了,其主要用来保存历史版本信息,在更新以及回滚操作时使用,daemonset controller 也是使用 controllerrevision 保存历史版本信息,在回滚时会使用历史 controllerrevision 中的信息替换 daemonset 中 Spec.Template。
1  | // 查看 ds 历史版本信息  | 
暂停
daemonset 目前不支持暂停操作。
删除
daemonset 也支持两种删除操作。
1  | // 非级联删除  | 
DaemonSetController 源码分析
kubernetes 版本:v1.16
首先还是看 startDaemonSetController 方法,在此方法中会初始化 DaemonSetsController 对象并调用 Run方法启动 daemonset controller,从该方法中可以看出 daemonset controller 会监听 daemonsets、controllerRevision、pod 和 node 四种对象资源的变动。其中 ConcurrentDaemonSetSyncs的默认值为 2。
k8s.io/kubernetes/cmd/kube-controller-manager/app/apps.go:36
1  | func startDaemonSetController(ctx ControllerContext) (http.Handler, bool, error) {  | 
在 Run 方法中会启动两个操作,一个就是 dsc.runWorker 执行的 sync 操作,另一个就是 dsc.failedPodsBackoff.GC 执行的 gc 操作,主要逻辑为:
- 1、等待 informer 缓存同步完成;
 - 2、启动两个 goroutine 分别执行 
dsc.runWorker; - 3、启动一个 goroutine 每分钟执行一次 
dsc.failedPodsBackoff.GC,从startDaemonSetController方法中可以看到failedPodsBackoff的 duration为1s,max duration为15m,failedPodsBackoff的主要作用是当发现 daemon pod 状态为 failed 时,会定时重启该 pod; 
k8s.io/kubernetes/pkg/controller/daemon/daemon_controller.go:263
1  | func (dsc *DaemonSetsController) Run(workers int, stopCh <-chan struct{}) {  | 
syncDaemonSet
daemonset 中 pod 的创建与删除是与 node 相关联的,所以每次执行 sync 操作时需要遍历所有的 node 进行判断。syncDaemonSet 的主要逻辑为:
- 1、通过 key 获取 ns 和 name;
 - 2、从 dsLister 中获取 ds 对象;
 - 3、从 nodeLister 获取所有 node;
 - 4、获取 dsKey;
 - 5、判断 ds 是否处于删除状态;
 - 6、调用 
constructHistory获取 current 和 oldcontrollerRevision; - 7、调用 
dsc.expectations.SatisfiedExpectations判断是否满足expectations机制,expectations机制的目的就是减少不必要的 sync 操作,关于expectations机制的详细说明可以参考笔者以前写的 “replicaset controller 源码分析”一文; - 8、调用 
dsc.manage执行实际的 sync 操作; - 9、判断是否为更新操作,并执行对应的更新操作逻辑;
 - 10、调用 
dsc.cleanupHistory根据spec.revisionHistoryLimit字段清理过期的controllerrevision; - 11、调用 
dsc.updateDaemonSetStatus更新 ds 状态; 
k8s.io/kubernetes/pkg/controller/daemon/daemon_controller.go:1212
1  | func (dsc *DaemonSetsController) syncDaemonSet(key string) error {  | 
syncDaemonSet 中主要有 manage、rollingUpdate和updateDaemonSetStatus 三个方法,分别对应创建、更新与状态同步,下面主要来分析这三个方法。
manage
manage 主要是用来保证 ds 的 pod 数正常运行在每一个 node 上,其主要逻辑为:
- 1、调用 
dsc.getNodesToDaemonPods获取已存在 daemon pod 与 node 的映射关系; - 2、遍历所有 node,调用 
dsc.podsShouldBeOnNode方法来确定在给定的节点上需要创建还是删除 daemon pod; - 3、判断是否启动了 
ScheduleDaemonSetPodsfeature-gates 特性,若启动了则需要删除通过默认调度器已经调度到不存在 node 上的 daemon pod; - 4、调用 
dsc.syncNodes为对应的 node 创建 daemon pod 以及删除多余的 pods; 
k8s.io/kubernetes/pkg/controller/daemon/daemon_controller.go:952
1  | func (dsc *DaemonSetsController) manage(ds *apps.DaemonSet, nodeList []*v1.Node, hash string) error {  | 
在 manage 方法中又调用了 getNodesToDaemonPods、podsShouldBeOnNode 和 syncNodes 三个方法,继续来看这几种方法的作用。
getNodesToDaemonPods
getNodesToDaemonPods 是用来获取已存在 daemon pod 与 node 的映射关系,并且会通过 adopt/orphan 方法关联以及释放对应的 pod。
k8s.io/kubernetes/pkg/controller/daemon/daemon_controller.go:820
1  | func (dsc *DaemonSetsController) getNodesToDaemonPods(ds *apps.DaemonSet) (map[string][]*v1.Pod, error) {  | 
podsShouldBeOnNode
podsShouldBeOnNode 方法用来确定在给定的节点上需要创建还是删除 daemon pod,主要逻辑为:
- 1、调用 
dsc.nodeShouldRunDaemonPod判断该 node 是否需要运行 daemon pod 以及 pod 能不能调度成功,该方法返回三个值wantToRun,shouldSchedule,shouldContinueRunning; - 2、通过判断 
wantToRun,shouldSchedule,shouldContinueRunning将需要创建 daemon pod 的 node 列表以及需要删除的 pod 列表获取到,wantToRun主要检查的是 selector、taints 等是否匹配,shouldSchedule主要检查 node 上的资源是否充足,shouldContinueRunning默认为 true; 
k8s.io/kubernetes/pkg/controller/daemon/daemon_controller.go:866
1  | func (dsc *DaemonSetsController) podsShouldBeOnNode(...) (nodesNeedingDaemonPods, podsToDelete []string, err error) {  | 
然后继续看 nodeShouldRunDaemonPod 方法的主要逻辑:
- 1、调用 
NewPod为该 node 构建一个 daemon pod object; - 2、判断 ds 是否指定了 
.Spec.Template.Spec.NodeName字段; - 3、调用 
dsc.simulate执行GeneralPredicates预选算法检查该 node 是否能够调度成功; - 4、判断  
GeneralPredicates预选算法执行后的reasons确定wantToRun,shouldSchedule,shouldContinueRunning的值; 
k8s.io/kubernetes/pkg/controller/daemon/daemon_controller.go:1337
1  | func (dsc *DaemonSetsController) nodeShouldRunDaemonPod(node *v1.Node, ds *apps.DaemonSet) (wantToRun, shouldSchedule, shouldContinueRunning bool, err error) {  | 
syncNodes
syncNodes 方法主要是为需要 daemon pod 的 node 创建 pod 以及删除多余的 pod,其主要逻辑为:
- 1、将 
createDiff和deleteDiff与burstReplicas进行比较,burstReplicas默认值为 250 即每个 syncLoop 中创建或者删除的 pod 数最多为 250 个,若超过其值则剩余需要创建或者删除的 pod 在下一个 syncLoop 继续操作; - 2、将 
createDiff和deleteDiff写入到expectations中; - 3、并发创建 pod,创建 pod 有两种方法:(1)创建的 pod 不经过默认调度器,直接指定了 pod 的运行节点(即设定
pod.Spec.NodeName);(2)若启用了ScheduleDaemonSetPodsfeature-gates 特性,则使用默认调度器进行创建 pod,通过nodeAffinity来保证每个节点都运行一个 pod; - 4、并发删除 
deleteDiff中的所有 pod; 
ScheduleDaemonSetPods 是一个 feature-gates 特性,其出现在 v1.11 中,在 v1.12 中处于 Beta 版本,v1.17 为 GA 版。最初 daemonset controller 只有一种创建 pod 的方法,即直接指定 pod 的 spec.NodeName 字段,但是目前这种方式已经暴露了许多问题,在以后的发展中社区还是希望能通过默认调度器进行调度,所以才出现了第二种方式,原因主要有以下五点:
- 1、DaemonSet 无法感知 node 上资源的变化 (#46935, #58868):当 pod 第一次因资源不够无法创建时,若其他 pod 退出后资源足够时 DaemonSet 无法感知到;
 - 2、Daemonset 无法支持 Pod Affinity 和 Pod AntiAffinity 的功能(#29276);
 - 3、在某些功能上需要实现和 scheduler 重复的代码逻辑, 例如:critical pods (#42028), tolerant/taint;
 - 4、当 DaemonSet 的 Pod 创建失败时难以 debug,例如:资源不足时,对于 pending pod 最好能打一个 event 说明;
 - 5、多个组件同时调度时难以实现抢占机制:这也是无法通过横向扩展调度器提高调度吞吐量的一个原因;
 
更详细的原因可以参考社区的文档:schedule-DS-pod-by-scheduler.md。
k8s.io/kubernetes/pkg/controller/daemon/daemon_controller.go:990
1  | func (dsc *DaemonSetsController) syncNodes(ds *apps.DaemonSet, podsToDelete, nodesNeedingDaemonPods []string, hash string) error {  | 
RollingUpdate
daemonset update 的方式有两种 OnDelete 和 RollingUpdate,当为 OnDelete 时需要用户手动删除每一个 pod 后完成更新操作,当为 RollingUpdate 时,daemonset controller 会自动控制升级进度。 
当为 RollingUpdate 时,主要逻辑为:
- 1、获取 daemonset pod 与 node 的映射关系;
 - 2、根据 
controllerrevision的 hash 值获取所有未更新的 pods; - 3、获取 
maxUnavailable,numUnavailable的 pod 数值,maxUnavailable是从 ds 的rollingUpdate字段中获取的默认值为 1,numUnavailable的值是通过 daemonset pod 与 node 的映射关系计算每个 node 下是否有 available pod 得到的; - 4、通过 oldPods 获取 
oldAvailablePods,oldUnavailablePods的 pod 列表; - 5、遍历 
oldUnavailablePods列表将需要删除的 pod 追加到oldPodsToDelete数组中。oldUnavailablePods列表中的 pod 分为两种,一种处于更新中,即删除状态,一种处于未更新且异常状态,处于异常状态的都需要被删除; - 6、遍历 
oldAvailablePods列表,此列表中的 pod 都处于正常运行状态,根据maxUnavailable值确定是否需要删除该 pod 并将需要删除的 pod 追加到oldPodsToDelete数组中; - 7、调用 
dsc.syncNodes删除oldPodsToDelete数组中的 pods,syncNodes方法在manage阶段已经分析过,此处不再详述; 
rollingUpdate  的结果是找出需要删除的 pods 并进行删除,被删除的 pod 在下一个 syncLoop 中会通过 manage 方法使用最新版本的 daemonset template 进行创建,整个滚动更新的过程是通过先删除再创建的方式一步步完成更新的,每次操作都是严格按照 maxUnavailable 的值确定需要删除的 pod 数。
k8s.io/kubernetes/pkg/controller/daemon/update.go:43
1  | func (dsc *DaemonSetsController) rollingUpdate(......) error {  | 
总结一下,manage 方法中的主要流程为:
1  | |-> dsc.getNodesToDaemonPods  | 
updateDaemonSetStatus
updateDaemonSetStatus 是 syncDaemonSet 中最后执行的方法,主要是用来计算 ds status subresource 中的值并更新其 status。status 如下所示:
1  | status:  | 
updateDaemonSetStatus 主要逻辑为:
- 1、调用 
dsc.getNodesToDaemonPods获取已存在 daemon pod 与 node 的映射关系; - 2、遍历所有 node,调用 
dsc.nodeShouldRunDaemonPod判断该 node 是否需要运行 daemon pod,然后计算 status 中的部分字段值; - 3、调用 
storeDaemonSetStatus更新 ds status subresource; - 4、判断 ds 是否需要 resync;
 
k8s.io/kubernetes/pkg/controller/daemon/daemon_controller.go:1152
1  | func (dsc *DaemonSetsController) updateDaemonSetStatus(......) error {  | 
最后,再总结一下 syncDaemonSet 方法的主要流程:
1  | |-> dsc.getNodesToDaemonPods  | 
总结
在 daemonset controller 中可以看到许多功能都是 deployment 和 statefulset 已有的。在创建 pod 的流程与 replicaset controller 创建 pod 的流程是相似的,都使用了 expectations 机制并且限制了在一个 syncLoop 中最多创建或删除的 pod 数。更新方式与 statefulset 一样都有 OnDelete 和 RollingUpdate 两种, OnDelete  方式与 statefulset 相似,都需要手动删除对应的 pod,而  RollingUpdate  方式与 statefulset 和 deployment 都有点区别, RollingUpdate方式更新时不支持暂停操作并且 pod 是先删除再创建的顺序进行。版本控制方式与 statefulset 的一样都是使用 controllerRevision。最后要说的一点是在 v1.12 及以后的版本中,使用 daemonset 创建的 pod 已不再使用直接指定 .spec.nodeName的方式绕过调度器进行调度,而是走默认调度器通过 nodeAffinity 的方式调度到每一个节点上。 
参考: