kubernetes controller源码解读之DaemonSet

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
简介: 1. 适用场景通过 DaemonSet部署的应用(Pod)主要用于满足如下场景:类似守护进程,每个节点保证部署一个应用能跟随节点的新增/移除,自动创建/删除守护应用可以方便的对守护应用进行版本升级或者回滚实际应用场景中,每个节点都需要的agent类型组件(如日志收集组件fluentd等),一般都采用DaemonSet方式部署。

1. 适用场景

通过 DaemonSet部署的应用(Pod)主要用于满足如下场景:

  1. 类似守护进程,每个节点保证部署一个应用
  2. 能跟随节点的新增/移除,自动创建/删除守护应用
  3. 可以方便的对守护应用进行版本升级或者回滚

实际应用场景中,每个节点都需要的agent类型组件(如日志收集组件fluentd等),一般都采用DaemonSet方式部署。

2. DaemonSet资源定义

  • 单个 DaemonSet资源的定义结构( DaemonSet的yam定义需要遵守该结构)如下:
type DaemonSet struct {
    metav1.TypeMeta
    metav1.ObjectMeta
    Spec DaemonSetSpec
    Status DaemonSetStatus
}

DaemonSets Controller将根据 DaemonSetSpec定义来指示控制逻辑,使DaemonSetStatus中指示的状态最终符合用户的期待。

  • DaemonSetSpec的定义如下:
type DaemonSetSpec struct {
    Selector *metav1.LabelSelector
    Template v1.PodTemplateSpec
    UpdateStrategy DaemonSetUpdatestrategy
    MinReadySeconds int32
    RevisionHistoryLimit *int32
} 
  • DaemonSetStatus的定义如下:
type DaemonSetStatus struct {
    CurrentNumberScheduled int32
    NumberMisscheduled int32
    DesiredNumberscheduled int32
    NumberReady int32
    ObservedGeneration int64
    UpdatedNumberScheduled int32
    NumberAvailable int32
    NumberUnavailable int32
    CollisionCount *int32
    Conditions []DaemonSetCondition
}

DaemonSet的yaml具体实例,可以参照:
https://raw.githubusercontent.com/kubernetes/website/master/content/en/examples/controllers/daemonset.yaml

3. DaemonSets控制器详细说明

首先分析一下节点是否适合部署 DaemonSet Pod的细节:

3.1节点可否部署 DaemonSet Pod判定

节点可否部署DaemonSet Pod的function定义如下:

@kubernetes/pkg/controller/daemon/daemon_controller.go
func (dsc *DaemonSetsController) nodeShouldRunDaemonPod(node *v1.Node, ds *apps.DaemonSet) (wantToRun, shouldSchedule, shouldContinueRunning bool, err error) 

返回值说明如下:

  • wantToRun: 节点是否需要部署DaemonSet pod。主要用于DaemonSet状态更新。
  • shouldSchedule: 节点是否可以部署DaemonSet Pod
  • shouldContinueRunning: 节点上已经部署的DaemonSet Pod是否可以继续运行(如节点新增了Pod不能tolerate的NoExecute taint时,该返回值为false,即节点上DaemonSet Pod不能继续运行)

wantToRun和 shouldSchedule的设置区别:

Disk,Mem压力/冲突或者资源(CPU或者内存等)不足时,wantToRun仍为true,而shouldSchedule为 false。即需要部署但是暂时不能部署的意思。

代码中处理如下:

@kubernetes/pkg/controller/daemon/daemon_controller.go
func (dsc *DaemonSetsController) nodeShouldRunDaemonPod(node *v1.Node, ds *apps.DaemonSet) (wantToRun, shouldSchedule, shouldContinueRunning bool, err error) {
    ...
    case
        predicates.ErrDiskConflict,
        predicates.ErrVolumeZoneConflict,
        predicates.ErrMaxVolumeCountExceeded,
        predicates.ErrNodeUnderMemoryPressure,
        predicates.ErrNodeUnderDiskPressure:
            shouldSchedule = false //上述的error时, 暂时不能部署
    ...
    if shouldSchedule && insufficientResourceErr != nil {
        shouldSchedule = false // 资源不足时,也暂时不能部署
    }
    ...
}

