kubernetes controller源码解读之DaemonSet

简介: 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
相关实践学习
容器服务Serverless版ACK Serverless 快速入门:在线魔方应用部署和监控
通过本实验,您将了解到容器服务Serverless版ACK Serverless 的基本产品能力,即可以实现快速部署一个在线魔方应用,并借助阿里云容器服务成熟的产品生态,实现在线应用的企业级监控,提升应用稳定性。
云原生实践公开课
课程大纲 开篇:如何学习并实践云原生技术 基础篇: 5 步上手 Kubernetes 进阶篇:生产环境下的 K8s 实践 相关的阿里云产品:容器服务&nbsp;ACK 容器服务&nbsp;Kubernetes&nbsp;版(简称&nbsp;ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情:&nbsp;https://www.aliyun.com/product/kubernetes
相关文章
|
4月前
|
存储 Kubernetes 调度
k8s教程(pod篇)-DaemonSet(每个node上只调度一个pod)
k8s教程(pod篇)-DaemonSet(每个node上只调度一个pod)
67 0
|
3月前
|
消息中间件 Kubernetes 监控
kubernetes—Controller详解(二)
kubernetes—Controller详解(二)
50 0
|
3月前
|
Kubernetes 测试技术 Perl
kubernetes—Controller详解(一)
kubernetes—Controller详解(一)
37 0
|
9月前
|
Kubernetes 固态存储 API
【k8s 系列】k8s 学习十八,replicaSet,DaemonSet and Job
上一篇讲到的 ReplicationController 是用于复制和在异常的时候重新调度节点的 K8S 组件,后面 K8S 又引入了 ReplicaSet 资源来替代 ReplicationController
|
6月前
|
存储 Kubernetes 调度
【云原生】k8s核心概念—Pod & Controller & Service & Serect & ConfigMap介绍——20230213
【云原生】k8s核心概念—Pod & Controller & Service & Serect & ConfigMap介绍——20230213
121 0
|
6月前
|
Kubernetes 监控 安全
【K8S系列】深入解析DaemonSet
【K8S系列】深入解析DaemonSet
276 0
|
6月前
|
Kubernetes 应用服务中间件 调度
kubernetes Ingress、Ingress controller
kubernetes Ingress、Ingress controller
|
7月前
|
JSON Kubernetes 安全
Kubernetes Admission Controller 简介 - 注入 sidacar 示例
Kubernetes Admission Controller 简介 - 注入 sidacar 示例
70 0
|
9月前
|
Kubernetes 负载均衡 监控
一文读懂 Kubernetes Ingress Controller 选型实践
Hello folks,众所周知,Ingress 对于任何成功的 Kubernetes 集群部署拓扑架构都至关重要,尤其是在自建的容器云平台。 Ingress 允许我们在实际的业务场景中能够基于当前的网络环境定义外部(或内部)流量以及其如何路由至集群内的服务。
261 0
|
9月前
|
Kubernetes 监控 网络协议
kubernetes组件 Controller manager深刻认知
kubernetes组件 Controller manager深刻认知
125 0

推荐镜像

更多