Kubernetes控制器深度解析:DaemonSet与Job实战指南
1. 先理清控制器的分工别再看见工作负载就用Deployment做 Kubernetes 运维的日常打交道最多的控制器肯定是 Deployment。它能解决大部分无状态服务的部署、滚动升级和副本伸缩问题。但等你真要在生产环境里部署日志采集器、节点监控插件或者跑一个数据清洗、批量计算任务时你会发现 Deployment 完全不是这块料——它讲究的是“永远保持 N 个副本在线”而这两类需求一个要的是“每台节点恰好有一个 Pod”另一个要的是“跑完就要结束”。去翻文档你会找到两个名字DaemonSet 和 Job以及它的兄弟 CronJob。这篇文章我想把这两个控制器从原理到实战完整串一遍它们分别解决了什么问题YAML 怎么写线上怎么排查。适合刚学完 Deployment 想进一步掌握工作负载用法的同学也适合给生产集群里被日志采集、定时任务搞到头疼的运维一份可直接抄的作业。1.1 Deployment、DaemonSet、Job 三者的核心差异先看一个最本质的区别这三个控制器对“成功”的定义完全不同。Deployment 认为“成功”是集群里始终有指定数量的 Pod 在运行。比如你设置了 3 个副本控制器就会保证任何时刻都有 3 个 Pod 处于 Running 状态其中任何一个挂掉它会立刻补一个新的维持期望值。DaemonSet 认为“成功”是集群里每个符合条件的节点上恰好有一个 Pod 在运行。节点数量是动态的新增节点时DaemonSet 会自动在那个节点上拉起 Pod节点被删除时对应 Pod 也会被回收。它不关心副本数是不是 3 或 5它只关心“每个节点都有一份”。Job 认为“成功”是指定数量的 Pod 都完成了任务并正常退出。它会持续跟踪有多少个 Pod 以退出码 0 结束直到达到目标数量才把整个 Job 标记为 Completed。如果 Pod 失败了它会根据配置决定是否重新创建一个 Pod 继续尝试。一个通俗的类比是Deployment 像 7x24 小时在线的客服时刻保证有人接电话DaemonSet 像是每层楼各安排一位安全员楼层在安全员就在Job 像是外包团队合同约定的活儿干完就撤场干不完就重来。1.2 业务场景与控制器匹配速查很多人分不清某个需求到底该选哪个控制器其实只要把需求描述成一句话答案就出来了。我整理了一个速查逻辑需求特征首选控制器理由每台节点都要跑采集器/代理DaemonSet日志、监控、网络插件天然和节点绑定跑一个任务结束后不再需要Job有明确终止条件不占副本名额周期性地跑批处理任务CronJob本质是 Job 加定时调度无状态服务持续对外提供请求Deployment期望副本数固定随时扩缩容任务需要严格串行一次只能一个Jobparallelism1通过并发数限制实现串行这里有个容易混淆的点日志采集这种需求如果日志文件路径在节点上采集器必须跟日志文件在同一台主机上你用 Deployment 把采集器调度到任意节点只会采集到所在节点的日志。所以当需求里出现“每个节点”“全部节点”这类关键词时不要犹豫直接换 DaemonSet。2. DaemonSet 的设计原理与核心机制理解 DaemonSet 不能只看表面 YAML你得清楚它内部是怎么保证“每个节点一个 Pod”的以及它和普通调度有什么本质区别。2.1 一条 Pod 如何保证“每个节点一份”DaemonSet 控制器在创建 Pod 时会往 Pod 的调度信息里注入一个必需的节点亲和性条件把 Pod 限制在目标节点集合内。这个亲和性是requiredDuringSchedulingIgnoredDuringExecution类型意思是调度时必须满足Pod 运行后即使节点标签变化也不会驱逐它。具体来说DaemonSet 会根据 Pod 模板里的nodeSelector、节点亲和性规则以及节点上的污点容忍情况计算出当前应该运行该 Pod 的节点列表。然后对每个节点它都会单独构造一个 Pod 对象绑定到该节点。这就是为什么你用kubectl get pods -o wide查看 DaemonSet 的 Pod 时每个 Pod 的NODE列都是分散的并且 Pod 名称里会带上节点相关的哈希。另外 DaemonSet 控制器还会自动为 Pod 注入一些系统级容忍配置确保守护进程能在特殊节点上跑起来。比如控制面节点默认带NoSchedule污点如果你不在 Pod 模板里声明容忍调度器会拒绝把 Pod 放上去。实际部署日志采集、监控采集这类组件时通常需要显式声明对控制面污点的容忍否则 master 节点上就一直缺一个采集器。2.2 更新策略OnDelete 与 RollingUpdateDaemonSet 支持两种更新策略这是生产环境最容易踩坑的地方。OnDelete策略意味着当你修改 DaemonSet 的 Pod 模板后控制器不会主动去动任何现有 Pod只有当一个 Pod 被手动删除时它才会用新模板重建这个 Pod。这种策略适合灰度验证你先手动删除某个节点的采集器让集群用新镜像顶上观察没问题后再手动滚动删除其余节点。RollingUpdate策略是默认且常用的方案。它像 Deployment 的滚动更新一样逐步用新模板替换老版本 Pod。你可以在rollingUpdate.maxUnavailable里控制最多允许多少个 Pod 同时不可用也可以设置minReadySeconds让新 Pod 就绪后等待一段时间再继续滚动避免“刚起来又被压垮”。这里有个实际经验maxUnavailable建议按节点规模设置。节点少可以设 1节点多可以适度调大。如果你设置成 0就意味着滚动更新期间必须保证所有 Pod 都可用控制器只能等一个 Pod 就绪后才删下一个大规模集群下更新速度会非常慢甚至让人觉得“卡住了”。2.3 什么场景适合 DaemonSet什么场景要避开适合 DaemonSet 的场景基本都有“节点级”这个共性。日志采集是最典型的例子。节点上的容器日志、系统日志都在本地文件系统采集进程必须运行在同一节点上才能读到。用 DaemonSet 让每个节点一个采集 Pod再把宿主机的日志目录挂载进去是最合理的架构。节点监控指标采集也一样。监控 agent 需要读取节点 CPU、内存、磁盘、网络等指标这些数据必须本机读取通过宿主机网络暴露。还有网络插件组件比如节点上的 CNI 相关 agent以及 kube-proxy 这类集群网络组件本质上都是 DaemonSet 形态。但我见过一些错误用法把尊重数据的中间件也用 DaemonSet 部署比如想在每个节点跑一个缓存实例。这种需求要谨慎如果数据需要多副本同步或者需要持久化存储节点宕机时 DaemonSet 虽然会把 Pod 迁移到其他节点其实会重建但数据恢复成本很高。DaemonSet 更适合“无状态或本地临时状态”的守护进程不适合需要分布式一致性、持久化存储的服务。3. DaemonSet 实战部署写一份能落地的节点日志采集 YAML光讲原理不够我们还是直接写一份带注释的 DaemonSet 配置把一个典型的采集任务跑起来。3.1 一份能落地的节点日志采集 YAML 长什么样假设场景每个节点需要跑一个日志采集 agent采集/var/log和容器运行目录下的日志并且要能容忍控制面污点同时采集后需要上报到某处。以下是一份实际可用的模板apiVersion: apps/v1 kind: DaemonSet metadata: name: node-logger namespace: ops labels: app: node-logger spec: selector: matchLabels: app: node-logger updateStrategy: type: RollingUpdate rollingUpdate: maxUnavailable: 1 minReadySeconds: 10 template: metadata: labels: app: node-logger spec: hostNetwork: true tolerations: - key: node-role.kubernetes.io/control-plane operator: Exists effect: NoSchedule - key: node-role.kubernetes.io/master operator: Exists effect: NoSchedule containers: - name: collector image: your-image-registry/collector:latest args: - --log-dir/var/log securityContext: privileged: true env: - name: NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName volumeMounts: - name: varlog mountPath: /var/log readOnly: true - name: container-logs mountPath: /var/lib/docker/containers readOnly: true resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi volumes: - name: varlog hostPath: path: /var/log - name: container-logs hostPath: path: /var/lib/docker/containers这份配置里有几个关键点单独拿出来讲。第一hostNetwork: true。日志采集器通常需要直接访问宿主机的网络资源同时很多采集器会监听端口用 hostNetwork 可以直接把端口暴露在节点上。但这也带来端口冲突风险同一节点上不能同时跑两个监听相同端口的 Pod。第二污点容忍没有用operator: Exists一股脑全部容忍。我见过很多模板为了省事写- operator: Exists意思是容忍所有污点。这确实能确保 Pod 被调度到任何节点但也可能让采集器跑到你不想部署的节点上。生产环境建议精确到 key 和 effect只容忍必要的控制面污点。第三NODE_NAME环境变量通过 fieldRef 注入。采集器上报日志时通常需要带上节点名这个字段的值就是当前 Pod 所在的节点名。第四资源限制要结合节点数算总量。比如这个模板里单个 Pod 的内存 limit 是 512Mi如果集群有 20 个节点光这一套 DaemonSet 满跑就是 10GiB 内存上限。在节点规格较小的集群里这个占用比例会很明显。3.2 部署与验证从 apply 到 rollout status保存文件后用标准命令部署kubectl apply -f node-logger-ds.yaml然后依次检查状态kubectl get ds -n ops node-logger kubectl get pods -n ops -o wide -l appnode-logger kubectl rollout status ds/node-logger -n opskubectl get ds会看到DESIRED、CURRENT、READY、UP-TO-DATE、AVAILABLE几列。如果DESIRED等于节点数量而CURRENT小于它说明有些节点的 Pod 还没创建成功需要进一步看事件。更细致的排查推荐kubectl describe ds/node-logger -n ops它会列出控制器最近的调度事件。如果有节点因为污点无法调度这里会直接给出did not match tolerations之类的提示。更新配置时直接修改 YAML 再kubectl apply即可。如果希望手动控制的 Pod 在下次发布时不被动更新可以把updateStrategy.type改成OnDelete。滚动更新的过程可以用kubectl rollout status观察进度如果卡住通常是因为maxUnavailable设置太小或者镜像拉取失败。3.3 别忘了处理污点和节点选择器很多初学者会在 DaemonSet 的 Pod 模板里写nodeSelector这完全没问题它决定 DaemonSet 在哪些节点上运行。比如只想在 GPU 节点上采集 GPU 监控可以写spec: template: spec: nodeSelector: gpu-node: true这样 DaemonSet 只在带gpu-nodetrue标签的节点上创建 Pod其他节点不会跑。但要注意如果你同时用了nodeSelector和污点容忍调度行为是两者叠加的。节点必须满足标签条件同时 Pod 必须容忍该节点上的污点才能调度上去。实际生产环境中GPU 节点往往带有资源类污点比如nvidia.com/gpu:NoSchedule之类的第三方污点你需要在容忍列表里显式加一条否则 Pod 会一直 Pending。4. Job 控制器的执行模型与参数设计Job 和 DaemonSet 是两种思路完全相反的控制器。DaemonSet 关心“持续在线”Job 关心“执行完成”。这一节先把 Job 的执行模型和关键参数讲透。4.1 三种执行模型非并行、固定完成数、工作队列Kubernetes 官方文档把 Job 分成三类我结合实战讲讲它们的区别。非并行 Job不设置completions默认情况下只有一个 Pod 完成退出码 0后整个 Job 就算完成。适合“执行一次就结束”的简单任务比如数据库备份、数据导出。固定完成数的并行 Job同时设置completions和parallelism。比如completions: 12parallelism: 4意思是总共需要 12 个 Pod 全部成功才算完成但最多同时运行 4 个。适合把一个大任务拆成多个独立小任务比如 12 个文件分片处理每条数据独立计算。工作队列型 Job不设置completions只设置parallelismPod 之间通过外部队列竞争任务。每个 Pod 从队列里取任务处理处理完一个再取下一个所有任务消费完Pod 成功退出后 Job 完成。适合任务数量不固定、每个任务耗时差异大的场景。这种模型下completions没意义因为任务数量不由控制器决定。我在实际项目里最常用的还是固定完成数这种因为它的语义最直观我要跑 N 个子任务每个子任务对应一个 Pod。4.2 关键参数的计算与选择逻辑Job 的参数设计直接影响任务的总耗时和失败恢复能力。parallelism决定同时跑多少个 Pod。这个值不能拍脑袋定要结合下游系统的承受能力。如果你处理的是外部 API接口限流是 5 QPS每个 Pod 处理一个请求那parallelism最多设 5设大了会把接口打爆。如果任务本身是 CPU 密集的本地计算可以结合集群可用资源来定。completions决定总成功次数。如果每个 Pod 只处理一个任务completions就等于任务数。如果每个 Pod 通过工作队列消费多个任务completions就不需要设置了。backoffLimit决定失败重试的总次数默认是 6。这个值不要设太大失败说明大概率有问题重试 6 次已经很多了。而且要注意每次重试之后控制器会按指数退避的间隔创建新 Pod不会立即连续重试。activeDeadlineSeconds是 Job 的“绝对死刑时间线”。如果整个 Job 运行超过这个时间仍未完成控制器会终止所有 Pod并把 Job 标记为失败。这个参数适合保护那些可能会死循环或者被外部依赖卡住的任务给一个最大运行时长防止任务无限消耗资源。4.3 为什么 restartPolicy 必须是 Never 或 OnFailureJob 的 Pod 模板里restartPolicy只允许写Never或OnFailure写Always会被 API 拒绝。这个限制很多人第一次遇到会不理解。原因是这样的Job 的完成语义建立在 Pod 会退出这件事上。如果一个 Pod 里的容器永远会被重启那它永远处于 Running 状态Job 永远看不到成功或失败completions这个目标就永远无法达成。Never和OnFailure的行为差异也要搞清楚。Never模式下容器一旦失败退出Pod 进入 Failed 状态控制器创建一个全新的 Pod 来重试。OnFailure模式下容器失败后 kubelet 会在同一个 Pod 内重启容器Pod 本身不进入 FailedJob 控制器在后台持续等待这个 Pod 成功。两种模式的选择取决于任务的可恢复性。如果容器启动时会建立临时目录、初始化状态失败后重启容器大概率能自己清理那OnFailure效率更高不用重建 Pod。如果容器失败后留下的临时文件会影响重启后的行为建议用Never让控制器创建干净的新 Pod。5. Job 与 CronJob 实战部署理论讲完进入实际操作环节。这一节我会带着你从最简 Job 开始逐步延伸到并行任务和定时任务。5.1 跑一个最简 Job理解运行态先创建一个最简单的测试 Job用来观察运行状态和终态apiVersion: batch/v1 kind: Job metadata: name: demo-job spec: template: spec: restartPolicy: Never containers: - name: runner image: busybox command: [sh, -c, echo start; sleep 3; echo done] backoffLimit: 3 activeDeadlineSeconds: 60部署后立即查看状态kubectl apply -f demo-job.yaml kubectl get job demo-job kubectl get pods -l job-namedemo-job一开始 Job 的COMPLETIONS是 0/1Pod 处于 Running几秒后 Pod 变成 CompletedJob 显示COMPLETIONS是 1/1状态为 Complete。这里有几个细节值得记住Job 创建的 Pod 会带上job-namedemo-job这个标签方便你用标签筛选。Pod 退出后不会自动删除日志还有保留价值方便排查。如果 Job 成功结束后你想清理现场执行kubectl delete job demo-job它会把关联的 Pod 一并删掉。5.2 并行任务编排示例假设你有一个批处理场景需要把 12 个独立文件做哈希计算集群最多允许 4 个 Pod 同时运行。配置如下apiVersion: batch/v1 kind: Job metadata: name: batch-hash spec: parallelism: 4 completions: 12 backoffLimit: 2 activeDeadlineSeconds: 300 template: spec: restartPolicy: Never containers: - name: worker image: busybox command: [sh, -c, echo started; sleep 10; echo finished]部署后kubectl get job batch-hash会看到COMPLETIONS: 0/12然后随着一批批 Pod 完成这个数字逐步增长。这里要说一下并行度的实际计算感受。每个 Pod 跑 10 秒parallelism是 412 个任务需要分 3 批理想耗时约 30 秒。如果parallelism改成 2就需要 6 批耗时翻倍到约 60 秒。所以并行度不是越高越好它受两个约束一是业务能否并行二是下游系统能不能抗住。在很多真实项目里任务失败重跑导致的副作用比你想的严重。比如数据迁移 Job第一次跑的时候有一半任务成功写入数据另一半失败Job 重试后失败的 Pod 会重新跑但成功的那批任务已经写过了。如果你的业务逻辑没有做幂等处理或者断点续传就会出现重复数据。5.3 CronJob 定时任务配置与三个隐蔽坑CronJob 本质是“定时创建 Job”的控制器同一个 YAML 的kind改成CronJob里面嵌套一个jobTemplate就行apiVersion: batch/v1 kind: CronJob metadata: name:>