7.2 K8s 对象模型 (Pod / ReplicaSet / Deployment / StatefulSet / DaemonSet / Job)

一句话

对象模型不是 “一堆 YAML”,而是五件套:spec(你要什么)、status(现在是什么)、控制器(谁负责拉平)、ownerReferences(谁能级联删)、标签选择器(谁跟谁一组);看懂 Deployment → ReplicaSet → Pod 的三层套娃,就解释了 90% 的 “我改了怎么不生效”。

五种工作负载:差别在 “身份” 与 “终止语义”

对象 管什么 能不能有稳定身份 谁创建它下面的 Pod
Pod 一个或多个容器的运行单元 名字随机,重建即换 你自己(或用下面的对象)
Deployment 无状态副本 + 版本滚动 不能,Pod 可互换 Deployment → ReplicaSet → Pod
StatefulSet 有状态副本 + 固定身份与顺序 能(web-0、web-1 与独立 PVC) StatefulSet → Pod(逐个创建)
DaemonSet 每个节点一个副本 跟节点走,节点加/减自动增删 DaemonSet → Pod
Job / CronJob 跑到成功为止 / 按计划跑 无(可重试,completions 计数) Job → Pod

三句判据:

  • 要不要稳定名字与独立存储?要 → StatefulSet。
  • 要不要 “每个节点都有一份”(日志、网络、监控)?要 → DaemonSet。
  • 跑完就该退出?是 → Job;周期性 → CronJob。

套娃关系与 “改了不生效”

Deployment (声明副本数 3、镜像 A)
└── ReplicaSet (A-7d9f)   ← 每次换模板就新建一个 RS
    ├── Pod A-7d9f-1
    ├── Pod A-7d9f-2
    └── Pod A-7d9f-3
现象 真实原因
改了 Deployment 的 selector,报 field is immutable 选择器决定 “这个 Deployment 拥有哪些 ReplicaSet”,改了会切断所有权,API server 直接拒绝
kubectl set image 之后旧 Pod 还在 旧 ReplicaSet 被缩到 0 但保留(revisionHistoryLimit),用于回滚
手工 kubectl edit pod 改了镜像,一会儿又变回来 Pod 由 RS 管理,模板一收敛就覆盖;要改模板
删了 Deployment,Pod 还在 级联删除策略(--cascade=orphan)或 ownerReferences 被清掉
副本数是 3,实际跑了 5 个 有另一个控制器(HPA/RS/StatefulSet)也在写 spec.replicas

五个字段决定了 “谁拥有谁”

字段 作用 常见坑
metadata.ownerReferences 父对象删除时级联删除子对象 手写的对象没这个引用,删除父对象会留下垃圾
metadata.labels 被选择器匹配、被策略匹配 用作业务分组,却被 NetworkPolicy/Service 意外命中
spec.selector 控制器认领 Pod 的依据 手写 Pod 的 labels 与控制器的 selector 重叠 → 被 “抢走” 再删掉
metadata.finalizers 删除前的拦截器(清理外部资源) 控制器挂了,对象永远 Terminating
status.conditions 结构化的 “现在是什么状态” 只看 Ready 不看 observedGeneration,读到过期状态

四个 “号” 容易混:

字段 含义
metadata.resourceVersion 乐观并发的令牌,每次写入自增
metadata.generation spec 的版本号(模板改动才 +1)
status.observedGeneration 控制器已经处理到的 spec 版本;小于 generation 表示还在收敛
metadata.managedFields 哪个字段由谁(kubectl / 控制器)管理,SSA 的账本

排 “改了不生效” 的第一步:比 generation 与 observedGeneration。相等说明控制器已处理,问题在别处;不等说明控制器还没跟上(或卡住)。

kubectl get deploy app -o jsonpath='{.metadata.generation} {.status.observedGeneration}{"\n"}'
# 3 2   → 控制器还在处理第 2 代,不要急着怀疑“没生效”

反模式

反模式 后果
手写 Pod(不用工作负载对象) 节点故障后不会重建,“自愈” 只是传说
用 kubectl edit 改线上 Pod 与控制器打架,改动随时被覆盖且不可追溯
Deployment 的 selector 写成 “过宽” 的标签 认领了别人的 Pod,滚动更新时误删
把有状态服务塞进 Deployment 重启丢身份、PVC 挂错、顺序不可控
忽略 observedGeneration 把 “还在收敛” 误判成 “配置错”
一个对象塞 40 个 container 调度与探针粒度失真,一个挂了全换

三条铁律

🔑 只改模板,不改实例:实例(Pod)是控制器的产物,改它没用。

🔑 选对象看两件事:要不要稳定身份(StatefulSet)、要不要每节点一份(DaemonSet)。

🔑 generation ≠ observedGeneration 就是 “还没生效”,先等收敛,再查配置。

自检

Q1 手工创建一个 Pod,打上与某 Deployment 相同的标签,会发生什么?

▸ 答案:会被 Deployment 的 ReplicaSet 认领,然后被杀掉。RS 按 selector 统计 “我有几个 Pod”,多出来的(未被它创建但匹配标签的)会被删除以收敛到期望副本数。这也解释了一个常见事故:手写的调试 Pod 用了通用标签 app=web,几分钟后消失。解法是加一个不匹配的标签,或用 kubectl run 的独立标签。

Q2 为什么 Deployment 的 spec.selector 不可变,而 spec.template 可变?

▸ 答案:因为 selector 定义了所有权边界:控制器用它认领 Pod 与 ReplicaSet。改变它等于 “把已有的子对象全部抛弃并认领一批新的”,语义上不是更新而是重建,容易造成孤儿对象与业务中断。所以 API server 直接禁止;要换 selector,只能新建 Deployment 再迁移。

Q3 StatefulSet 的 web-0 被删了,重建后为什么还能挂回原来的数据?

▸ 答案:因为 StatefulSet 用 volumeClaimTemplates 为每个序号生成固定名字的 PVC(data-web-0),PVC 独立于 Pod 生命周期。新 Pod 复用同名 PVC 就接上了原数据。反过来说:删 StatefulSet 默认不会删 PVC,这是保护数据的设计,也常导致 “以为清理干净了,其实磁盘还在计费”。

Q4 kubectl get pods 显示 3 个 Running,但业务报 502。对象模型层面最该先看什么?

▸ 答案:看 status.conditions 的 Ready 与 Endpoints 是否为空。Running 只说明进程在,不代表就绪;没有 readiness 通过就不会进 Endpoints,Service 转发不到它。其次是 observedGeneration(是否收敛完)与容器 restartCount(是否在反复重启)。这就是 “看对象不看日志” 的典型盲区。

Q5 为什么 “把 5 个服务放进一个 Pod” 通常是错的?

▸ 答案:因为 Pod 是调度与生命周期的最小单位:同一个 Pod 内的容器总是一起调度、一起重建、共享网络与卷。放 5 个服务意味着:一个崩溃/升级会带走全部、无法独立扩缩容、端口冲突只能靠 localhost 协调。正确的组合原则是 “是否必须同生共死”(如 sidecar 代理),而不是 “部署方便”。

跨篇相关:控制器的调谐循环与水平触发见 7.1 声明式 API 与控制器模式;滚动更新与回滚由 Deployment/RS 驱动,见 7.6 自愈、滚动更新与回滚;Service 如何挑 Pod 见 7.3 Service 与 Ingress;存储身份与 PVC 见 7.7 存储卷与持久化;资源与 QoS 见 7.9 资源限制与 QoS;编排方式的取舍见 7.12 何时不该用 K8s。