前置回顾
第 01 篇:Controller Manager 的核心逻辑是观察 → 比对 → 修正.
第 02 篇:Pod 是 K8s 的最小调度单元
但手动管理 Pod 的生命周期是不现实的--Pod 挂了没人重建,更新要手动删建.本篇的五种控制器就是对不同"期望状态模式"的自动化实现.
一个 Pod 还是管理一组 Pod?
直接在 K8s 里创建一个裸 Pod--可行.但它挂了不会自动重建,更新镜像要手动删了再建,跨节点调度也没有自动修复.
|
工作负载控制器解决的就是这件事:你声明一组 Pod 应该长什么样,控制器持续保证它实际长成那样.
但"应该长什么样"这个问题,不同场景答案完全不同:
|
如果这些全用同一种控制器管理,你需要手工处理 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 是使用率最高的控制器.先看一个最基本的样子:
|
对比裸 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 工作的前提.
提交后发生的事:
|
Deployment → ReplicaSet → Pod
Deployment 不直接管 Pod: 它管 ReplicaSet,ReplicaSet 再管 Pod.第一篇里已经提过这个分层设计,现在从"更新"的角度再看一遍:
|
旧 ReplicaSet 不会删: 它保留了上一版本的完整 Pod 模板,回滚的本质就是把 Deployment 重新指向旧 RS.
滚动更新:maxSurge 与 maxUnavailable
这两个参数控制滚动更新的速度与安全性,直接定义了每一步能建几个新 Pod,能删几个旧 Pod.
| 参数 | 含义 | 默认值 |
|---|---|---|
maxSurge |
更新期间最多允许超出 replicas 多少个 Pod | 25%(四舍五入向上取整) |
maxUnavailable |
更新期间最多允许多少个 Pod 不可用 | 25%(四舍五入向下取整) |
两者不能同时为 0--否则没有空间建新 Pod 也不允许删旧 Pod,更新永远无法推进.
以 replicas: 3 为例,看三种典型配置:
|
|
|
|
百分比还是绝对值?
两个参数都支持百分比和绝对值.
- 百分比按 replicas 计算:
maxSurge: "25%",replicas=10 时允许超出 3 个(10×25%=2.5,四舍五入向上=3) - 绝对值写整数:
maxSurge: 2. - replicas 较小时(3~5),绝对值更直观;大副本数时百分比更合理.
回滚机制
每次更新(template 变更)产生一个新的 revision.旧 revision 的 ReplicaSet 保留不删,回滚只是把 Deployment 重新指向它.
|
Deployment 默认保留 10 个 revision(由 revisionHistoryLimit 控制).超出的旧 ReplicaSet 会被清理.
其他更新策略
| 策略 | 行为 | 适用场景 |
|---|---|---|
| RollingUpdate(默认) | 逐批替换,保证可用副本数 | 绝大多数场景 |
| Recreate | 先全部删光,再全部新建 | 不支持多版本共存(如旧版连新版数据库会出错);或资源极紧无法容纳 surge |
|
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,也不做负载均衡:
|
|
为什么 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.
|
配合 Headless Service(clusterIP: None),每个 Pod 获得一个独立 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 共享一个?
|
|
缩容不会自动删除 PVC
StatefulSet 缩容时 PVC 保留不删.这是刻意的设计: 防止误操作导致数据丢失.
如果你真的不需要数据了,需要手动 kubectl delete pvc data-mysql-2.反之,扩容时如果 PVC 还在,直接复用.
有序部署与缩容
StatefulSet 默认严格按照序数顺序操作:
|
可以改 podManagementPolicy: Parallel 关闭顺序保证(所有 Pod 并行启停),适用于不在意顺序的场景(如 Kafka--它自己管 leader 选举).
DaemonSet - 每节点一个 Pod
DaemonSet 的语义最简单:每个符合条件的 Node 上跑且只跑一个 Pod.
- 新节点加入集群 → 自动调度一个上去
- 节点下线 → 对应 Pod 被清理
|
|
如果只想部署到部分节点(比如只有 GPU 节点需要 GPU 监控),用 nodeSelector 或 affinity:
|
| 典型用途 | 例子 |
|---|---|
| 日志采集 | 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).
|
|
并行度与完成次数
| 参数 | 默认 | 作用 |
|---|---|---|
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 取任务,队列空了就退出 |
|
CronJob - 定时执行的 Job
CronJob 是一个 Job 的调度器--它在指定时间创建 Job,Job 再创建 Pod. CronJob 本身不运行任何容器.
|
| 参数 | 默认 | 作用 |
|---|---|---|
schedule |
必填 | Cron 表达式(5 段:分 时 日 月 周),K8s 还支持 @hourly @daily 等简写 |
concurrencyPolicy |
Allow | Allow(允许并发)/ Forbid(跳过)/ Replace(杀掉旧的跑新的) |
startingDeadlineSeconds |
无 | 到了调度时间但没来得及启动(控制平面挂了),超过这个窗口就放弃 |
suspend |
false | 暂停调度(不删已有 Job,不创建新 Job) |
|
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 防止重叠执行 |
动手练习
- 创建一个 Deployment(replicas=5, nginx:1.25),用
kubectl set image更新到 1.26,同时在另一个终端kubectl get pods -w观察 Pod 的创建/删除顺序.改maxSurge和maxUnavailable后再试,对比差异. - 创建 Headless Service + StatefulSet(nginx),验证 Pod 名称是
xxx-0, xxx-1, xxx-2.exec 进 xxx-0,用nslookup xxx-1.验证稳定 DNS 解析. - 给 StatefulSet 配 volumeClaimTemplates,扩容到 3 个副本,观察是否有 3 个独立 PVC.缩容到 1,确认 PVC 是否保留.
- 创建一个 DaemonSet(busybox +
sleep 3600),确认每个 Node 上恰好有一个 Pod. - 创建一个 Job(
completions=5, parallelism=2),用sleep 10模拟工作,观察并行执行和完成计数. - 创建一个 CronJob(每分钟打印当前时间),等 3 分钟后检查产生了几个 Job.暂停 CronJob(suspend=true),确认不再产生新 Job.