先泼一盆冷水:我见过太多人把 Deployment 当万能膏药,任何服务上来就是一个 deployment.yaml,遇到有状态应用再急急忙忙去查 StatefulSet 的文档,结果越补越乱。反而是在打游戏的时候想明白了一件事——Kubernetes 的工作负载,本质上和英雄联盟里的角色定位一模一样。
这篇文章就是来帮你把这层窗户纸捅破的。我会用 LOL 的对线逻辑把 K8s 里最常见的四种工作负载(Deployment、StatefulSet、DaemonSet、Job/CronJob)拆开揉碎,不光是讲概念,还会把每个控制器背后的设计原理、适用场景、生产环境里的坑都过一遍。不管你是刚接触 K8s 的新手,还是已经在集群里摸爬滚打过的老手,换个视角看这些熟悉的 API 对象,往往能帮你跳出背文档的思维定式,真正理解控制器模式为什么这么设计。
1. 从召唤师峡谷到集群控制面:先搞清楚谁在对线谁
如果你第一次打开 K8s 的架构图,大概率会觉得这群组件各有各的脾气,完全不知道从哪儿开始看。但把它当成一场 LOL 对局就好理解了:你是召唤师(开发者/运维),控制面是裁判席(etcd、API Server、Controller Manager),节点是三条路和野区,Pod 是站在线上的英雄单位。
在 LOL 里,英雄不是凭空出现的,得有召唤师操作、有泉水的刷新机制、有装备商店提供补给。K8s 也一样,Pod 不会自己好端端冒出来,它必须由一个控制器创建和管理。这个控制器就是所谓的“工作负载控制器”,它们负责维持集群的期望状态——就像排位赛里每个位置的英雄各司其职,谁也不能越位,谁也不能挂机。
工作负载这个概念,可以理解为“你向集群声明的运行目标”。你告诉 K8s:我这里需要 5 个 Nginx 副本,或者我需要一个唯一 ID 的数据库实例,或者我希望每个节点都跑一个日志采集器。剩下的事情,控制器负责盯着,不符合预期就自动调整回期望状态。
这些控制器的共同出发点,就是 K8s 那套声明式 API 的设计哲学:你只需要描述“最终应该长什么样”,而不是“具体每一步怎么做”。这就像你在游戏里给队友发信号“打龙”,队友自己知道怎么站位、怎么拉仇恨、什么时候交惩戒,你不需要手把手教他每一步。理解了这条主线,再看后面四种工作负载,就都是在回答同一个问题:谁来负责让这些 Pod 存活、更新、扩展、退役。
2. 被误解的“最小单位”:Pod 才是站上线的英雄
很多入门教程一上来就讲 Pod 是什么,但很少解释清楚为什么 Pod 会成为 K8s 的最小调度单位。这个问题我从 LOL 的角度想了很久,后来发现一个特别贴切的类比:Pod 就像一个英雄身上的装备栏和技能组,它们必须同时出现、同时使用、共享同一个生命周期。
2.1 为什么一个英雄需要多件装备
英雄联盟里你不可能只出一件装备就去打团,你需要攻击装、防御装、鞋子、饰品,它们各自负责一种能力。Pod 里面的多个容器也是这个逻辑。一个 Pod 可以包含一个主容器(比如Web服务)和若干个辅助容器(比如日志收集、配置加载、监控上报),这些容器共享同一个网络命名空间和存储卷。
这意味着它们通过 localhost 就能互相通信,不需要走 Service 的 DNS 转发,延迟更低,也更安全。同时它们共享生命周期的命运:一个容器崩溃了,整只 Pod 会被重建,里面的兄弟容器也要跟着重来。这就是“英雄单位”的绑定关系。
在实际业务里,最常见的多容器 Pod 就是 sidecar 模式。比如你的主容器是一个 Java 应用,旁边挂个 Filebeat 容器负责把日志输出到 Kafka,Java 应用本身不用关心日志怎么传输。这就像 ADC 只管打输出,辅助负责视野和保命,两个英雄各司其职,但共享同一条线。
2.2 Pod 的生命周期:从泉水走向战场的全过程
Pod 的状态机其实非常像英雄在泉水里的状态变化。你点了一个英雄,它先出现在泉水的队列里(Pending),然后你购买了装备、加载了技能(ContainerCreating),随后英雄走到线上开始对线(Running),如果你被击杀(Failed),泉水就会重新刷新一个(如果由控制器管理)。
这里面有一个容易踩坑的点:Pod 本身是“短命”的。如果 Deployment 管理的 Pod 挂掉,控制器会创建一个全新的 Pod,它会有新的名字、新的 IP,所有状态都会丢失。这就是为什么无状态应用用 Deployment 很舒服,但数据库这种带状态的东西不能 사용 Deployment 一梭子带走——连 MySQL 重启一次都可能出问题,更别指望 Pod 重建之后还能记得自己是谁。
所以在排查问题的时候,你别对着一个 Pod 的名字使劲儿看,它只是个马甲,真正需要关心的是背后的控制器在做什么。这也是我后来习惯性先kubectl get deploy再看kubectl get pod的原因,顺序反了,排查思路很容易被带偏。
3. Deployment:ADC 被打爆了,泉水里马上刷新一个
Deployment 是 K8s 里最常用的工作负载,它的定位就是“无状态服务的副本管理器”。为什么说它是 ADC?因为 ADC 死了并不可怕,换一个装备重新上线就行,没有谁非要记住上一把 ADC 的 ID 或者专属配置。
3.1 从 ReplicaSet 说起:五个人的团队不能少人
Deployment 本质上并不直接管理 Pod,它管理的是 ReplicaSet。ReplicaSet 负责控制“当前有几个 Pod 活着”这件事。这和排位赛里“五个人在线”是一个道理,你觉得中路得有一个法师坐镇,那这个位置就不能空着。有人挂机了,系统得自动补位,哪怕补进来的是个水平不一的替补(新 Pod),也好过四打五。
我来给你看一个最基础的 Deployment YAML,你自己跑一遍就明白了:
apiVersion: apps/v1 kind: Deployment metadata: name: lol-ADC labels: app: adc spec: replicas: 3 selector: matchLabels: app: adc template: metadata: labels: app: adc spec: containers: - name: web image: nginx:1.25 ports: - containerPort: 80 livenessProbe: httpGet: path: /health port: 80 initialDelaySeconds: 3 periodSeconds: 5 readinessProbe: httpGet: path: /ready port: 80 initialDelaySeconds: 3 periodSeconds: 5注意看spec.replicas这个字段,它就是“五个人在线”的人数声明。ReplicaSet 会持续对比当前 Pod 数和期望 Pod 数,发现少了就创建,发现多了就杀掉。这个循环往复的调谐逻辑,就是 K8s 控制器的核心思想。
3.2 滚动更新:你说的算,但别一次团灭
Deployment 最爽的功能是滚动更新。以前发布一个新版本,大家习惯先停服再部署,这在 K8s 里是反面教材。Deployment 默认会执行 RollingUpdate 策略,先启动一个新版本 Pod,等它通过就绪检查之后再杀掉一个旧版本 Pod,逐个替换。
这个过程就像是英雄回城买装备——你不可能先把身上的六件装备全部卖掉,再去商店买新的,万一对面在这时候开团你就彻底废了。合理的做法是一件一件替换,新装备入手且确认能用(readinessProbe 通过)之后,再卖掉旧的。
Deployment 更新的时候,你可以用kubectl set image deployment/lol-ADC web=nginx:1.26,它会自动创建一个新的 ReplicaSet,然后按照 maxSurge 和 maxUnavailable 两个参数控制更新节奏。默认是 25% 的额外 Pod 数和 25% 的最大不可用数。生产环境建议把这两个值调保守一点,别让更新动作本身成为故障源。
3.3 健康检查:挂机队友必须被踢出对局
这就涉及到 livenessProbe 和 readinessProbe 了。很多新手只知道 Pod 挂了会重启,但不知道是怎么判断“挂了”的。
- livenessProbe(存活探针):相当于检测英雄是否活着。如果探针连续失败,K8s 会杀掉这个 Pod 并用新 Pod 替换。这比 LOL 的挂机检测还要严格,它还能检测到“英雄活着但完全没作用”的情况。
- readinessProbe(就绪探针):相当于判断英雄能不能参加团战。就绪探针失败时 Pod 不会被杀,而是从 Service 的 Endpoints 里摘掉,流量不会再打到它身上,但它还在线上继续跑,等恢复后再重新加入负载池。
这两个探针一定要配合着用。我有一个朋友只配了 livenessProbe,结果服务启动很慢,每次发布都要经历几次被杀重来的“诈尸”,实际上只是启动时间超过了存活探针的容忍时间,接口还没就绪就被当成死了。解决方案也不复杂,把initialDelaySeconds调大,或者用 startupProbe 兜底,给慢启动应用一个缓冲期。
4. StatefulSet:那个必须记住自己ID的选手,才能打赢BO5
如果说 Deployment 是 ADC 随时能换,那 StatefulSet 就是在打 BO5 的核心选手,队伍不能随便换人,他必须拥有稳定的身份、专属的外设配置,并且队友们要按照固定顺序登场。
4.1 稳定的网络标识:选手身上印着编号
StatefulSet 创建的 Pod 会带有一个稳定的、可预测的名字,格式是名字-序号。比如你定义了一个名为mysql-rs的 StatefulSet,副本数是3,那么创建的 Pod 就是 mysql-rs-0、mysql-rs-1、mysql-rs-2。
这一点和 Deployment 完全不一样。Deployment 的 Pod 名字是随机后缀,每次重建都换一个;StatefulSet 的 Pod 即便被删掉重建,名字还是会恢复到原来的序号。这是因为它要保证每个实例拥有稳定的网络身份,尤其在数据库集群里,每个节点都有自己的主机名,主从关系靠主机名来维护,换了名字一切就崩了。
生产环境里我们经常用 Headless Service(无头服务)配合 StatefulSet,让每个 Pod 拥有独立的 DNS 记录。类似于每个选手有自己专属的队服编号,不管他在哪里换线,队友喊的都是同一个 ID,不会喊错人。
4.2 稳定的存储:外设和手感不能变
除了网络标识,StatefulSet 还会和 PersistentVolumeClaim 绑定。每个副本的 Pod 会对应独立的 PVC,Pod 重建之后存储卷还是一样挂载回来。这点特别好理解,就跟你用顺手的鼠标键盘一样,换了一个全新的外设,操作手感会差很多。
这意味着数据库实例的状态是跨 Pod 生命周期保存的,你杀掉 Pod,重启后它还认得自己之前写入的数据。StatefulSet 在存储编排上有一个很反直觉的设计:如果你删掉一个 Pod,与其绑定的 PVC 默认不会被删除,因为 K8s 默认启用persistentVolumeClaimRetentionPolicy的行为是有讲究的。这个字段在不同版本里表现还不一样,涉及 StatefulSet 缩容时尤其要小心。
4.3 顺序启停:团战必须按站位进场
StatefulSet 还要求严格的有序部署和有序缩容。创建的时候按 0、1、2 的顺序依次启动,前一个 Pod 处于 Running 和 Ready 状态后,才会创建下一个。缩容的时候则相反,从最大的序号开始杀,依次往下删。
这种设计显然不是为了好玩,而是有状态服务经常需要“大哥先上,小弟才能跟上”。以 Kafka 为例,如果序号大的 broker 先启动,它可能连不上序号小的 broker 导致集群脑裂;反过来从 0 开始,让 controller 节点先起来,后面的节点加入时才有主心骨可找。
所以 StatefulSet 特别适合那些自带集群、需要成员发现的有状态应用,比如 ZooKeeper、Etcd、ClickHouse,或者你需要固定主机名的中间件。
5. DaemonSet:地图上每个点位都有的防御塔和视野守卫
DaemonSet 的定位非常专一:确保集群中的每一个 K8s 节点(Node)上都有一个 Pod。这就像召唤师峡谷里的防御塔,不管你走哪一路,该路的防御塔一定在那里。它不需要你关心副本数量,因为副本数量是由节点数量自动决定的。
5.1 设计哲学:一个萝卜一个坑
你不需要在 DaemonSet 里写 replicas,它天然认为有多少节点就起多少个 Pod。节点加入集群,它自动补一个 Pod;节点被移出集群,对应的 Pod 也就被清掉。所以 DaemonSet 的理想使用场景很明确:分布式系统里那些“每个机器都得有”的基础组件。
我最常用的三个场景:
- 日志收集:每个节点跑一个 fluentd / Filebeat,把容器日志统一采集到 Elasticsearch 或者 Kafka,节点新增后日志采集能力自动跟上。
- 监控指标采集:Prometheus Node Exporter,每个节点一个 exporter,抓取 Node 级别的 CPU、内存、磁盘指标。
- CNI 网络插件:比如 Calico、Flannel 的 agent 组件,通常都以 DaemonSet 方式部署,确保每个节点都具备网络转发能力。网络插件挂了,节点上的所有 Pod 都会处于异常状态,这个问题后文会讲。
5.2 典型玩法:每个节点上的守护者
部署一个 DaemonSet 异常简单,因为它的 API 结构和 Deployment 基本一致,只是把 kind 改了,也不用写 replicas。我给你看一个采集 CPU 指标的典型配置:
apiVersion: apps/v1 kind: DaemonSet metadata: name: node-exporter namespace: monitoring spec: selector: matchLabels: app: node-exporter template: metadata: labels: app: node-exporter spec: hostNetwork: true tolerations: - key: node-role.kubernetes.io/control-plane effect: NoSchedule containers: - name: node-exporter image: prom/node-exporter:v1.5.0 args: - --path.procfs=/host/proc - --path.sysfs=/host/sys volumeMounts: - name: proc mountPath: /host/proc readOnly: true - name: sys mountPath: /host/sys readOnly: true volumes: - name: proc hostPath: path: /proc - name: sys hostPath: path: /sys注意hostNetwork: true这个字段,它让 Pod 直接使用宿主机的网络栈。监控组件这样设置很正常,它需要看到宿主机上的真实网络设备状态,而不是容器网络给它包装出来的假象。
5.3 误删 DaemonSet 的惨痛教训
我必须提一个生产环境的高频故障。有些同学在清理无用资源时,看名字差不多就把某个 CNI 插件当成“不重要的东西”删了,比如把 calico-node 的 DaemonSet 给删了。结果就是集群里所有节点的网络瞬间紊乱,新 Pod 调度不起来,跨节点的 Pod 通信全部失败,整个集群实际上已经处于“慢性死亡”状态。
这种故障和 LOL 里防御塔被推掉是一样的——你丢的不是一座塔,而是整片视野和推进节奏。排查的时候你会发现 Pod 一个接一个 CrashLoopBackOff,但找不到具体原因,最后才意识到是基础网络组件被误删了。
所以我的建议是:生产集群里 DaemonSet 的资源名一定要有明确的前缀和命名空间隔离,权限控制也要到位。涉及 CNI、kube-proxy 这些核心组件的 DaemonSet,不要给普通开发者删除权限。
6. Job 和 CronJob:练级任务和定点的野怪刷新
如果 Deployment、StatefulSet、DaemonSet 都是为了“服务长期在线”而生的,那 Job 和 CronJob 就是另一个极端——它们是为了“跑完就跑”而生的。
6.1 Job:打完这局排位就下班
Job 负责运行一次性任务。你会告诉它“去把这段 SQL 跑完”或者“把这批图片压缩完”,它启动 Pod 去执行,执行成功之后 Pod 进入 Completed 状态,任务结束。如果执行失败,Job 会根据配置重试,直到成功或者达到重试上限。
这和 LOL 里打完一把排位一模一样:你开局匹配、选人、进游戏、打完结算,整套流程到结算就结束,不会一直保持在线。Job 的重点在于它默认用 restartPolicy: Never 或者 OnFailure,让 Pod 在任务完成后不会像 Deployment 那样被重新拉起,保持着 Completed 的终态。
来看一个典型的批处理任务定义:
apiVersion: batch/v1 kind: Job metadata: name: batch-mission-01 spec: completions: 3 parallelism: 2 template: spec: restartPolicy: OnFailure containers: - name: mission image: busybox command: - sh - -c - echo "开始执行数据处理任务"; sleep 30; echo "任务完成"completions: 3表示总共需要成功完成 3 次,parallelism: 2表示同时允许两个 Pod 并行跑。这是很经典的扇出(Fan-out)模式,把一批任务拆成多个 Pod 并发执行,执行完成的数量达到 3 就整体结束。
6.2 CronJob:每天定点刷新的大龙与野怪
CronJob 就是“定时版的 Job”。它按照 cron 表达式在固定时间点创建 Job,然后由 Job 去实际运行任务。这个逻辑相当于召唤师峡谷里定时刷新的野怪、小龙和大龙——时间一到,它们就出现了,不管你有没有在打。
它的 YAML 是在 Job 的基础上多了一层schedule字段:
apiVersion: batch/v1 kind: CronJob metadata: name: daily-clear spec: schedule: "0 3 * * *" # 每天凌晨3点执行 concurrencyPolicy: Forbid startingDeadlineSeconds: 100 successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 1 jobTemplate: spec: template: spec: restartPolicy: OnFailure containers: - name: cleanup image: busybox command: - sh - -c - echo "开始清理过期数据"; ...定时任务最常见的用途是数据清理、报表生成、批量邮件发送、数据库备份。这里的concurrencyPolicy: Forbid很关键,它表示同一时间只允许一个任务实例运行。你总不希望上一个备份任务还没跑完,下一个备份任务又启动了,两个任务同时写同一个备份文件,那就是灾难了。
6.3 任务失败的重试与超时控制
Job 和 CronJob 管理最容易忽略的是超时时间。如果一个任务里的小 bug 导致 Pod 一直挂起,它不会自动结束,除非你设置了activeDeadlineSeconds。我碰到过 CronJob 里一个 SQL 查询因为死锁一直没返回,CronJob 里的 Pod 挂了两天都没被发现,任务队列越积越长,最后拖垮了数据库。
所以给出两个实际建议:
- 每个 Job 模板里都写上
activeDeadlineSeconds,强制任务最久能跑多久。 - CronJob 的
historyLimit也要控制好,不然每次执行都会留下一堆 Completed 的 Pod,攒多了 inode 会被占满。
7. 工作负载选型:一把钥匙开一把锁
看了上面四类工作负载,你可能已经感觉到它们各自解决一类特定的问题。但真到了选型的时候,还是有人会犹豫。我给你一套直接从游戏理解映射到技术判断的速查思路,顺便回答一下面试题里常问的“Deployment 和 StatefulSet 到底什么区别”。
7.1 四类工作负载速查表
面试之前或者方案评审之前,你直接看这张表就够了。
| 工作负载 | LOL 类比 | 核心特征 | 典型应用 | 生产环境高风险点 |
|---|---|---|---|---|
| Deployment | ADC/法师,换了就换了 | 无状态、随机 Pod 名、可滚动更新、共享无持久化存储 | Web 服务、API 网关、微服务 | 探针配置不当导致滚动更新雪崩 |
| StatefulSet | 打 BO5 的核心选手,ID 和外设不能换 | 稳定 Pod 名、稳定存储、顺序启停、Headless Service 支持 | 数据库、消息队列、协调服务 | 缩容时 PVC 残留、集群成员发现断裂 |
| DaemonSet | 防御塔和视野守卫,每路必有 | 每个节点固定一个 Pod、节点增减自动跟随 | CNI、日志采集、监控 agent | 误删导致全网网络故障 |
| Job/CronJob | 一次性排位赛、定时刷新的野怪 | 执行完即结束、支持并发数和重试策略 | 数据批处理、定时备份、跑报表 | 超时缺失导致任务僵尸化 |
7.2 从游戏理解回到控制器模式
如果你去面 K8s,面试官大概率会问“Kubernetes 的控制器模式是什么”。你可以用很通俗的话解释:控制器是一个不断比较“期望状态”和“实际状态”的循环。期望状态来自 YAML 声明,实际状态来自 API Server 里实时存储的对象数据,二者有差异就执行操作去收敛差异。
这个“对线”的过程非常像你在 LOL 里看小地图:你知道自己应该补多少刀(期望状态),看了一眼自己的补刀数(实际状态),发现落后了(差异),那就赶紧操作多补几刀(调谐)。Deployment、StatefulSet、DaemonSet、Job 都是这个模式的具体实例化,只是因为要管理的对象性质不同,细节策略才不同。
7.3 生产环境最容易踩的三个坑
最后,我把这几年在生产环境里见过的高频故障集中列一下,你排查问题时可以直接对号入座。
第一个坑:滚动更新时探针误判导致流量雪崩。readinessProbe 的路径写错了,或者依赖的数据库还没就绪,新 Pod 永远无法 Ready,老的 Pod 又在逐步缩容,最终服务和流量全无。解决办法是先用kubectl rollout status观察,卡住时立刻kubectl rollout undo回滚。
第二个坑:StatefulSet 缩容后 PVC 未清理。很多人以为删掉 StatefulSet 或缩容副本数,数据卷会自动删除。实际上 PVC 默认还在,如果下一次重新创建同名 StatefulSet,旧 PVC 会和新 Pod 绑定,里面可能有旧数据也有脏数据,更糟糕的是同名 PVC 资源卡住,新 Pod 一直 Pending。建议在缩容前先备份确认,再用kubectl delete pvc手动清理。
第三个坑:Job 并发参数设置不合理。很多新手没意识到同一 Job 的多个 Pod 可能跑在不同的 Node 上,如果它们同时操作同一个外部系统(比如共享数据库里的同一张表),会造成锁竞争和数据错乱。需要仔细评估业务是否真的允许并行执行,不允许的话把 parallelism 改成 1 就行。
我不太想给你灌什么大道理,但确实想说一句:K8s 的工作负载设计并不是一堆 API 对象的陌生排列,它只是把你熟悉的业务规则用云原生的方式重新表达了一遍。下次配 Deployment 的时候,多想一下你的服务是 ADC 还是需要固定 ID 的选手;下次看 DaemonSet 的时候,想想防御塔的职责边界在哪。当你习惯了从“角色定位”去理解基础设施,很多排障和选型问题都会变得顺理成章。
如果你能从这篇文章里带走一样东西,我希望是:先想清楚服务有没有“记忆”,再决定用哪个工作负载。无状态优先 Deployment,有状态选 StatefulSet,全节点覆盖用 DaemonSet,跑完就跑用 Job,定期的就包一层 CronJob。选型正确了,后面的运维日子会轻松非常多。