阶段一 · Kubernetes 核心

Deployment / StatefulSet / DaemonSet / Job / CronJob

前置回顾

第 01 篇:Controller Manager 的核心逻辑是观察 → 比对 → 修正.
第 02 篇:Pod 是 K8s 的最小调度单元
但手动管理 Pod 的生命周期是不现实的--Pod 挂了没人重建,更新要手动删建.本篇的五种控制器就是对不同"期望状态模式"的自动化实现.

一个 Pod 还是管理一组 Pod?

直接在 K8s 里创建一个裸 Pod--可行.但它挂了不会自动重建,更新镜像要手动删了再建,跨节点调度也没有自动修复.

# 裸 Pod:K8s 不会替你修复任何事
$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:1.25
EOF
# 这个 Pod 挂了就挂了,没人重建它.
# 想更新镜像?先删掉再建一个新的.
# 这不是"编排"--这是手动运维.

工作负载控制器解决的就是这件事:你声明一组 Pod 应该长什么样,控制器持续保证它实际长成那样.
但"应该长什么样"这个问题,不同场景答案完全不同:

Web 服务:         "始终 3 个相同的 Pod,更新时挨个替换,可一键回滚"
数据库: "3 个 Pod 各自有独立身份和数据,按顺序启动,按逆序停止"
日志采集 agent: "每个节点上一个 Pod,新节点加入立即给它也补一个"
数据迁移脚本: "跑完就结束,失败了就重试,最多试 5 次"
定时任务: "每天凌晨 3 点跑一次"

如果这些全用同一种控制器管理,你需要手工处理 Pod 命名,启动顺序,数据绑定--那 K8s 就没干什么事.
K8s 给出了五种控制器,各自针对一类场景,这个分类本身就是 K8s 把运维经验编码成 API 的结果.

五种控制器速查

控制器 适用场景 核心能力
Deployment 无状态 Web 服务(Nginx,API Server,微服务) 滚动更新,版本回滚,水平扩缩
StatefulSet 有状态服务(MySQL,Kafka,Redis Cluster,Elasticsearch) 稳定网络标识,有序扩缩,每 Pod 独立 PVC
DaemonSet 每节点一个守护进程(日志采集,监控 Agent,网络插件) 自动随节点同步,支持仅部署到部分节点
Job 一次性任务(数据迁移,批量处理,集成测试) 保证成功完成 N 次,失败自动重试
CronJob 定时任务(备份,报表,清理过期数据) Cron 表达式触发,自动管理 Job 留存

Deployment - 无状态服务的声明式更新

Deployment 是使用率最高的控制器.先看一个最基本的样子:

apiVersion: apps/v1        # Deployment 属于 apps 组,v1 版本
kind: Deployment
metadata:
name: nginx-deploy # 1. Deployment 自己的名字
spec:
replicas: 3 # 2. 期望几份副本?3 个一模一样的 Pod
selector: # 3. 哪些 Pod 归这个 Deployment 管?
matchLabels:
app: nginx # 标签匹配规则:app=nginx
template: # 4. Pod 模板(就是 Pod 的 metadata + spec)
metadata:
labels:
app: nginx # 必须和 3 的 selector 匹配,否则创建报错
spec: # ↓ 从这里开始和裸 Pod 的 spec 完全一样 ↓
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80

对比裸 Pod,Deployment 多了三个字段--它们正是"管理一组 Pod"所需要的:

新增字段 含义 裸 Pod 有吗?
spec.replicas 期望副本数.Deployment Controller 保证"始终有这么多 Pod 在跑"--挂了就补一个,多了就删一个 ❌ 裸 Pod 没有"副本"概念
spec.selector 标签选择器.Deployment 通过它找到"哪些 Pod 是我管的".如果集群里已经有 app: nginx 的 Pod,Deployment 也会把它们纳入管理 ❌ 裸 Pod 不需要找谁
spec.template Pod 模板.里面就是完整的 Pod metadata + spec.每次创建新 Pod,Deployment 都按这个模板生成 ❌ 裸 Pod 直接写 kind: Pod,不经过模板

