ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Kubernetes DaemonSet 实战指南:原理、调度与避坑

Kubernetes DaemonSet 实战指南:原理、调度与避坑 做 Kubernetes 的人基本都绕不开 DaemonSet 这个控制器。它不像 Deployment 那样天天出现在业务发布里也不像 StatefulSet 老被拿来讨论数据库和中间件但只要涉及集群的基础能力建设——网络、日志、监控、安全、存储十有八九都有它的份。这篇文章我打算把 DaemonSet 从原理到落地完整聊一遍包含调度机制、清单写法、更新策略、资源控制、故障排查也会结合我在企业集群里踩过的几个坑希望帮你把它真正用顺。先说清楚一件事这里说的“控制器”不是硬件控制器也不是 PID 控制器、电机驱动电路那类东西而是 Kubernetes 控制面里的控制器Controller。它负责持续观察集群状态把实际状态往期望状态拉。Deployment、StatefulSet、DaemonSet 都属于控制器只是管理逻辑不同。DaemonSet 的核心承诺就一句话在集群中满足条件的所有节点上保证恰好运行一个 Pod 副本。节点新增它会自动补 Pod节点删除对应 Pod 也随之清理。这个能力听起来简单但企业里很多基础组件离开了它运维成本会高到让你怀疑人生。1. 先从调度说起DaemonSet 到底凭什么做到“每个节点一个 Pod”1.1 Deployment、StatefulSet 与 DaemonSet 的本质区别很多初学者会困惑Deployment 也能通过调度把 Pod 打散到不同节点为什么还要单独搞一个 DaemonSet区别在于调度维度。Deployment 调度的是“副本数量”它只保证 N 个副本分布在集群里具体落在哪些节点由调度器决定。节点不够时 Pod 会 Pending节点多了副本也不会因此增加。StatefulSet 则是为有状态应用设计的强调每个 Pod 有稳定的网络标识和存储但也不绑定“一个节点一个副本”的语义。DaemonSet 绑定的是节点维度。它遍历集群里的节点把每个匹配条件的节点当成一个“必须服务的位置”然后在这个位置上放一个 Pod。这种拓扑约束天然适合三类工作负载节点级基础设施kube-proxy、CNI 网络插件Calico、Cilium 的 agent、容器运行时辅助组件没有它们节点就是“裸奔”的。数据采集器日志采集、监控指标采集、安全审计 Agent需要覆盖每一个节点才能不漏数据。本地存储/运维工具需要在每台机器上挂载磁盘、清理镜像、执行巡检任务的组件。我见过有人用 Deployment 强行配合反亲和性来模拟 DaemonSet比如把副本数手动设置成节点数再给每个 Pod 加 nodeName 或者 nodeAffinity。这样看起来“也能用”但代价很大每次集群扩容都要手动改副本数节点故障后 Pod 会被调度到别的地方完全破坏了“节点本机数据必须本机处理”的假设。后来这个团队还是老老实实换成了 DaemonSet。1.2 一次“偷懒”让我意识到它的价值讲一个我自己的经历。之前负责一套生产集群为了节省资源我一开始把日志采集器部署成了 Deployment副本数按当时的节点数填。后面集群扩容新节点进来自动纳管但我忘了同步更新 Deployment 的副本数结果新节点跑了大半天都没有日志采集。等到排查问题的时候才发现那段时间的日志全丢了。这种问题本质上不是操作失误而是选型错了。采集器必须跟随节点生命周期自动伸缩这就是 DaemonSet 存在的意义。后来我把采集器改成 DaemonSet集群加节点后 Pod 自动创建删节点后 Pod 自动回收再也没有出现过“节点在但 Agent 不在”的盲区。再补充一个判断标准如果你的应用“每台机器都要有一份且节点不存在时这个副本也没必要存在”那它大概率应该用 DaemonSet。如果只是“总共要 N 份具体放哪无所谓”那就用 Deployment。这个区分想清楚后面很多方案就顺了。2. 核心机制拆解DaemonSet 控制器是怎么把 Pod 送到每个节点上的2.1 节点筛选与调度逻辑的演进DaemonSet 控制器本身是 kube-controller-manager 里的一个控制器。它的工作流程大致是不断监听 Node 变化、DaemonSet 变化和 Pod 变化然后计算“每个节点上应该有的 DaemonSet Pod”和“当前实际存在的 Pod”之间的差异有差异就补齐或者清理。关键点在于它怎么决定“哪些节点需要 Pod”。早期版本做法比较粗暴DaemonSet 控制器直接把 Pod 的 spec.nodeName 写死为某个节点然后交给 kubelet 去启动。这个方式的问题在于绕过了调度器很多调度策略、资源过滤、亲和性规则都用不上。后来 Kubernetes 改了实现DaemonSet 控制器会为每个匹配的节点生成一个 Pod 模板并且在这个 Pod 上自动注入一条 nodeAffinity要求它必须调度到指定节点。调度器仍然参与资源判断、污点容忍、优先级抢占这些流程只是最终结果被约束在那个节点上。对我们使用者来说这意味着 DaemonSet 的 Pod 同样受调度器管理所以节点资源不足、存在无法容忍的污点、标签不匹配等都会导致 Pod 无法运行。它不是“硬塞给节点”而是“定向调度到节点”。2.2 节点标签、污点和容忍是如何参与决策的DaemonSet 控制器在生成 Pod 时会先判断节点是否满足条件。默认情况下如果你的 DaemonSet 清单里没有写 nodeSelector、nodeAffinity、tolerations那它会在所有节点上创建 Pod包括工作节点也会尝试在控制平面节点上创建——前提是控制平面节点没有不可容忍的污点。实际企业集群里控制平面节点通常会打上污点比如node-role.kubernetes.io/control-plane:NoSchedule。如果你想让某个 DaemonSet 也覆盖控制平面节点必须显式加 toleration。如果不加控制器会跳过这些节点。这里容易踩坑你以为写了一个“所有节点”的 DaemonSet结果主节点上没有等检查的时候才发现控制面组件少了采集器。节点筛选的核心参数包括nodeSelector按节点 label 精确匹配最简单直接。tolerations允许 Pod 调度到带污点的节点。nodeAffinity支持更灵活的标签匹配比如In、NotIn、Exists。举个例子如果只想让 DaemonSet 跑在带有disktypessd标签的节点上可以直接写 nodeSelector。如果想跑在除 GPU 节点外的所有节点可以用 nodeAffinity 的NotIn排除node-role.kubernetes.io/gputrue的节点。2.3 controller revision 与滚动更新时的版本管理另一个容易被忽略的机制是 ControllerRevision。Kubernetes 会为 DaemonSet 的每次模板变更生成一个版本记录。你修改 DaemonSet 的 Pod 模板并执行 apply 之后控制器会创建一个新的 ControllerRevision然后按更新策略逐步替换 Pod。这也是为什么你可以用kubectl rollout history daemonset/name查看历史版本再通过kubectl rollout undo回滚。很多时候更新卡住不一定是 Pod 启动失败也可能是 ControllerRevision 数量过多、历史版本堆积导致 etcd 压力增大。企业里跑了一两年的集群如果频繁更新 DaemonSet建议定期清理旧的 ControllerRevision。虽然默认有revisionHistoryLimit限制默认 10但遇到特殊情况可以检查一下。3. 从零写一个可落地的 DaemonSet3.1 最小清单逐行拆解先看一个最基础的 DaemonSet 清单我用 node-exporter 当例子因为它结构简单、但包含了很多企业落地必须的字段apiVersion: apps/v1 kind: DaemonSet metadata: name: node-exporter namespace: monitoring labels: app: node-exporter spec: selector: matchLabels: app: node-exporter template: metadata: labels: app: node-exporter spec: hostNetwork: true hostPID: true tolerations: - operator: Exists containers: - name: node-exporter image: prom/node-exporter:v1.7.0 args: - --path.procfs/host/proc - --path.sysfs/host/sys - --path.rootfs/host/root ports: - name: metrics containerPort: 9100 hostPort: 9100 resources: requests: cpu: 50m memory: 50Mi limits: cpu: 250m memory: 256Mi securityContext: privileged: true volumeMounts: - name: proc mountPath: /host/proc - name: sys mountPath: /host/sys - name: root mountPath: /host/root readOnly: true volumes: - name: proc hostPath: path: /proc - name: sys hostPath: path: /sys - name: root hostPath: path: /逐个说下关键字段为什么存在。spec.selector.matchLabels必须跟template.metadata.labels一致而且 DaemonSet 创建后 selector 是不允许修改的要改只能删除重建。这一点比 Deployment 更严格因为 DaemonSet 的 Pod 直接绑定节点改 selector 会让控制器无法正确识别新旧 Pod。hostNetwork: true表示 Pod 直接使用宿主机网络。node-exporter、kube-proxy、CNI 插件这类组件通常都需要它因为它们要么要监听宿主机端口要么要访问宿主机网络命名空间。hostPID: true允许 Pod 看到宿主机上的进程。采集指标、清理进程时少不它。tolerations: - operator: Exists表示容忍所有污点。对于采集器这类需要覆盖所有节点的 DaemonSet我一般直接这么写。但如果你对安全要求高不建议全家桶式容忍最好只容忍确定的那几个污点。注意“容忍所有污点”也意味着即使节点被加了 NoExecute 污点Pod 也不会被驱逐这对某些运维场景是好事但对某些安全场景需要谨慎。hostPath挂载宿主机目录时需要确认运行时路径。比如容器运行时是 containerd 的集群日志目录通常在/var/log/pods而不是/var/lib/docker/containers。不同 K8s 版本、不同容器运行时路径会差很多。写死目录前先上节点ls看一眼。创建命令kubectl apply -f daemonset-node-exporter.yaml kubectl get ds -n monitoring kubectl get pods -n monitoring -o wide | grep node-exporter正常情况下每个节点都会有一个 Running 的 Pod。3.2 控制调度范围标签、污点与容忍、亲和性企业里不可能所有 DaemonSet 都跑满“所有节点”。常见的受限场景有三种场景做法示例只跑在指定节点池nodeSelectordisktype: ssd跑在所有节点但不跑 GPU 节点nodeAffinityNotIn排除gputrue容忍主节点污点覆盖控制面节点tolerationskey: node-role.kubernetes.io/control-planenodeSelector 最简单但无法表达“排除”和“范围”的概念。比如你想让 DaemonSet 跑在除 Windows 节点外的所有 Linux 节点nodeSelector 做不到你必须给所有 Linux 节点打一个标签或者用 nodeAffinityaffinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/os operator: In values: - linux注意填 affinity 时DaemonSet 控制器还是会加一层自己的节点亲和性所以实际 Pod 上会同时有控制器注入的亲和性和你写的亲和性两者叠加最终调度到指定节点。容忍度的典型例子是让采集器也跑在主节点上tolerations: - key: node-role.kubernetes.io/control-plane effect: NoSchedule如果你的集群有多个污点比如node-role.kubernetes.io/master老版本集群要单独加。新版本统一用control-plane。3.3 企业实战日志采集与节点监控两个典型场景日志采集是 DaemonSet 用得最多的场景之一。我常用的日志采集方案是 Filebeat 或者 Fluent Bit它们的 DaemonSet 需要把宿主机日志目录挂载进去。核心模板大概长这样spec: template: spec: serviceAccountName: filebeat tolerations: - operator: Exists containers: - name: filebeat image: docker.elastic.co/beats/filebeat:8.13.4 volumeMounts: - name: varlog mountPath: /var/log - name: dockercontainers mountPath: /var/lib/docker/containers readOnly: true - name: podlogs mountPath: /var/log/pods readOnly: true volumes: - name: varlog hostPath: path: /var/log - name: dockercontainers hostPath: path: /var/lib/docker/containers - name: podlogs hostPath: path: /var/log/pods这里我把 docker 和 pods 两个目录都挂进去了是因为很多集群是部分节点用 docker、部分节点用 containerd 的过渡状态。卷名如果冲突可以分两个容器实现但一般挂载不同路径没关系。真正需要关心的是磁盘占用日志量大时 hostPath 目录会暴涨建议给采集器设置日志轮转同时在宿主机上配置 docker/containerd 的 log driver 限制。节点监控的典型代表是 kube-prometheus 里的 node-exporter。它会把宿主机/proc、/sys、/挂载进容器用 hostPID、hostNetwork 读取节点指标。这类组件的核心要求是权限和端口。使用 hostNetwork 时9100 端口直接落在宿主机上要注意端口冲突。如果同一节点另一个进程占用了 9100node-exporter 会启动失败。另外还有网络插件。Calico 的 calico-node、Cilium 的 cilium-agent本质上都是 DaemonSet。它们需要写入宿主机网络配置、创建虚拟网卡所以通常要特权模式还要挂载/lib/modules、/run/xtables.lock这些路径。这些组件不是普通业务 Pod不建议随意修改它们的调度配置否则整个节点网络都可能出问题。4. 更新、灰度与回滚企业里最常用的操作路径4.1 更新策略选型OnDelete 还是 RollingUpdateDaemonSet 支持两种更新策略OnDelete和RollingUpdate。OnDelete的含义是更新模板后控制器不会主动动任何现有 Pod只有你手动删除某个节点的 Pod控制器才会按新模板创建新 Pod。这种模式适合环境敏感的组件比如你想先在几个节点上验证新版本确认没问题后再手动滚动其他节点。RollingUpdate是默认策略控制器会自动滚动更新。它有两个关键参数updateStrategy: type: RollingUpdate rollingUpdate: maxUnavailable: 1 minReadySeconds: 30maxUnavailable控制更新过程中最多可以有多少个 Pod 处于不可用状态。默认值是 1表示控制器逐节点滚动最多一个节点的采集器暂时不可用。minReadySeconds表示新 Pod 至少保持 Ready 多少秒后才继续更新下一个。对于日志采集器、监控采集器这类组件我建议必须设置minReadySeconds最好 30 秒以上否则新版本启动慢、还没 Ready 就被判定失败会引发连锁滚动。从 Kubernetes 1.21 开始DaemonSet 的 RollingUpdate 还支持maxSurge但这个参数只能设置为 0 或 1。当maxSurge1时控制器会先在目标节点上启动一个新 Pod等新 Pod Ready 后再删除旧 Pod实现“先起后删”减少节点上空窗期。代价是更新的瞬间节点上会同时存在两个 DaemonSet Pod需要节点留有足够资源。基础组件建议保持maxSurge0减少资源峰值。4.2 灰度发布实操先让一小撮节点跑新版本DaemonSet 不像 Deployment 支持按比例灰度。它天然是“所有匹配节点一个 Pod”所以常见的灰度做法是利用节点标签划分批次。我常用的流程分三步给灰度节点打标签比如agent-graytrue。创建一个新的 DaemonSet模板和正式版一致只是镜像指向新版本并且 nodeSelector 指向agent-graytrue。验证新 DaemonSet 在这些节点上运行正常后直接把正式 DaemonSet 的镜像也更新为新版本让它滚动到全量节点。这套“双 DaemonSet 并行”的方法在企业里很实用。灰度 DaemonSet 只服务带特殊标签的节点不会影响其他节点正式 DaemonSet 可以等到灰度验证通过再更新。如果节点数量大还可以继续用标签控制批次。比如第一批打batch1第二批打batch2。每验证一个批次再把下一批节点打上标签。灰度 DaemonSet 会自动在新增节点上创建 Pod不需要手动调整副本数。4.3 回滚与版本查看回滚 DaemonSet 的姿势和 Deployment 基本一致kubectl rollout history daemonset/node-exporter -n monitoring kubectl rollout undo daemonset/node-exporter -n monitoring kubectl rollout undo daemonset/node-exporter -n monitoring --to-revision2回滚后控制器会创建新的 ControllerRevision然后按 updateStrategy 滚动到旧版本。注意回滚不会修改节点的“期望状态”而是把 DaemonSet 模板切回旧版本。如果你的集群已经升级到新版本旧模板可能不兼容新节点回滚前最好先看历史版本的镜像和参数是否还支持当前节点。另一个常见的坑是回滚时 maxUnavailable 太小旧版本镜像比较大节点上同时跑着新旧两个版本的 Pod磁盘吃紧导致滚动卡住。这种情况下可以临时调大 maxUnavailable 到 2~3加速回滚。5. 企业落地绕不开的细节资源、权限、安全与网络5.1 资源预留与优先级别让基础组件成为压垮节点的最后一根稻草DaemonSet 默认会跑在每个节点上意味着它的资源请求会乘以节点数。假如你给日志采集器设置了requests: cpu: 500m那 100 个节点就是 50 核的预留资源这个数字相当可观。所以企业里给 DaemonSet 设置资源时一定要克制。我通常按组件类型给参考值组件requests.cpurequests.memory说明日志采集器Filebeat/Fluent Bit100m100Mi日志量大时再上调节点监控node-exporter50m50Mi指标采集资源消耗低网络插件Calico/Cilium agent250m256Mi视节点规模调整kube-proxy100m100Mi连接数高时需上调limits 不要比 requests 大太多防止某个节点日志量爆增把整机 CPU 打满。limits 设置过小也会出问题比如 node-exporter 在/proc很大时内存不够会被 OOM Kill。建议先观察几天的实际使用量再按 p95 值设置。资源紧张时优先级类PriorityClass非常关键。Kubernetes 默认有两个系统级优先级system-cluster-critical和system-node-critical。基础组件建议设置为system-node-critical这样节点资源不足时调度器会优先保证它们的运行甚至可能驱逐低优先级 Pod。priorityClassName: system-node-critical5.2 网络与存储hostNetwork、hostPort 与 hostPathDaemonSet 的网络模式要特别小心。很多基础组件必须使用hostNetwork: true此时 Pod 的端口直接绑定在宿主机上。优点是性能好、配置简单缺点是端口冲突不可由 K8s 自动规避你需要人工保证端口不重复。如果你不想使用 hostNetwork只想让每个节点的 DaemonSet Pod 都暴露一个宿主机端口可以用hostPort。但这里有个坑hostPort 与 hostNetwork 不同前者创建的是 iptables 规则调度时 K8s 不能完全避免同节点端口冲突而且性能略差。一般情况下DaemonSet 暴露端口优先考虑 hostNetwork或者干脆通过 Service 访问指标。存储方面DaemonSet 最常见的卷类型是hostPath。它直接使用宿主机路径适合读日志、挂载运行时 socket、访问设备文件等场景。但 hostPath 不适合做有状态数据持久化比如每个节点的本地数据库因为 Pod 被删除再重建后数据还在原路径但你没有自动清理机制。如果确实需要“每节点一个本地存储”建议用 local PV StorageClass 的延迟绑定模式而不是直接 hostPath。5.3 权限与多集群交付最小化权限和模板化DaemonSet 里的采集器、监控组件、网络插件很多都需要访问 Kubernetes API。比如日志采集器要关联 Pod 元数据就需要读 Pod、Node、Namespace 的权限。正确的做法是给每个 DaemonSet 创建一个独立的 ServiceAccount绑定最小权限的 Role/ClusterRole。一个最小 RBAC 示例apiVersion: v1 kind: ServiceAccount metadata: name: filebeat namespace: kube-system --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: filebeat rules: - apiGroups: [] resources: [pods, namespaces, nodes] verbs: [get, list, watch] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: filebeat roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: filebeat subjects: - kind: ServiceAccount name: filebeat namespace: kube-system然后在 DaemonSet 的 template.spec 里写serviceAccountName: filebeat。别图省事直接给 default ServiceAccount 绑 ClusterRole那是在给整个集群开后门。多集群交付时我建议把所有 DaemonSet 清单用 Kustomize 或者 Helm 管理。不同环境的差异通常只有镜像版本、资源大小、污点容忍、节点标签这几处用模板参数化比复制粘贴 YAML 靠谱得多。特别是集群节点标签不一致时同一套清单通过 values 覆盖 nodeSelector能少走很多弯路。6. 常见问题与排查技巧实录6.1 Pod 一直 Pending事件里报调度失败DaemonSet Pod 如果是 Pending第一步看事件kubectl describe pod daemonset-pod -n namespace最常见的三类原因节点标签不匹配你写了 nodeSelector但目标节点没有对应的 label。检查节点标签kubectl get nodes --show-labels存在不可容忍的污点比如主节点的 control-plane 污点或者运维人员临时加的排空污点。看事件里有没有didnt match pod anti-affinity rules或者didnt tolerate taint。解决方式是加 toleration。节点资源不足DaemonSet 在每个节点上都请求一份资源如果某些节点资源被业务 Pod 占满调度器会跳过。看事件里的Insufficient cpu或Insufficient memory。这种情况可以调小 requests或者给 DaemonSet 设置更高优先级必要时让调度器驱逐低优先级 Pod。还有一个隐藏陷阱DaemonSet 控制器只会在“匹配条件”的节点上创建 Pod如果节点标签在 DaemonSet 创建后才变化新增节点不会自动创建。此时 DaemonSet 控制器会持续尝试但状态可能表现为“期望 5当前 4但没报错”。用kubectl get ds -A看 DESIRED 和 CURRENT 是否一致不一致就是控制器认为某些节点还没建 Pod。6.2 更新卡住滚动状态不前进滚动更新时kubectl rollout status daemonset/name一直卡住常见原因有三个maxUnavailable 设置太小。比如只有一台节点maxUnavailable1 时新 Pod 还没 Ready 就把旧 Pod 删了节点上暂时没有任何可用副本。但节点资源不够启动新 Pod于是卡死。建议基础组件 maxUnavailable 保持 1但 minReadySeconds 不宜太长极端场景下可以临时调大 maxUnavailable。新镜像拉取失败。更新后的 Pod 一直 ImagePullBackOff导致 Ready 永远等不到。需要看具体事件确认镜像地址、tag 是否存在、私有仓库认证是否配置。旧 Pod 一直卡在 Terminating。节点上的容器被阻塞删除不掉。可以强制删除kubectl delete pod pod-name -n namespace --force --grace-period0但这只是权宜之计根本原因要上节点看 containerd/dockerd 日志。遇到滚动卡住先查 ControllerRevisionkubectl get controllerrevisions -n namespace -l apps.kubernetes.io/controller-revision-hash再对比新旧 Pod 模板差异有时候是资源字段写错了导致新 Pod 无法被调度但表面看起来只是“没有滚动”。所以我建议更新前先用kubectl diff预览kubectl diff -f daemonset.yaml6.3 镜像拉取失败与批量节点拉取性能问题镜像问题在 DaemonSet 里会被放大。业务应用只有几十个副本DaemonSet 是全节点跑拉一次大镜像就是几十上百个节点同时拉。新版本发布时镜像仓库很容易被打爆特别是在自建 Harbor 且网络带宽有限的情况下。几个缓解措施更新前先在部分节点上手动拉取镜像预热缓存ctr -n k8s.io images pull registry.example.com/agent:new给镜像仓库配置限流或使用 P2P 分发方案。开源社区已经有成熟的镜像分发加速工具企业内部部署一套非常值得。避免新老镜像体积差距过大。很多 DaemonSet 镜像体积膨胀是因为把调试工具、各种依赖都打进去了。基础组件镜像尽量精简能显著降低滚动更新的失败率。另外私有镜像仓库一定要在 DaemonSet 里配置imagePullSecrets否则新节点加入集群后DaemonSet Pod 会因为拉不到镜像而反复 CrashLoopBackOff。而且如果你把 DaemonSet 的管理权限交给了多个团队建议将 imagePullSecret 统一放到 kube-system 等公共命名空间避免每个团队各自维护一遍。6.4 一个值得养成的排查习惯最后分享一个我自己的习惯每次新建或修改 DaemonSet我都不会直接在集群上改。我会先写一个临时的最小规模版本把 nodeSelector 指向一个临时标签确认新 Pod 在目标节点上运行正常、日志正常、端口正常再把标签扩展到全量节点或者直接把正式 DaemonSet 的模板更新过去。这样做看起来多了一步但对基础组件这种“出问题影响面极大”的负载来说值得。DaemonSet 一上线就是全局生效不像业务应用可以只灰度一批。你永远不希望“新版本采集器把整个集群的日志采集搞挂”这种事情发生。宁愿多花十分钟验证也别拿生产节点当实验田。
返回列表