kubernetes controller源码解读之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
上一篇:kubernetes controller源码解读之StatefulSet


下一篇:图文详解:漏洞扫描快速入门的流程