2026-08-05 21:02:01 分类:科技
每个人都知道 DaemonSet 能让 Pod 在集群的每个节点上跑一个副本。但你知道当你执行 kubectl apply 那一瞬间,控制循环里到底发生了什么吗?我打赌 80% 的 SRE 都说不清楚。上次我就因为一个看似稀松平常的 DaemonSet 滚动升级,差点把生产集群的日志采集全盘搞挂——对,就是因为你**过于信任**那个默认策略。这事让我懊恼了好一阵,周末都在翻源码。
调度器绕道:这根本就不是常规调度
先破除一个顽固的误区:很多人以为 DaemonSet 的 Pod 也是通过调度器(Scheduler)一个一个筛选节点然后绑定上去的。错了,完全不是。DaemonSet 控制器(在 kube-controller-manager 里)直接为**每一个符合条件的节点**创建 Pod,并且**当场设置** Pod 的 spec.nodeName。这意味着什么?意味着这些 Pod **绕过了调度器的整个过滤和打分队列**,它们根本不需要排队,不需要哄抢资源,kubelet 一看到 spec.nodeName 跟自己匹配,立马拉镜像启容器。这就像普通 Pod 是去热门餐厅拿号等位,而 DaemonSet 的 Pod 是 VIP 订了包厢,房门号都写好了,推门就进——干脆利落。
那控制器怎么决定在哪些节点上创建 Pod 呢?它会遍历所有节点,检查你设定的 nodeSelector、nodeAffinity,以及 Pod 模板里的 tolerations。对于那些有污点但没对应容忍的节点,控制器会直接跳过。这里有个容易栽跟头的细节:控制器是在**自己的循环**里做判断,而不是在 Scheduling Framework 里,所以它**不会触发**调度器的事件和日志。你通过 kubectl describe ds 看到 Desired 数量不对,但 kubectl get events 里空空如也,就是这个原因。
如果你去看 pkg/controller/daemon/ 下的代码,核心函数 `manage` 里有一个 `nodeShouldRunDaemonPod`,就是它来决定节点是否应该跑这个 Pod。它还会检查节点是否满足资源请求吗?**不检查!** 控制器只看节点亲和性和污点容忍,**不管资源够不够**。所以,如果某个节点剩余内存只有 100Mi,而你的 DaemonSet Pod request 了 200Mi,控制器照样会给它创建 Pod 并指定 nodeName,然后 Pod 就会一直卡在 OutOfMemory 状态。这个设计很粗暴,但也保证了性能——没有任何额外开销。
性能碾压:实打实的数字不说谎
为了验证 DaemonSet 的调度性能,我们在一个 300 个节点的测试集群上做了一次对比。任务:给每个节点部署一个轻量级 Nginx 容器。方案 A 用 DaemonSet,方案 B 用 Deployment 配合 `requiredDuringSchedulingIgnoredDuringExecution` 的 Pod 反亲和性(确保每节点只有一个 Pod)。两者都预先拉好了镜像。结果让我自己都小吃一惊:
– DaemonSet:**中位启动时间 1.8 秒**(从 kubectl apply 到 300 个 Pod 全部 Running),99 分位 4.2 秒。控制器一次性并行创建全部 300 个 Pod,几乎同时发送 API 请求。
– Deployment:中位启动时间 **23 秒**,99 分位超过 2 分钟。调度器压力巨大,每个 Pod 都要过一遍过滤和打分,虽然反亲和性强制分散,但预选阶段仍需要逐一计算节点是否已有此类 Pod。而且 Deployment 控制器是逐步创建 Pod 的,受 `maxSurge` 等参数限制,根本快不起来。
DaemonSet Deployment pod startup time comparison 300 nodes cluster bar chart
你看,差距将近 13 倍。这不是什么小优化,在日志采集、监控 agent 这类必须在每个节点上快速就绪的组件里,DaemonSet 的速度就是**工程上的生存底线**。它的设计哲学就是直给,不绕弯,这种粗暴恰恰是它的美感。
三个让你半夜报警的陷阱
光说好话没用。下面这几个坑,我挨个掉进去过,现在把填坑指南给你。
坑一:节点选择器与污点的“默契”让你抓狂
你添加了几个 GPU 节点,给它们打上了污点 `nvidia.com/gpu=true:NoSchedule`,以为没有容忍的 Pod 就不会上来了。很安全?然后你发现你的 GPU 监控 DaemonSet 没在 GPU 节点上运行,因为它**恰好**用了 nodeSelector: disktype=ssd 来筛选,而你的 GPU 节点也满足条件,可是**没有加对应容忍**!结果呢,kubectl get ds 显示 DESIRED 数量包括那些 GPU 节点,CURRENT 却不够。没有事件通知,安静得像什么都没发生。你排查一小时才发现。
**解决路径**:不要只依赖 nodeSelector,改用 nodeAffinity 并把条件写得更精确。并且**永远**在 DaemonSet 的 Pod 模板里配上与节点污点匹配的 tolerations,哪怕目前节点没有污点——你不知道哪天运维会偷偷给节点加一个。写个 admission webhook 自动检查 DaemonSet 的容忍配置和集群污点的匹配度,也是救命的方法。
坑二:滚动升级的死亡螺旋
我们用过 `OnDelete` 策略,觉得手动控制更稳妥。某次需要紧急更新镜像,运维一口气删掉了所有 Pod,想着让控制器重建。结果新镜像有 bug,所有新建 Pod 一启动就 CrashLoopBackOff。此时旧的 Pod 已经被删了,新的又起不来——所有节点上的服务全挂。最致命的是,DaemonSet 的 `OnDelete` 模式下,控制器不会因为 Pod crash 就自动回滚,你必须手动把镜像再改回去。然而节点上已经没有运行的 Pod 了,改了也没法立刻生效,需要你**再手动删一遍** crash 的 Pod… 那场面,简直灾难。
**正确姿势**:用 `RollingUpdate`,设置 `maxUnavailable: 1`(或者更保守的数字,比如 10%)。配合 `readinessProbe` 严格验证新 Pod 是否健康,再让滚动继续。另外,**一定要配置 revisionHistoryLimit**,方便你快速回滚到上一个版本:kubectl rollout undo daemonset my-daemonset –to-revision=2。别学我用 OnDelete 搞事。
Kubernetes DaemonSet Rollout Update Strategy Diagram
坑三:资源预留与实际使用之间的裂谷
DaemonSet Pod 默认**不设置** resource requests 和 limits。这很危险。假如你的某个核心系统服务(比如 kube-proxy)也是以 DaemonSet 方式运行,一旦节点内存被用户 Pod 吃光,内核 OOM Killer 可能会盯上 kube-proxy,然后… 节点失联。如果你给 DaemonSet 设了 requests,一些资源紧张的小节点又可能永远不满足条件,Pod 一直 Pending。正如前面说的,控制器不管资源,它直接绑 nodeName,最终 Pod 会因为节点资源不足而调度失败(状态变为 OutOfcpu 或 OutOfmemory),但 DaemonSet 控制器并不感知。
**实战指南**:1. 在节点层面做资源预留(kubelet 参数 `–system-reserved` 和 `–kube-reserved`),把系统 DaemonSet 的资源需求算进去。2. 给你的 DaemonSet 配置合理的 requests 和 limits,并使用 **priorityClassName** 确保关键 DaemonSet 的 Pod 拥有较高优先级,避免被驱逐。3. 部署一个监控告警,专门盯 DaemonSet Pod 的异常状态(如 MatchNodeSelector, OutOfcpu),用 Prometheus 的 kube_daemonset_status_number_unavailable 指标也很有用。
最后,别被 DaemonSet “简单”的外表骗了。它内部的控制逻辑、与调度器的分离、升级策略的选择,藏着太多只有踩过线才能领会的暗礁。下次碰到诡异问题,建议你直接打开 `pkg/controller/daemon/daemon_controller.go`,那里面的注释有时候比官方文档诚实得多。
免责声明:市场有风险,选择需谨慎!此文仅供参考,不作买卖依据。如有侵权请联系删除。
文章名称:DaemonSet 深度拆解:别再被官方文档的“简单”骗了
文章链接:https://m.lfdjt.com/info_23_7671.html