template.metadata.labels 必须匹配 selector

这是新手最容易踩的坑.
template.metadata.labels(Pod 模板里写的标签)必须包含 selector.matchLabels 的所有键值对,否则 API Server 直接拒绝: "Pod 模板的标签和 selector 不匹配,这样创建的 Pod 你永远选不到".
两者一致是 Deployment 工作的前提.

提交后发生的事:

$ kubectl apply -f nginx-deploy.yaml
# Deployment Controller → 创建 ReplicaSet (rs-abc123, replicas=3)
# ReplicaSet Controller → 创建 3 个 Pod
$ kubectl get deploy,rs,pod
NAME READY UP-TO-DATE AVAILABLE
deployment.apps/nginx-deploy 3/3 3 3

NAME DESIRED CURRENT READY
replicaset.apps/nginx-deploy-7d4f8c9b6 3 3 3

NAME READY STATUS
pod/nginx-deploy-7d4f8c9b6-x2k9l 1/1 Running
pod/nginx-deploy-7d4f8c9b6-y7h3m 1/1 Running
pod/nginx-deploy-7d4f8c9b6-z8j4n 1/1 Running

Deployment → ReplicaSet → Pod

Deployment 不直接管 Pod: 它管 ReplicaSet,ReplicaSet 再管 Pod.第一篇里已经提过这个分层设计,现在从"更新"的角度再看一遍:

镜像从 nginx:1.25 改为 nginx:1.26 的完整过程:

时刻 0:稳定态
Deployment (replicas: 3)
└── RS-v1.25 (replicas: 3)
├── Pod-A (1.25)
├── Pod-B (1.25)
└── Pod-C (1.25)

时刻 1:Deployment Controller 发现 template 变了
→ 创建 RS-v1.26,replicas 暂为 0
→ 根据滚动更新策略,逐步调整两个 RS 的 replicas

时刻 2:滚动进行中
RS-v1.25 (replicas: 2) RS-v1.26 (replicas: 1)
├── Pod-A (1.25) ✓ └── Pod-D (1.26) ✓
└── Pod-B (1.25) ✓

→ Deployment Controller 只改 replicas 数字
→ ReplicaSet Controller 负责增删 Pod

时刻 3:滚动完成
RS-v1.25 (replicas: 0) RS-v1.26 (replicas: 3)
(保留,用于 rollback) ├── Pod-D (1.26)
├── Pod-E (1.26)
└── Pod-F (1.26)

旧 ReplicaSet 不会删: 它保留了上一版本的完整 Pod 模板,回滚的本质就是把 Deployment 重新指向旧 RS.

滚动更新:maxSurge 与 maxUnavailable

这两个参数控制滚动更新的速度与安全性,直接定义了每一步能建几个新 Pod,能删几个旧 Pod.

参数 含义 默认值
maxSurge 更新期间最多允许超出 replicas 多少个 Pod 25%(四舍五入向上取整)
maxUnavailable 更新期间最多允许多少个 Pod 不可用 25%(四舍五入向下取整)

两者不能同时为 0--否则没有空间建新 Pod 也不允许删旧 Pod,更新永远无法推进.

replicas: 3 为例,看三种典型配置:

配置 A:maxSurge=1, maxUnavailable=0  (安全优先,需要额外资源)
─────────────────────────────────────────────────────────
初始: [v1·A] [v1·B] [v1·C] 可用=3
步骤 1: [v1·A] [v1·B] [v1·C] [v2·D] 可用=3 ↑ 先建新(surge)
步骤 2: [v1·A] [v1·B] [v2·D] 可用=3 ↓ 新 Pod Ready 后删旧
步骤 3: [v1·A] [v1·B] [v2·E] [v2·D] 可用=3
步骤 4: [v1·A] [v2·E] [v2·D] 可用=3
...
→ 全程 ≥ 3 个 Pod 可用,但资源峰值 = 4 个 Pod
→ 适合:对可用性要求高的生产环境,有资源余量
配置 B:maxSurge=0, maxUnavailable=1  (资源受限,允许短暂降级)
─────────────────────────────────────────────────────────
初始: [v1·A] [v1·B] [v1·C] 可用=3
步骤 1: [v1·A] [v1·B] 可用=2 ↓ 先删旧(unavailable)
步骤 2: [v1·A] [v1·B] [v2·D] 可用=3 ↑ 建新替代
步骤 3: [v1·A] [v2·D] 可用=2 ↓
步骤 4: [v1·A] [v2·E] [v2·D] 可用=3 ↑
...
→ 资源峰值 = 3(不超 replicas),但可用 Pod 最低 = 2
→ 适合:资源紧张的环境,可以短暂容忍少一个 Pod
配置 C:maxSurge=1, maxUnavailable=1  (最快,可用性波动大)
─────────────────────────────────────────────────────────
初始: [v1·A] [v1·B] [v1·C] 可用=3
步骤 1: [v1·A] [v1·B] [v2·D] 可用=3 ↑ 建新
步骤 2: [v1·A] [v2·E] [v2·D] 可用=3 ↓ 删旧 ↑ 建新
...
→ 资源峰值 = 4,可用 Pod 最低 = 2,但速度最快
→ 适合:大批量 Pod 的快速更新(如 100 个副本想尽快完成)
# 显式配置
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0

百分比还是绝对值?

两个参数都支持百分比和绝对值.

  • 百分比按 replicas 计算:maxSurge: "25%",replicas=10 时允许超出 3 个(10×25%=2.5,四舍五入向上=3)
  • 绝对值写整数:maxSurge: 2.
  • replicas 较小时(3~5),绝对值更直观;大副本数时百分比更合理.

回滚机制

每次更新(template 变更)产生一个新的 revision.旧 revision 的 ReplicaSet 保留不删,回滚只是把 Deployment 重新指向它.

# 查看版本历史
$ kubectl rollout history deployment/nginx-deploy
REVISION CHANGE-CAUSE
1 kubectl apply --filename=nginx-deploy.yaml
2 kubectl set image deployment/nginx-deploy nginx=nginx:1.26

# 回滚到上一版本
$ kubectl rollout undo deployment/nginx-deploy

# 回滚到指定版本
$ kubectl rollout undo deployment/nginx-deploy --to-revision=1

# 暂停/恢复(连续改几个配置,一次性触发)
$ kubectl rollout pause deployment/nginx-deploy
$ kubectl set resources deployment/nginx-deploy -c nginx --limits=cpu=500m
$ kubectl set env deployment/nginx-deploy LOG_LEVEL=debug
$ kubectl rollout resume deployment/nginx-deploy
# → 所有修改合并为一次 revision,只触发一次滚动更新

Deployment 默认保留 10 个 revision(由 revisionHistoryLimit 控制).超出的旧 ReplicaSet 会被清理.

其他更新策略

策略 行为 适用场景
RollingUpdate(默认) 逐批替换,保证可用副本数 绝大多数场景
Recreate 先全部删光,再全部新建 不支持多版本共存(如旧版连新版数据库会出错);或资源极紧无法容纳 surge
spec:
strategy:
type: Recreate # 全删再全建,中间服务中断

StatefulSet - 有状态服务的管家

StatefulSet 的每个 Pod 有自己的身份数据,不能随便替换.

StatefulSet 额外给出四个保证:

保证 Deployment StatefulSet
Pod 名称 随机后缀(nginx-deploy-7d4f-abc123) 固定序号(redis-0, redis-1, redis-2)
网络标识 无稳定 DNS redis-0.redis-svc.default.svc.cluster.local(Pod 名 + Headless Service)
存储 所有 Pod 共享同一个 PVC(或根本无持久化) 每 Pod 独立 PVC(通过 volumeClaimTemplates 自动创建)
启停顺序 并行,无顺序保证 启动 0→1→2,停止 2→1→0

下面讲解与这几个特性相关的概念:

Headless Service 是什么

在第 01 篇的 kube-proxy 中提过:普通 Service 分配一个虚拟 ClusterIP,kube-proxy 在 iptables 里写规则做负载均衡.DNS 查到的是 ClusterIP,流量经过 iptables 随机选一个后端 Pod.调用方无法指定"我要连某个具体的 Pod".