另主要是调用kube-schedulerPredicate处理对节点进行评估,判断节点可否运行该DaemonSet Pod。具体更多细节可以翻阅simulate()@kubernetes/pkg/controller/daemon/daemon_controller.go的代码实现。

在分析完节点是否可以部署DaemonSet Pod后,下面看一下 DaemonSet Pod的创建和删除处理。

3.2 DaemonSet Pod的创建和删除

Pod的创建和删除是动态过程,当有节点接入或者状态变化时,都可能执行Pod的创建和删除,因此每次对DaemonSet的worker处理,都需要遍历所有集群所有节点来评估Pod的删除和创建。当评估完成后再执行具体的创建和删除操作。

3.2.1 节点评估(获取od创建和删除的节点列表)

遍历集群中所有节点,归类出需要创建Pod和删除Pod的节点。然后依据归类结果进行Pod创建和删除

  • 条件1: 需要部署(wantToRun=true)但是不能部署(shouldSchedule=false)时,先把Pod放入挂起队列
  • 条件2: 可以部署(shouldSchedule=true)且Pod未运行时,则要创建Pod
  • 条件3: Pod可以继续运行(shouldContinueRunning=true)时,如果Pod运行状态为failed,则删除该Pod。如果节点上已经运行 DaemonSet Pod数 > 1,则删除多余的pod
  • 条件4: Pod不可以继续运行(shouldContinueRunning=false)但是Pod正在运行时,则删除Pod。
    代码处理如下:
@kubernetes/pkg/controller/daemon/daemon_controller.go
func (dsc *DaemonSetsController) podsShouldBeOnNode(
    node *v1.Node,
    nodeToDaemonPods map[string][]*v1.Pod,
    ds *apps.DaemonSet,
) (nodesNeedingDaemonPods, podsToDelete []string, failedPodsObserved int, err error) {
    // 节点是否可以部署Pod判定处理
    wantToRun, shouldSchedule, shouldContinueRunning, err := dsc.nodeShouldRunDaemonPod(node, ds)
    if err != nil {
        return
    }

    daemonPods, exists := nodeToDaemonPods[node.Name]
    dsKey, _ := cache.MetaNamespaceKeyFunc(ds)
    dsc.removeSuspendedDaemonPods(node.Name, dsKey)

    switch {
    case wantToRun && !shouldSchedule: // 条件1
        dsc.addSuspendedDaemonPods(node.Name, dsKey)
    case shouldSchedule && !exists:    // 条件2
        nodesNeedingDaemonPods = append(nodesNeedingDaemonPods, node.Name)
    case shouldContinueRunning:       // 条件3
        var daemonPodsRunning []*v1.Pod
        for _, pod := range daemonPods {
            if pod.DeletionTimestamp != nil {
                continue
            }
            // 运行结束且状态为失败时,则删除该Pod
            if pod.Status.Phase == v1.PodFailed {
                podsToDelete = append(podsToDelete, pod.Name)
                failedPodsObserved++
            } else {
                daemonPodsRunning = append(daemonPodsRunning, pod)
            }
        }
        // 运行Pod数量超过1个时,则删除所有后创建的DaemonSet Pod
        if len(daemonPodsRunning) > 1 {
            sort.Sort(podByCreationTimestampAndPhase(daemonPodsRunning))
            for i := 1; i < len(daemonPodsRunning); i++ {
                podsToDelete = append(podsToDelete, daemonPodsRunning[i].Name)
            }
        }
    case !shouldContinueRunning && exists:  // 条件4
        for _, pod := range daemonPods {
            podsToDelete = append(podsToDelete, pod.Name)
        }
    }

    return nodesNeedingDaemonPods, podsToDelete, failedPodsObserved, nil
}

