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。