Headless Service 就是把这一层关掉--clusterIP: None,不分配虚拟 IP,也不做负载均衡:

# 普通 Service
kind: Service
spec:
clusterIP: 10.96.0.1 分配一个虚拟 IP
DNS 返回 10.96.0.1 iptables 负载均衡 随机后端 Pod

# Headless Service
kind: Service
spec:
clusterIP: None 不分配虚拟 IP
DNS 直接返回所有后端 Pod IP 列表 调用方自己选连哪个
# DNS 解析的差异
$ nslookup nginx-svc # 普通 Service:返回单个 ClusterIP
10.96.0.1

$ nslookup nginx-svc # Headless Service:返回所有 Pod IP
10.244.1.5
10.244.2.3
10.244.3.7

# 更进一步:Headless Service + StatefulSet 可以有逐 Pod 的 DNS
$ nslookup redis-0.redis-svc # 只解析 redis-0 这个 Pod 的 IP
10.244.1.9

# 这就是"稳定网络标识"的实现机制

为什么 Headless Service 能支持逐 Pod 解析而普通 Service 不行?因为 StatefulSet 的 Pod 名称是固定的(redis-0),CoreDNS 自动为 {pod-name}.{service-name} 格式生成 A 记录.Deployment 的 Pod 名称是随机 hash,不存在 nginx-0 这种名字,自然没法生成对应的 DNS.

  • 普通 Service = 公司总机(400 号码): 拨过去自动转接到某个员工,你不知道也无需知道接电话的是谁
  • Headless Service = 公司通讯录: 直接列出每个员工的分机号,你决定打给谁.StatefulSet 的 Pod 就是那些"有固定工位和分机号"的员工.

稳定网络标识

稳定网络标识依赖两个东西配合:StatefulSet 本身的命名规则 + Headless Service.

# StatefulSet 的 Pod 名称是固定的序数索引
StatefulSet: redis-cluster
├── redis-cluster-0 (始终是这个名字,重建后还是)
├── redis-cluster-1
└── redis-cluster-2

# Deployment 的 Pod 名称是随机 hash
Deployment: nginx
├── nginx-7d4f-abc123 (重建后变成 nginx-7d4f-xyz789)

配合 Headless Service(clusterIP: None),每个 Pod 获得一个独立 DNS 记录:

# Headless Service 定义
apiVersion: v1
kind: Service
metadata:
name: redis-svc
spec:
clusterIP: None # ← Headless:不分配 ClusterIP
selector:
app: redis
ports:
- port: 6379

# DNS 解析结果:
redis-svc.default.svc.cluster.local 所有 Pod IP(A 记录列表)
redis-cluster-0.redis-svc.default.svc... 只有 redis-cluster-0 IP
redis-cluster-1.redis-svc.default.svc... 只有 redis-cluster-1 IP

# 应用里可以这样连:
redis-cluster-0.redis-svc:6379 始终连到同一个 Pod
# Pod 被重建,IP 变了,DNS 自动更新,名字不变

volumeClaimTemplates:每 Pod 独立存储

这是 StatefulSet 和 Deployment 最根本的区别.Deployment 的所有 Pod 共享同一个 PVC 模板(或者根本没有),而 StatefulSet 为每个 Pod 自动创建并绑定一个独立 PVC.

PVC是什么

PVC = PersistentVolumeClaim(持久卷声明),通俗理解:向集群申请一块不会因为 Pod 重启就消失的硬盘.(完整机制见第 07 篇存储系统,这里只讲跟 StatefulSet 相关的部分.)

对比 Pod 里用的普通 Volume:

存储类型 生命周期 Pod 重建后
emptyDir 随 Pod 存在 数据丢失 → emptyDir 是 Pod 创建时在节点上临时建的目录,Pod 删了就清空
hostPath 随 Node 存在 Pod 调到新节点 → 看不到旧节点的数据;同节点两次调度可能冲突
PVC 独立于 Pod 和 Node Pod 重建后重新挂载同一个 PVC → 数据原封不动

