1. 适用场景
通过 DaemonSet部署的应用(Pod)主要用于满足如下场景:
- 类似守护进程,每个节点保证部署一个应用
- 能跟随节点的新增/移除,自动创建/删除守护应用
- 可以方便的对守护应用进行版本升级或者回滚
实际应用场景中,每个节点都需要的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-scheduler
的Predicate
处理对节点进行评估,判断节点可否运行该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升级处理
- Pod升级动作: 更新
Spec.Template
中的内容(一般指更新镜像),然后触发新旧Pod的替换。 - Pod升级策略由
Spec.Update.Strategy字段指定,目前支持
OnDelete和
RollingUpdate`两种模式 -
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
}
}
...
}
-
Spec.UpdateStrategy.Type=RollingUpdate
:Spec.Template
更新后,DaemonSets Controller会先删除一定数量的旧Pod,然后再创建新Pod(新Pod创建细节参照3.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的控制流程。具体如下:
- 获取 DaemonSet: 由key从dsLister(本地缓存)中获取到需要处理的DaemonSet实例
- 获取最新的 ControllerRevision和所有旧的ControllerRevision: 如果新的 ControllerRevision不存在,就新创建一个(3.3.2章节)
- 获取创建Pod用的hash: 从最新ControllerRevision的 Labels中提取
curLabels[extensions.DefaultDaemonSetUniqueLabelKey]
- 遍历所有节点,创建或者删除DaemonSet Pod (3.1章节和3.2章节)
- DaemonSet Pod创建或者删除完成后,进入Pod升级或者回滚处理逻辑(3.3章节)
- 清理掉多余的 ControllerRevision(3.3.2章节)
- 更新 DaemonSet的Status(3.4章节)
5. 关于DaemonSet的几点思考
- 因为 DaemonSet部署的Pod需要作为守护进程运行在每个节点上,所以当容器的Probe检查为非健康时,需要可以重启容器。因此
Spec.Template.Spec.RestartPolicy
一定要设置要为Always
-
相比下面的方式部署守护进程,采用DaemonSet来部署更具优势。
- 采用二进制方式运行守护进程(比如用 monit或者 systemd管理): Daemon Pod运行方式可以充分利用kubectl等工具的配置能力(如应用升级和回滚等)。同时相比二进制进程,容器具备良好的资源隔离能力。
- 直接部署Bare Pod: DaemonSet对DaemonSet Pod有更好的生命周期管理。如DaemonSet Pod的结束,重启等。
- Static Pod运行守护进程: 因为不能使用 kubectl等工具来管理static Pod,所以Pod的升级和回滚将会有不小的工作量。
- 用 Deployment部署守护进程: 主要是因为 Deployment主要关注Pod的副本数满足用户期待,而不太关注Pod是否在某节点运行起来等。同时用户需要自己配置Pod和节点的亲和性规则。
-
Daemon Set的 RollingUpdate可能卡住的原因和定位分析如下:
- RollingUpdate升级是先删除部分旧Pod,再启动新Pod。如果升级卡住,一般应该是新Pod无法启动成功。
- 首先查找新Pod启动失败原因: 执行
kubectl describe pod <new-pod-name>
查找Pod启动失败原因。 - 然后找出问题节点: 比较
kubectl get nodes
和kubectl get pods -l <daemonset-selector-key>=<daemonset-selector-value> -o wide
,找到只在kubectl get nodes
结果中存在的节点即为问题节点 - 结合Pod启动失败原因和问题节点,再调查问题原因。
- DeamonSet的Rollback处理需要用户提取ControllerRevision中保存的
DaemonSet.Spec.Template
数据,然后刷新DaemonSet。这样对用户使用来说稍显麻烦,其实可以向 Deployment的RollBack机制学习,在DaemonSet.Spec
中增加RollBack相关字段,用户通过更新RollBack中的Revision来回滚,会更友好一些。
6. 参考链接
- DaemonSet
- DaemonSet源码(V1.11.0)
- Performing a Rollback on a DaemonSet