另外后续处理中根据nodesNeedingDaemonPods和podsToDelete来调用kubeapi进行Pod创建和删除(具体参照syncNodes()@kubernetes/pkg/controller/daemon/daemon_controller.go的代码实现)
3.2.2 DaemonSet Pod创建和删除

因为DaemonSet Pod在每个节点上最多运行1个Pod,所以Pod创建有以下两种方法:

  • 方法1. 创建的Pod不经过kube-scheduler调度: 直接指定Pod运行节点(即设定pod.Spec.NodeName)。也意味DaemonSet Pod可以在kube-scheduler组件运行之前就启动。
  • 方法2. 创建的Pod需要经过kube-scheduler调度: 主要是抢占调度时,所有Pod都由kube-scheduler来统筹调度更合理。实现上主要通过nodeAffinity来保证Pod最终会调度到该节点。代码实现如下:
@kubernetes/pkg/controller/daemon/daemon_controller.go
func (dsc *DaemonSetsController) syncNodes(ds *apps.DaemonSet, podsToDelete, nodesNeedingDaemonPods []string, hash string) error {
    ...
    if utilfeature.DefaultFeatureGate.Enabled(features.ScheduleDaemonSetPods) {
        // 方法2: 设置NodeAffinity,经过kube-scheduler调度
        podTemplate = template.DeepCopy()
        podTemplate.Spec.Affinity = util.ReplaceDaemonSetPodNodeNameNodeAffinity(
            podTemplate.Spec.Affinity, nodesNeedingDaemonPods[ix])
        podTemplate.Spec.Tolerations = util.AppendNoScheduleTolerationIfNotExist(podTemplate.Spec.Tolerations)

        err = dsc.podControl.CreatePodsWithControllerRef(ds.Namespace, podTemplate, ds, metav1.NewControllerRef(ds, controllerKind))
    } else {
        // 方法1: 直接设置pod.Spec.NodeName,不经过kube-scheduler调度
        err = dsc.podControl.CreatePodsOnNode(nodesNeedingDaemonPods[ix], ds.Namespace, podTemplate, ds, metav1.NewControllerRef(ds, controllerKind))
    }
    ...

从上面代码可知,K8S的V1.11.0版本中如果需要使用方法2,需要在kube-controller-manager的启动参数中打开features.ScheduleDaemonSetPods功能。

上面两个章节已经把各个节点上的Pod创建和删除细节说明完了,下面分析Pod的升级和回滚

3.3 DaemonSet Pod升级和回滚
3.3.1 Pod升级处理
  1. Pod升级动作: 更新Spec.Template中的内容(一般指更新镜像),然后触发新旧Pod的替换。
  2. Pod升级策略由Spec.Update.Strategy字段指定,目前支持OnDeleteRollingUpdate`两种模式
  3. spec.UpdateStrategy.Type=OnDelete: Spec.Template更新后,但是需要用户手动删除旧Pod,然后DaemonSets Contro‖er会利用更新后的Spec.Template创建新Pod(新Pod创建细节参照3.2章节)。代码中处理如下
@kubernetes/pkg/controller/daemon/daemon_controller.go
func (dsc *DaemonSetsController) syncDaemonSet(key string) error {
    ...
    // Process rolling updates if we're ready.
    if dsc.expectations.SatisfiedExpectations(dsKey) {
        switch ds.Spec.UpdateStrategy.Type {
        // OnDelete模式时,直接退出。等待用户自行删除旧Pod
        case apps.OnDeleteDaemonSetStrategyType:
        case apps.RollingUpdateDaemonSetStrategyType:
            err = dsc.rollingUpdate(ds, hash)
        }
        if err != nil {
            return err
        }
    }
    ...
}
  1. Spec.UpdateStrategy.Type=RollingUpdate: Spec.Template更新后,DaemonSets Controller会先删除一定数量的旧Pod,然后再创建新Pod(新Pod创建细节参照3.2章节)
  2. RollingUpdate模式的删除旧Pod操作,需要保证不可用Pod数量小于等于Spec.UpdateStrategy.RollingUpdate.MaxUnavailable指定的数量。
    RollingUpdate模式的代码如下(下面主要为旧Pod删除,新Pod创建请参照3.2章节)
@kubernetes/pkg/controller/daemon/update.go
func (dsc *DaemonSetsController) rollingUpdate(ds *apps.DaemonSet, hash string) error {
    // 获取所有节点上该DS已经运行的Pods
    nodeToDaemonPods, err := dsc.getNodesToDaemonPods(ds)
    if err != nil {
        return fmt.Errorf("couldn't get node to daemon pod mapping for daemon set %q: %v", ds.Name, err)
    }

    // 获取所有的旧Pods
    _, oldPods := dsc.getAllDaemonSetPods(ds, nodeToDaemonPods, hash)
    // 获取最大的不可用Pod数和当前不可用Pod数
    maxUnavailable, numUnavailable, err := dsc.getUnavailableNumbers(ds, nodeToDaemonPods)
    if err != nil {
        return fmt.Errorf("Couldn't get unavailable numbers: %v", err)
    }
    // 对旧Pod进行分类,分为可用Pod和不可用Pod
    oldAvailablePods, oldUnavailablePods := util.SplitByAvailablePods(ds.Spec.MinReadySeconds, oldPods)

    // 不可用旧Pod全部加入待删除队列
    var oldPodsToDelete []string
    for _, pod := range oldUnavailablePods {
        if pod.DeletionTimestamp != nil {
            continue
        }
        oldPodsToDelete = append(oldPodsToDelete, pod.Name)
    }

    // 从可用旧Pod中选取( maxUnavai1ab1e- numUnavai1able)个旧Pod加入待删除队列
    for _, pod := range oldAvailablePods {
        if numUnavailable >= maxUnavailable {
            break
        }
        oldPodsToDelete = append(oldPodsToDelete, pod.Name)
        numUnavailable++
    }
    // 删除oldPodsToDe1ete中的旧pod(保证可用Pod数不低于要求值)
    return dsc.syncNodes(ds, oldPodsToDelete, []string{}, hash)
}
3.3.2 Pod回滚处理

Pod回滚: 意味着DaemonSet的Spec.Template切换成旧的版本。所以可以理解Pod回滚为RollingUpdate模式的升级到旧版本。如果Spec.Template要替换成旧版本,那么首先需要保存旧版本的Spec.Template数据。下面首先说明下保存Spec.Template的数据结构

1). Controller Revision结构说明

- 每次升级的`Spec.Template`数据就是以 Controllerrevision结构存储在ETCD中。Controller Revision结构如下所示:
type ControllerRevision struct {
    metav1.TypeMeta
    metav1.ObjectMeta
    Data runtime.RawExtension
    Revision int64
}
- 其中`Data`中保存序列化的`Spec.Template`数据,Revison是每次升级对应的版本号,从1开始每次升级Revison值+1(即使回滚操作, Revision也会+1)。代码中处理如下:
@kubernetes/pkg/controller/daemon/update.go
func (dsc *DaemonSetsController) constructHistory(ds *apps.DaemonSet) (cur *apps.ControllerRevision, old []*apps.ControllerRevision, err error) {
    ...
    // 最新spec.Template对应的版本号=最大旧版本号+1
    currRevision := maxRevision(old) + 1
    switch len(currentHistories) {
    case 0:
        // 当前ControllerRevision不存在时,创建新的Contro1lerRevision
        cur, err = dsc.snapshot(ds, currRevision)
        if err != nil {
            return nil, nil, err
        }
    default:
        cur, err = dsc.dedupCurHistories(ds, currentHistories)
        if err != nil {
            return nil, nil, err
        }
        // 当版本回滚时会出现ControllerRevision.Revison < currRevision的状态,
这时更新ControllerRevision的Revision为currRevision
        if cur.Revision < currRevision {
            toUpdate := cur.DeepCopy()
            toUpdate.Revision = currRevision
            _, err = dsc.kubeClient.AppsV1().ControllerRevisions(ds.Namespace).Update(toUpdate)
            if err != nil {
                return nil, nil, err
            }
        }
    }
    return cur, old, err
}

2). Pod回滚相关kubectl指令

- `kubectl rollout history daemonset <daemonset-name>`: 列出 DaemonSet所有的 ControllerRevision。输出如下所示:
daemonsets "<daemonset-name>"
REVISION        CHANGE-CAUSE
1               ...
2               ...
...
- `kubectl rollout history daemonset <daemonset-name> --revision=1`: 查看revision=1的ControllerRevision内容。输出如下所示:
daemonsets "<daemonset-name>" with revision #1
Pod Template:
Labels:       foo=bar
Containers:
app:
 Image:       ...
 Port:        ...
 Environment: ...
 Mounts:      ...
Volumes:       ...
- `kubectl rollout undo daemonset <daemonset-name> --to-revision=<revision>`: 回滚到`to-revision`指定的 DaemonSet

- `kubectl rollout status ds/<daemonset-name>`: 查看回滚进度。

3). Pod可以回滚的版本号由Spec.RevisionHistoryLimit控制。当 ControllerRevision的数量超过Spec.RevisionHistoryLimit时,旧的ControllerRevision会被清除。当然被清除的ControllerRevision代表的版本就不能回滚回去了。

3.4 Daemon Set Status的各字段说明:
  • DesiredNumberScheduled: 需要运行该DaemonSet Pod的节点数量
  • CurrentNumberScheduled: 已经运行DaemonSet Pod的节点数量(DesiredNumberScheduled的子集)
  • NumberMisscheduled: 不需要运行该DeamonSet Pod但是已经运行了DaemonSet Pod的节点数量
  • NumberReady: DaemonSet Pod状态为Ready的节点数量(CurrentNumberScheduled的子集)
  • NumberAvailable: DaemonSet Pod状态为Ready且运行时间超过Spec.MinReadySeconds的节点数量(NumberReady的子集)
  • UpdatedNumberScheduled: 已经完成DaemonSet Pod更新的节点数量(DesiredNumberScheduled的子集)
  • NumberUnavailable: DaemonSet Pod尚未就绪的节点数量(= DesiredNumberScheduled- NumberAvailable)

4. DaemonSets Controller的控制流程

经过上面代码级别的细节说明,下面大致梳理一下DaemonSets Controller的控制流程。具体如下:

  1. 获取 DaemonSet: 由key从dsLister(本地缓存)中获取到需要处理的DaemonSet实例
  2. 获取最新的 ControllerRevision和所有旧的ControllerRevision: 如果新的 ControllerRevision不存在,就新创建一个(3.3.2章节)
  3. 获取创建Pod用的hash: 从最新ControllerRevision的 Labels中提取
    curLabels[extensions.DefaultDaemonSetUniqueLabelKey]
  4. 遍历所有节点,创建或者删除DaemonSet Pod (3.1章节和3.2章节)
  5. DaemonSet Pod创建或者删除完成后,进入Pod升级或者回滚处理逻辑(3.3章节)
  6. 清理掉多余的 ControllerRevision(3.3.2章节)
  7. 更新 DaemonSet的Status(3.4章节)

5. 关于DaemonSet的几点思考

  1. 因为 DaemonSet部署的Pod需要作为守护进程运行在每个节点上,所以当容器的Probe检查为非健康时,需要可以重启容器。因此Spec.Template.Spec.RestartPolicy一定要设置要为Always
  2. 相比下面的方式部署守护进程,采用DaemonSet来部署更具优势。

    • 采用二进制方式运行守护进程(比如用 monit或者 systemd管理): Daemon Pod运行方式可以充分利用kubectl等工具的配置能力(如应用升级和回滚等)。同时相比二进制进程,容器具备良好的资源隔离能力。
    • 直接部署Bare Pod: DaemonSet对DaemonSet Pod有更好的生命周期管理。如DaemonSet Pod的结束,重启等。
    • Static Pod运行守护进程: 因为不能使用 kubectl等工具来管理static Pod,所以Pod的升级和回滚将会有不小的工作量。
    • 用 Deployment部署守护进程: 主要是因为 Deployment主要关注Pod的副本数满足用户期待,而不太关注Pod是否在某节点运行起来等。同时用户需要自己配置Pod和节点的亲和性规则。
  3. Daemon Set的 RollingUpdate可能卡住的原因和定位分析如下:

    • RollingUpdate升级是先删除部分旧Pod,再启动新Pod。如果升级卡住,一般应该是新Pod无法启动成功。
    • 首先查找新Pod启动失败原因: 执行kubectl describe pod <new-pod-name>查找Pod启动失败原因。
    • 然后找出问题节点: 比较kubectl get nodeskubectl get pods -l <daemonset-selector-key>=<daemonset-selector-value> -o wide,找到只在kubectl get nodes结果中存在的节点即为问题节点
    • 结合Pod启动失败原因和问题节点,再调查问题原因。
  4. DeamonSet的Rollback处理需要用户提取ControllerRevision中保存的DaemonSet.Spec.Template数据,然后刷新DaemonSet。这样对用户使用来说稍显麻烦,其实可以向 Deployment的RollBack机制学习,在DaemonSet.Spec中增加RollBack相关字段,用户通过更新RollBack中的Revision来回滚,会更友好一些。

6. 参考链接

  1. DaemonSet
  2. DaemonSet源码(V1.11.0)
  3. Performing a Rollback on a DaemonSet
相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
7天前
|
Kubernetes 监控 调度
【赵渝强老师】K8s的DaemonSet控制器
DaemonSet控制器确保每个节点上运行一个Pod副本,适用于监控、日志收集等场景。通过示例创建DaemonSet并查看Pod信息,展示了其自动扩展和回收的能力。视频讲解和代码示例详细说明了DaemonSet的使用方法和调度机制。
|
3月前
|
Kubernetes 监控 调度
在K8S中,DaemonSet类型资源特性?
在K8S中,DaemonSet类型资源特性?
|
1月前
|
Kubernetes 负载均衡 应用服务中间件
k8s学习--ingress详细解释与应用(nginx ingress controller))
k8s学习--ingress详细解释与应用(nginx ingress controller))
163 0
|
3月前
|
Prometheus Kubernetes 监控
在K8S中,DaemonSet类型的资源特性有哪些?
在K8S中,DaemonSet类型的资源特性有哪些?
|
3月前
|
Prometheus Kubernetes 监控
在k8S中,DaemonSet类型的资源特性有哪些?
在k8S中,DaemonSet类型的资源特性有哪些?
|
3月前
|
Kubernetes 容器 Perl
在K8S中,Replica Set和Replication Controller之间有什么区别?
在K8S中,Replica Set和Replication Controller之间有什么区别?
|
3月前
|
存储 Kubernetes 关系型数据库
Kubernetes(K8S) Controller - StatefulSet、DaemonSet 介绍
Kubernetes(K8S) Controller - StatefulSet、DaemonSet 介绍
33 0
|
22天前
|
JSON Kubernetes 容灾
ACK One应用分发上线:高效管理多集群应用
ACK One应用分发上线,主要介绍了新能力的使用场景
|
23天前
|
Kubernetes 持续交付 开发工具
ACK One GitOps:ApplicationSet UI简化多集群GitOps应用管理
ACK One GitOps新发布了多集群应用控制台,支持管理Argo CD ApplicationSet,提升大规模应用和集群的多集群GitOps应用分发管理体验。
|
1月前
|
Kubernetes Cloud Native 云计算
云原生之旅:Kubernetes 集群的搭建与实践
【8月更文挑战第67天】在云原生技术日益成为IT行业焦点的今天,掌握Kubernetes已成为每个软件工程师必备的技能。本文将通过浅显易懂的语言和实际代码示例,引导你从零开始搭建一个Kubernetes集群,并探索其核心概念。无论你是初学者还是希望巩固知识的开发者,这篇文章都将为你打开一扇通往云原生世界的大门。
120 17