为什么有状态服务需要每 Pod 独立 PVC,而不是多 Pod 共享一个?

# 错误:3 个 MySQL Pod 挂载同一个 PVC
Deployment mysql (replicas: 3,共享 PVC)
├── mysql-abc ─┐
├── mysql-def ─┼── /var/lib/mysql(同一份数据文件)
└── mysql-ghi ─┘
→ 三个 MySQL 进程同时写同一份数据文件 → 数据损坏
→ 这不是高可用,这是数据竞争

# 正确:每个 Pod 独立 PVC
StatefulSet mysql (replicas: 3)
├── mysql-0 ── PVC: data-mysql-0 (/dev/sdb 100Gi)
├── mysql-1 ── PVC: data-mysql-1 (/dev/sdc 100Gi)
└── mysql-2 ── PVC: data-mysql-2 (/dev/sdd 100Gi)
→ 各自有独立的 /var/lib/mysql
→ 通过 MySQL 复制协议(或 Galera/Group Replication)保持数据一致
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql-svc # 必须关联一个 Headless Service
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
volumeMounts:
- name: data
mountPath: /var/lib/mysql
volumeClaimTemplates: # ← 自动为每个 Pod 生成 PVC
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Gi

# 产生的 PVC:
$ kubectl get pvc
NAME STATUS CAPACITY POD
data-mysql-0 Bound 100Gi mysql-0
data-mysql-1 Bound 100Gi mysql-1
data-mysql-2 Bound 100Gi mysql-2

# 关键行为:
# mysql-0 挂了重建 → 新 Pod 还是叫 mysql-0,重新挂载 data-mysql-0 PVC → 数据还在
# mysql-1 缩容 → data-mysql-1 PVC 不会被删(保留数据,手动扩容恢复)
# 手动删 PVC 才会丢数据

缩容不会自动删除 PVC

StatefulSet 缩容时 PVC 保留不删.这是刻意的设计: 防止误操作导致数据丢失.
如果你真的不需要数据了,需要手动 kubectl delete pvc data-mysql-2.反之,扩容时如果 PVC 还在,直接复用.

有序部署与缩容

StatefulSet 默认严格按照序数顺序操作:

# 扩容(scale up):0 → 1 → 2
# 前一个 Pod Running + Ready 后,下一个才开始创建
$ kubectl scale statefulset redis-cluster --replicas=3
redis-cluster-0 创建中...
redis-cluster-0 Running ✓ → 开始创建 redis-cluster-1
redis-cluster-1 Running ✓ → 开始创建 redis-cluster-2

# 缩容(scale down):2 → 1 → 0
# 从最大序数开始删,前一个删完再删下一个
$ kubectl scale statefulset redis-cluster --replicas=1
redis-cluster-2 删除中...
redis-cluster-2 已删除 → 开始删除 redis-cluster-1
redis-cluster-1 已删除 → 停止,只剩 redis-cluster-0

可以改 podManagementPolicy: Parallel 关闭顺序保证(所有 Pod 并行启停),适用于不在意顺序的场景(如 Kafka--它自己管 leader 选举).

DaemonSet - 每节点一个 Pod

DaemonSet 的语义最简单:每个符合条件的 Node 上跑且只跑一个 Pod.

  • 新节点加入集群 → 自动调度一个上去
  • 节点下线 → 对应 Pod 被清理
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd-logger
spec:
selector:
matchLabels:
name: fluentd
template:
metadata:
labels:
name: fluentd
spec:
containers:
- name: fluentd
image: fluentd:latest
volumeMounts:
- name: varlog
mountPath: /var/log # 读宿主机的日志目录
volumes:
- name: varlog
hostPath:
path: /var/log
# 3 节点集群,每个节点自动跑一个
$ kubectl get pod -o wide
NAME READY NODE
fluentd-logger-abc12 1/1 node-1
fluentd-logger-def34 1/1 node-2
fluentd-logger-ghi56 1/1 node-3

