第144天学习打卡整理这篇笔记的时候我还挺有成就感的因为今天终于把Kubernetes DaemonSet这个一直靠背概念撑着的知识点变成了能动手跑通、也能说清原理的东西。如果你也在学K8s或者工作中正好要给集群加日志采集、节点监控这类“每个节点都得有一份”的组件这篇打卡笔记应该能帮上忙我先梳理K8s核心概念和集群组件之间的关系再重点拆DaemonSet的设计动机、调度机制和滚动更新最后把我这几天实验踩过的坑完整复盘一遍。1. 先把地图画出来K8s核心概念与集群组件是谁在干什么1.1 控制面与工作节点一个集群的“大脑”和“手脚”学K8s最怕一上来就背一堆名词我今天尝试的办法是先分清两类节点然后再谈组件。控制面上的几个东西说白了就是集群的“大脑”API Server是唯一的前门你所有的kubectl命令、各种控制器的请求全部都要先过它这一关它负责鉴权、校验然后把资源状态写进最底层的存储那个负责存状态的就是etcd集群里任何东西的“期望状态”和“实际现状”都存在这里再往上是Scheduler它专门做决策——“一个刚创建的Pod应该放到哪台节点上”最后是Controller Manager它里面装着一堆小控制器每个控制器各管一类资源处理Deployment的有Deployment控制器处理节点状况的有节点控制器我们今天的主角DaemonSet也有一个专门的DaemonSet控制器。工作节点上的组件要简单一些但同样不能缺席。kubelet是每台节点上的“监工”它负责跟API Server确认这台节点该跑哪些Pod然后调用容器运行时把容器真正拉起来、跑起来、健康检查容器运行时就是实际创建和运行容器的那一层kube-proxy则负责维护节点上的网络规则让Service的访问能正确转发到Pod。你会发现一个有意思的现象连kube-proxy这种K8s自己的组件在很多集群里本身也是以DaemonSet的形式跑在每个节点上的。也就是说K8s内部就在用DaemonSet来解决“每节点一个系统组件”的问题这比任何教程都更能说明它的定位。1.2 “期望状态”驱动的控制器循环为什么K8s总能把事情掰回正轨我学K8s到今天觉得最核心的一个概念就是“期望状态”加上“控制器循环”。你可以把集群想象成一个带恒温器的空调你设定26度这是期望状态温度传感器告诉你现在实际是28度控制器就开始制冷直到温度和设定值一致期间如果有人开窗导致温度升高控制器又会继续工作把温度拉回去。K8s就是一套被放大无数倍的恒温器。你通过YAML告诉它“我要3个副本”这份声明就是期望状态会被写进etcd。控制器时刻在对比“期望状态”和“实际状态”发现副本只有2个就创建第3个发现某个Pod挂了就重新拉起一个发现节点多了就补上新节点上缺失的副本。这套机制叫做调谐循环也是声明式API的核心价值——你不必一步步告诉系统“先删这个、再建那个”你只需要说清楚最终要什么剩下的收敛过程由控制器负责。这个思维一旦建立起来后面遇到的很多“怪现象”就都不怪了。比如你删掉了一个Deployment管理的Pod它马上又冒出来一个不是系统抽风而是控制器发现实际副本数低于期望副本数正在努力把状态掰回去。我今天后面踩的坑好几个都是因为没有先想清楚这个逻辑。1.3 DaemonSet在这张地图里的位置把前面两节串起来DaemonSet的位置就很清晰了。它本身是Controller Manager里的一个控制器它的任务是保证每个符合条件的节点上恰好运行一个Pod副本当新节点加入集群时自动在新节点上创建Pod当节点被删除时该节点上的Pod也会被回收。它跟Deployment最大的区别在于Deployment关心的是“总共几个副本”而DaemonSet关心的是“每个节点有没有那一份”。这意味着DaemonSet的带宽和效果也跟节点数绑定节点多Pod就多节点少Pod就少。它天生不适合用来跑无状态Web服务但非常适合跑那些“必须呆在节点本地”的组件比如网络插件、日志采集、监控采集、存储挂载这些。理解这一层你就不会再把DaemonSet和Deployment当成可以互相替换的同类工具了。2. DaemonSet的定位它和其他控制器到底差在哪2.1 三张牌对比Deployment、StatefulSet、DaemonSet很多人学到这里会问为什么K8s要搞这么多种控制器一个Deployment打天下不行吗答案是不行因为它们解决的问题维度完全不同。我整理了一张对照表是我自己最常用的理解方式维度DeploymentStatefulSetDaemonSet副本逻辑指定N个副本分布由调度器决定指定N个副本每个有稳定编号每个节点一份副本数匹配节点的数量网络身份Pod名字随机IP不固定有稳定的网络标识和顺序Pod归属节点靠节点区分存储通常用共享卷不强调绑定每个副本可绑定独立PVC有粘性一般直接读写节点本地目录或hostPath典型场景无状态服务、API、Web前端数据库、消息队列等有状态服务kube-proxy、CNI、日志采集、监控agent扩容逻辑手动或HPA调整副本数顺序扩缩容节点加入自动扩展无需手动设置副本数这张表的重点是Deployment的副本数是“拍脑袋定的”你可以随便写3个、5个、10个DaemonSet的副本数不是写出来的而是“节点数减去不匹配的节点数”自动算出来的。所以你写DaemonSet的YAML时spec里根本没有replicas这个字段这是它最容易识别的特征。2.2 四个“天生必须每节点跑一份”的场景为什么有些东西就是必须每节点一个我结合工程场景给你拆四个例子。第一个是网络相关组件。比如每个节点上都需要的kube-proxy它要把Service的ClusterIP规则同步到本机的iptables或者IPVS里这份规则是节点本地的不可能在一台节点上生成然后远程分发到别的节点。第二个是容器网络插件也就是常说的CNI它需要在每个节点上创建虚拟网卡、维护路由表把Pod的网络打通同样离不开节点本地操作。第三个是日志采集。容器日志默认写在节点磁盘的固定目录下比如/var/log/containers采集agent必须直接读本机目录才能拿到这台节点所有Pod的日志。第四个是节点监控。CPU、内存、磁盘这类指标必须从每台节点上采集所以监控agent也天然是DaemonSet。这四个例子有个共同点它们都要访问节点本地的资源——网络栈、文件系统、设备信息一旦跑成普通DeploymentPod可能都被调度到同一两台节点上其他节点就没人管了。2.3 只跑部分节点从nodeSelector到nodeAffinityDaemonSet默认是“每个节点一份”但现实里经常有“只跑在部分节点上”的需求。比如你的集群里混着GPU节点和普通节点GPU节点需要额外跑一个驱动管理组件那这个组件就应该只出现在带GPU标签的节点上。最原始的办法是nodeSelector你在Pod模板里写死一个标签选择器比如disktype: ssd调度器就会只看带有这个标签的节点。这个方法简单直观但表达能力有限它只能做“键值完全相等”的判断。想要更复杂的匹配比如“节点属于某几个可用区之一”“节点上存在某个特殊标签”就要用nodeAffinity了。nodeAffinity支持In、NotIn、Exists、DoesNotExist、Gt、Lt这些操作符还能区分硬性要求和软性倾向。硬性要求写成requiredDuringSchedulingIgnoredDuringExecution意思是调度时必须满足不满足就不调度软性倾向是preferredDuringSchedulingIgnoredDuringExecution表示尽量满足满足不了也能落到别的节点。我在实践中通常先把nodeSelector用起来因为它最直观只有当出现“多个条件组合”或者“取反逻辑”的时候才升级到nodeAffinity。3. 实测用DaemonSet给集群装上节点指标采集器3.1 实验环境与这次要验证的三个问题理论看得再多不动手等于没学。我今天专门搭了个三节点的测试集群来验证DaemonSet一台控制面节点两台工作节点安装方式不重要关键是节点数和角色要对齐。我选了一个非常典型的场景——给每个节点部署一个指标采集器把节点自身的CPU、内存、磁盘指标暴露出来。这东西如果不用DaemonSet就得手动在每台节点上装agent和管理生命周期想想就头大。这次实验我给自己定了三个验证目标第一DaemonSet创建后两台工作节点上是否各出现一个Pod第二我给节点加标签限制范围后DaemonSet是否能只跑在匹配标签的节点上第三我把Pod模板里的镜像版本改一下滚动更新能否按预期一个节点一个节点推进。三个问题都跑通我才敢说这个知识点真的掌握了。3.2 逐段拆解DaemonSet的YAML我写的YAML大概是这样的里面每段都有讲究apiVersion: apps/v1 kind: DaemonSet metadata: name: node-agent namespace: ops-monitor spec: selector: matchLabels: app: node-agent updateStrategy: type: RollingUpdate rollingUpdate: maxUnavailable: 1 minReadySeconds: 10 revisionHistoryLimit: 5 template: metadata: labels: app: node-agent spec: nodeSelector: kubernetes.io/os: linux tolerations: - operator: Exists containers: - name: agent image: ops-registry.local/ops/node-agent:v1.0.0 imagePullPolicy: IfNotPresent args: [--node$(NODE_NAME)] env: - name: NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName resources: requests: cpu: 100m memory: 64Mi limits: cpu: 500m memory: 256Mi先说apiVersion和kindDaemonSet现在稳定版本用的是apps/v1这个不要写旧版本。selector是Pod标签选择器必须和template里的labels对得上而且selector创建后一般不能改因为控制器要靠它来“认领”哪些Pod是它管的。这里没有replicas字段就是因为副本数由节点数决定。nodeSelector我用来排除非Linux节点如果你的环境只有一种系统这行甚至可以省略但写上能帮你养成好习惯。tolerations那里我写了operator: Exists相当于容忍所有污点这是为了演示方便让控制面节点如果有污点也能跑。生产环境里你通常只会在DaemonSet上容忍特定污点而不是无脑容忍全部特别是有NoSchedule效果的污点全容忍容易让不该跑的节点也跑上系统组件。容器镜像我用了内部registry的一个假地址你实验时要换成你自己能拉取到的镜像。args里的--node$(NODE_NAME)是从env注入的env里用fieldRef取了spec.nodeName也就是Pod被调度的节点名。这个技巧在DaemonSet里非常常用因为agent通常需要知道自己跑在哪台节点上。最后resources那段我给agent设置了requests和limits避免它在节点上吃太多资源这是生产环境的基本素养。3.3 部署、验证和缩放到部分节点创建命名空间和应用YAML我就不啰嗦了。部署完以后我第一时间执行了这几条命令kubectl get ds -n ops-monitor kubectl get pods -n ops-monitor -o wide第一条输出里有个很关键的栏位组合Desired、Current、Ready三个数字理论上一台节点对应一个所以两台工作节点就应该是2、2、2。第二条命令能看到Pod各自落在了哪台节点上并且节点名互不相同。如果Desired数量大于Ready数量说明有Pod没起来这时候要看Pod状态属于后面排错环节的内容。验证通过后我做了第二个实验给其中一台节点打上标签然后修改YAML里的nodeSelector让DaemonSet只跑在这种标签的节点上。kubectl label node worker-01 agent-modeedge把nodeSelector改成agent-mode: edge之后重新apply你会发现一个很典型的行为不符合标签的另一台节点上的Pod被自动删掉了符合标签的节点上Pod保留。因为控制器发现“这个节点不再匹配我的选择器了”于是主动回收了那个Pod。这个行为在官方文档里没有特别强调但实际运维中经常用到比如上线新节点时先打标签让DaemonSet只在新节点上跑。最后清理的时候要注意删除DaemonSet会连它创建的Pod一起删掉不需要你一个个去清。4. 调度方式与滚动更新DaemonSet的两个核心机制4.1 调度方式的演进为什么现在交给默认调度器我在读官方文档时发现DaemonSet的调度方式有过一次重要演进理解这个演进能帮你少踩很多坑。早期的实现里DaemonSet控制器会直接给Pod指定节点相当于绕过调度器“钦定”运行位置。这样做的好处是简单但坏处也很明显调度器拥有的那些高级能力比如资源请求匹配、节点亲和性、污点容忍、拓扑分布约束全都用不上了很多Pod都无法用统一逻辑去调度。现在的实现改成了这样DaemonSet控制器先算出每台匹配节点上要跑Pod然后把“这台Pod只能在这台节点上”表达成一个强制性的节点亲和性要求再交给默认调度器去完成调度。也就是说最终放哪里的决定权在调度器手里控制器只是把范围圈定到了某一台节点。这个演进让我体会到K8s设计上的一个倾向尽量复用统一机制而不是给每种资源都搞一套特殊逻辑。因为走的是调度器逻辑DaemonSet的Pod同样要遵守资源requests和limits同样要处理污点和容忍。我前面说过DaemonSet控制器还会给Pod自动加上一些默认容忍比如节点not-ready或者unreachable场景下的驱逐容忍具体细节会随版本微调但思路是一样的让DaemonSet这类节点级组件在节点暂时出问题时不至于第一时间被驱逐毕竟你通常希望节点恢复后它还在。4.2 maxUnavailable、maxSurge、minReadySeconds怎么配合DaemonSet更新模板的方式有两种OnDelete和RollingUpdate。OnDelete的意思是只有你手动删掉某个Pod控制器才会用新模板创建新的RollingUpdate则是控制器自动分批替换也是默认策略。这个设计跟Deployment几乎一样但这里有个隐藏细节普通的Deployment滚动更新常用maxSurge来控制“多起几个新Pod再摘旧Pod”而DaemonSet的滚动更新里maxSurge是一个相对较新的能力很多版本里默认只支持maxUnavailable需要注意版本支持情况。我这次实验只配置了maxUnavailable: 1含义是更新过程中最多允许一个Pod处于不可用状态。控制器会一次挑一台节点在这台节点上先删掉旧Pod再创建新Pod等它Ready了再动下一台。这样做的好处是整个过程不会让两个节点同时缺失agent适合节点监控这种希望始终有数据的场景。如果你把maxUnavailable调大比如2那可能同时有两台节点短暂不可用更新更快但风险更高。minReadySeconds我也设了10秒意思是Pod进入Ready状态后再观察10秒才算真正“可用”控制器才会继续下一台节点。这个参数能过滤掉那些“刚起来的时候说Ready、一加载业务就崩”的假健康现象我在生产环境里一般不会省掉它。另外DaemonSet没有Deployment那种paused字段也就是说你不能把滚动更新挂起只能靠调整maxUnavailable以及观察历史版本来控制节奏。4.3 更新失败别慌回滚命令与判断时机一次成功的滚动更新当然好但我在实验里故意制造了更新失败这部分经验特别值钱。我修改了镜像标签从v1.0.0改成v1.1.0然后执行kubectl rollout status ds/node-agent -n ops-monitor这条命令本意是等滚动完成结果它一直不成功配合别的命令一看新Pod卡在镜像拉取失败。这时候第一反应别急着改回旧版本先确认失败原因。查看历史版本和回滚的常用命令是kubectl rollout history ds/node-agent -n ops-monitor kubectl rollout undo ds/node-agent --to-revision1 -n ops-monitorrollout history能看到这个DaemonSet更新过的版本列表undo可以回滚到指定历史版本。不过回滚前一定先搞清楚失败原因如果是因为镜像标签写错了那直接修正YAML再apply也行如果是因为配置整体有问题回滚到上一版是更稳妥的选择。判断“是否该回滚”的标准我个人的经验是当新版本Pod持续ImagePullBackOff、CrashLoopBackOff而且你短时间内找不到根因时立刻回滚恢复服务比在现场硬扛更符合运维原则。5. 踩坑实录这次学习打卡路上翻车的三段经历5.1 翻车一nodeSelector拼错Pod在Pending里躺了半小时我做完基础部署后想做定向调度实验给一台节点打了标签agent-modeedge然后改YAML。改的时候手一抖把nodeSelector写成了agentmode: edge少了个连接符。apply之后kubectl get pods看到的Pod一直Pending还以为是集群出问题了。排查的时候我执行了kubectl describe pod在事件里看到一行很明确的提示节点没有匹配的标签。我这才回头去看节点标签发现标签键是agent-mode而YAML里写的是agentmode。修复的方式就是改回正确的字段名重新apply。这个坑看起来很小但它的排查思路很有代表性Pod调度不上去第一件事永远是看describe里的Events那里会直接告诉你调度器拒绝的原因而不是猜这猜那。5.2 翻车二镜像地址写错滚动更新卡死在ImagePullBackOff这个坑是我故意埋的但埋得过于真实了。我把镜像地址的仓库名从ops-registry.local/ops/node-agent写成了ops-reigstry.local/ops/node-agent多了一个字母。apply之后rollout status一直不结束然后我看到新Pod的状态是ImagePullBackOff。这里最值得记的排查动作是看事件。我执行了kubectl get events --sort-by.lastTimestamp里面明确写出了拉取失败的原因连地址拼写都能看出来。修正镜像地址后重新apply滚动更新立刻恢复正常。事后我总结凡是涉及滚动更新卡住的场景先看DaemonSet的Desired和Ready数量再看Pod状态最后看事件三步下来基本能定位。5.3 翻车三手动删Pod之后它还“阴魂不散”实验做完了我想清理现场手动去删了一个DaemonSet管理的Pod结果刚删完它又自动冒出来了。我当时还以为自己没删干净又删了一遍又冒出来一个。反复几次才反应过来这就是我一直念叨的“期望状态”在起作用——DaemonSet要求每个节点有一个Pod我删掉一个它的控制器立刻就补一个。想真正让Pod消失只能删DaemonSet本身或者通过修改nodeSelector让节点不匹配。这个坑说明了一个问题学K8s不能只记命令必须理解背后的声明式模型。如果你不理解“控制器会不断收敛状态”这个核心概念很多K8s上的“灵异事件”都能让你排查到怀疑人生。5.4 我总结的四步排错链路这几天的翻车经历让我沉淀了一套排错顺序分享给你遇到DaemonSet问题可以照这个顺序来先看全局kubectl get ds -n 确认Desired、Current、Ready三个数字的差距快速判断是没调度、没启动还是没就绪。再看控制器kubectl describe ds -n 看它的滚动更新状态和Events。再看Podkubectl get pods -n -o wide把Pod状态和所在节点对应起来有异常就describe单Pod读里面的事件。最后看事件流kubectl get events --sort-by.lastTimestamp拉出集群最近的完整事件很多时候根因就藏在最后几行里。这套顺序的核心思想是从控制器到Pod再到底层事件逐层收敛范围不要一上来就钻到某个Pod里去猜。6. 第144天打卡今天真正弄懂的东西和明天的计划6.1 今天真正沉淀下来的东西今天最大的收获不是多背了一个控制器的YAML写法而是真正理解了“控制器”的含义。以前我总觉得Deployment、DaemonSet这些是一堆需要记忆的模板今天自己动手把每个字段的作用、每个调度行为背后的机制走了一遍才意识到它们共用的是同一套“期望状态与调谐循环”的内核只不过各自关心的期望维度不同。第二个沉淀是调度和更新的细节。DaemonSet从“控制器指定节点”演进到“走默认调度器”这不是一个冷门历史而是理解了为什么DaemonSet Pod也会遵守资源请求、污点容忍这些通用规则。滚动更新的参数组合我也在实验里验证了maxUnavailable和minReadySeconds的配合节奏以后在真实集群里给日志采集组件升级版本心里就有底了。6.2 明天要啃的硬骨头按我自己的计划下一步准备研究Controller Manager里的informer机制也就是控制器到底是怎么实时感知“Pod被删了”“节点加入了”这些变化的。理解了watch和informer才算真正打通从API Server到控制器的最后一段链路。另外我给自己留了个小任务把现在集群里的日志采集组件从手动部署改成DaemonSet托管并且在改的过程中观察它在新节点加入时能否自动补位。如果你也在学这部分我建议你也找一个“节点本地属性很强”的组件来练手——亲手把DaemonSet跑起来观察一遍比看十篇教程都管用。