# 新加 node-4 → 自动创建 fluentd-logger-xxx
# 不需要手动扩容

如果只想部署到部分节点(比如只有 GPU 节点需要 GPU 监控),用 nodeSelector 或 affinity:

spec:
template:
spec:
nodeSelector:
accelerator: nvidia # 只有带这个 label 的节点才部署
典型用途 例子
日志采集 Fluentd / Filebeat / Logstash 每节点采集容器日志
监控 Agent Prometheus Node Exporter / Datadog Agent 收集节点指标
网络插件 Calico / Flannel / Cilium 的 Agent 组件部署到每个节点
存储插件 CSI 驱动的 Node Plugin(如 EBS CSI,Ceph CSI)

DaemonSet = 每栋楼配一个保安.
新楼建好自动派一个保安过去,楼拆了保安调走.不需要每次盖楼都重新签一份保安合同.
Deployment 管的是"流动的租户",DaemonSet 管的是"固定配给每个单元的设施".

Job - 跑完就结束

前面三个控制器都假设 Pod 应该持续运行.Job 反过来--它的 Pod 跑完就应该退出.Job 的核心保证是:指定数量的 Pod 成功完成(exit 0).

apiVersion: batch/v1
kind: Job
metadata:
name: db-migrate
spec:
backoffLimit: 4 # 最多重试 4 次(超出则 Job = Failed)
completions: 1 # 需要多少个 Pod 成功完成
parallelism: 1 # 最多同时跑几个 Pod
template:
spec:
restartPolicy: Never # Job 必须用 Never 或 OnFailure
containers:
- name: migrate
image: my-app:migrate
command: ['./migrate.sh']
$ kubectl get job,pod
NAME COMPLETIONS DURATION
job.batch/db-migrate 1/1 45s

NAME READY STATUS RESTARTS
pod/db-migrate-abcde 0/1 Completed 0
# Pod 成功退出后不会被删(默认保留,方便查日志)

并行度与完成次数

参数 默认 作用
completions 1 总共需要多少个 Pod 成功完成
parallelism 1 最多同时跑几个 Pod
backoffLimit 6 Pod 失败的累计重试上限(全局,不是单个 Pod)
activeDeadlineSeconds 整个 Job 的硬超时(超过则终止所有 Pod,Job = Failed)
ttlSecondsAfterFinished Job 完成多少秒后自动删除(不设则永久保留)

三种 Job 模式

模式 配置 场景
一次性任务 completions=1, parallelism=1 数据库迁移,数据导入
固定完成次数 completions=N, parallelism=M 逐个处理 1000 条消息,每 Pod 处理一条(类似工作队列)
工作队列 completions 不设,每个 Pod 自己判断何时退出 从 Redis/RabbitMQ 取任务,队列空了就退出
# 工作队列模式:100 条待处理任务,5 个 worker 并行
spec:
completions: 100 # 总共要跑完 100 个 Pod
parallelism: 5 # 同时最多 5 个 worker
backoffLimit: 6
template:
spec:
restartPolicy: Never
containers:
- name: worker
image: worker:latest
env:
- name: TASK_ID
valueFrom:
fieldRef:
fieldPath: metadata.name # 每个 Pod 根据名字处理不同任务

CronJob - 定时执行的 Job

CronJob 是一个 Job 的调度器--它在指定时间创建 Job,Job 再创建 Pod. CronJob 本身不运行任何容器.

apiVersion: batch/v1
kind: CronJob
metadata:
name: nightly-backup
spec:
schedule: "0 2 * * *" # 每天凌晨 2:00(UTC)
concurrencyPolicy: Forbid # 如果上次 Job 还在跑,跳过这次
successfulJobsHistoryLimit: 3 # 保留最近 3 个成功的 Job
failedJobsHistoryLimit: 1 # 保留最近 1 个失败的 Job
jobTemplate: # ← 就是完整的 Job spec
spec:
backoffLimit: 2
template:
spec:
restartPolicy: Never
containers:
- name: backup
image: backup-tool:latest
command: ['./backup.sh']
参数 默认 作用
schedule 必填 Cron 表达式(5 段:分 时 日 月 周),K8s 还支持 @hourly @daily 等简写
concurrencyPolicy Allow Allow(允许并发)/ Forbid(跳过)/ Replace(杀掉旧的跑新的)
startingDeadlineSeconds 到了调度时间但没来得及启动(控制平面挂了),超过这个窗口就放弃
suspend false 暂停调度(不删已有 Job,不创建新 Job)
# 手动触发一次(创建一个 Job,忽略 schedule)
$ kubectl create job --from=cronjob/nightly-backup manual-backup-001

# 查看 CronJob 最近创建的 Job
$ kubectl get jobs -l job-label-from-cronjob

Cron 表达式是 5 段,不是 6 段

Linux crontab 是 5 段(分 时 日 月 周),而某些实现(如 Quartz)是 6 段(多了一个秒).
K8s 用的是标准的 5 段 Unix cron 格式."0 2 * * *" = 每天 2:00,"*/5 * * * *" = 每 5 分钟.

五者对比总表

维度 Deployment StatefulSet DaemonSet Job CronJob
Pod 名称 随机 固定序号 随机 随机 随机(Job 名+随机)
Pod 生命周期 持续运行 持续运行 持续运行 完成即退出 完成即退出
启停顺序 并行 0→1→2 / 2→1→0 并行 并行
每 Pod 独立存储 是(volumeClaimTemplates)
稳定网络标识 是(+Headless Service)
节点亲和 用 affinity 用 affinity nodeSelector + 自动随节点扩缩 用 affinity 用 affinity
回滚 rollout undo 不支持(需手动) 不支持 不适用 不适用
API 版本 apps/v1 apps/v1 apps/v1 batch/v1 batch/v1

小结

本篇要点回顾

要点 一句话概括
为什么需要五种控制器 不同工作负载对"期望状态"的定义不同--无状态服务要滚动更新,有状态服务要稳定身份和存储
Deployment 分层 Deployment 管 ReplicaSet 的版本切换,ReplicaSet 管 Pod 数量;回滚 = 重回旧 RS
maxSurge / maxUnavailable 控制滚动更新的速度与安全:surge 先建(需资源余量),unavailable 先删(资源受限)
Recreate 全删再全建,中间服务中断;适用于不支持多版本共存的场景
StatefulSet 三保证 固定名称(n-0, n-1...),稳定 DNS(+Headless Service),每 Pod 独立 PVC(volumeClaimTemplates)
有序启停 启动 0→1→2,停止 2→1→0;可通过 podManagementPolicy: Parallel 关闭
DaemonSet 语义 每节点自动跑一个 Pod,新节点加入自动补上;常用于日志采集,监控 Agent,网络插件
Job 核心参数 completions(完成次数),parallelism(并行数),backoffLimit(重试上限),ttlSecondsAfterFinished(自动清理)
CronJob Job 的定时触发器;注意 concurrencyPolicy 防止重叠执行

动手练习

  1. 创建一个 Deployment(replicas=5, nginx:1.25),用 kubectl set image 更新到 1.26,同时在另一个终端 kubectl get pods -w 观察 Pod 的创建/删除顺序.改 maxSurgemaxUnavailable 后再试,对比差异.
  2. 创建 Headless Service + StatefulSet(nginx),验证 Pod 名称是 xxx-0, xxx-1, xxx-2.exec 进 xxx-0,用 nslookup xxx-1. 验证稳定 DNS 解析.
  3. 给 StatefulSet 配 volumeClaimTemplates,扩容到 3 个副本,观察是否有 3 个独立 PVC.缩容到 1,确认 PVC 是否保留.
  4. 创建一个 DaemonSet(busybox + sleep 3600),确认每个 Node 上恰好有一个 Pod.
  5. 创建一个 Job(completions=5, parallelism=2),用 sleep 10 模拟工作,观察并行执行和完成计数.
  6. 创建一个 CronJob(每分钟打印当前时间),等 3 分钟后检查产生了几个 Job.暂停 CronJob(suspend=true),确认不再产生新 Job.