1. 什么是 Controller
先忘掉 K8S
问你一个问题:你家里的空调是怎么工作的?
你拧到 26 度,空调就开始吹冷风。吹到 26 度,停。温度升到 27 度,又吹。
你有没有想过,空调根本不知道"26 度"是什么意思——它只是不停地在做一件事:测量当前温度,跟目标温度比,高了就制冷,低了就停。
这就是 Controller。空调是一个温度 Controller。
你设定的 26 度 ──→ 空调 Controller ──→ 实际室温
↑ │
└──── 传感器读数 ─────┘
这个模式到处都是:
| 场景 | 期望状态 | 谁在调谐 | 实际状态 |
|---|---|---|---|
| 空调 | 26°C | 压缩机启停 | 室温 |
| 定速巡航 | 120 km/h | 油门开合 | 车速 |
| 恒温水壶 | 55°C | 加热棒 | 水温 |
| GitOps | Git 里的 YAML | ArgoCD | 集群里的资源 |
Controller 的本质就一句话:有人设定了"我想要的样子",有个东西不停地把系统往那个样子推。
K8S 只是把这个模式搬到了集群管理上。你写一个 YAML 说"我要 3 个 Pod",K8S 里的 Controller 就不停地看——现在有几个?多了就杀,少了就建。
K8S 里的 Controller
把空调的模型套到 K8S 上:
你的 YAML (spec.replicas: 3) ──→ K8S Controller ──→ 集群里的 Pod
↑ │
└── Watch API Server ─┘
"现在有几个 Pod?"
你写的 Deployment YAML 就是"26 度"——你告诉 K8S 你想要什么,而不是怎么做。
Controller 干的活:
- 读期望状态:从 etcd 里看你要几个 Pod
- 读实际状态:看现在集群里到底有几个
- 算差异:3 - 2 = 少了 1 个
- 调谐:创建 1 个 Pod
这四步循环转,就是 Controller 的全部工作。
声明式 vs 命令式
这里有个根本性的转变,值得停下来想清楚。
命令式:你告诉系统"做什么"。
# 命令式:你手动执行每一步
docker run nginx
docker run nginx
docker run nginx
声明式:你告诉系统"我要什么样子",怎么达到由它决定。
# 声明式:你只描述目标状态
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 3
打个比方:命令式像你亲自炒菜,一步一步放盐放油。声明式像你跟厨师说"我要一份宫保鸡丁",怎么做是厨师的事。
K8S 是声明式的。你写 YAML 描述"我要 3 个 Nginx Pod",Controller 就是那个厨师——你不管它怎么把菜做出来,你只看最后端上来对不对。
K8S 内置的 Controller
K8S 自己就跑着一堆 Controller,你一直在用但可能没意识到:
| Controller | 管什么 | "26度"是什么 |
|---|---|---|
| Deployment Controller | Pod 副本数 | spec.replicas |
| ReplicaSet Controller | Pod 副本数(被 Deployment 包着) | spec.replicas |
| StatefulSet Controller | 有状态 Pod 的顺序和身份 | spec.replicas + 序号 |
| DaemonSet Controller | 每个节点跑一个 | 节点数 |
| Job Controller | 跑完就退出的任务 | spec.completions |
| CronJob Controller | 定时任务 | spec.schedule |
| Endpoint Controller | Service 背后的 Pod 列表 | Service selector |
| Node Controller | 节点健康 | 节点心跳 |
这些 Controller 全部跑在一个叫 kube-controller-manager 的进程里。你看 K8S 架构图时那个 "Controller Manager" 的方块,里面就是这一堆 Controller。
┌─────────────────────────────────────┐
│ kube-controller-manager │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │Deployment│ │DaemonSet │ ... │
│ │Controller│ │Controller│ │
│ └──────────┘ └──────────┘ │
│ │
│ 每个 Controller 就是一个调谐循环 │
└───────────────────────────────────────┘
为什么要写自定义 Controller
内置 Controller 管的都是 K8S 原生资源——Pod、Deployment、Service 这些。但现实世界的需求千奇百怪:
- 我要管 MySQL 集群——主从同步、备份、故障切换
- 我要管 Redis 集群——分片、迁移、扩缩容
- 我要管一组定时任务——比 CronJob 更复杂的调度逻辑
- 我要管证书——快过期了自动续
这些东西,K8S 不认识。你写一个 kind: MySQL 的 YAML,K8S 看不懂——它不知道 MySQL 是什么。
自定义 Controller 干的事就是:教 K8S 认识一个新东西,然后像空调一样把它往你想要的样子推。
这就是 Operator——自定义 Controller + 自定义资源(CRD)的组合。
Controller 和 Operator 的关系
这俩词经常混着用,其实关系很简单:
Operator = CRD(自定义资源) + Controller(调谐逻辑)
CRD 是"语言"——教 K8S 认识 MySQL 这个词
Controller 是"大脑"——看到 MySQL 资源后知道该干什么
打个比方:
- CRD 像菜单上加了一道新菜——告诉客人"我们这里有宫保鸡丁"
- Controller 像厨师学会了做这道菜——看到订单就知道怎么炒
光加菜单(CRD)没人会做,没用。光会做但菜单上没有,客人点不了。两个一起才是完整的 Operator。
一个 Controller 长什么样
伪代码画个骨架,后面几章会一步步填满:
// Controller 的核心就是一个 for 循环
for {
// 1. 读期望状态:从 API Server 拿你写的 YAML
desired := getFromAPIServer()
// 2. 读实际状态:看集群里现在什么样
actual := getFromCluster()
// 3. 算差异
diff := compare(desired, actual)
// 4. 调谐:多的删,少的建
if diff > 0 {
createResources(diff)
} else if diff < 0 {
deleteResources(-diff)
}
// 5. 歇会儿,继续
time.Sleep(time.Second)
}
真实的 K8S Controller 比这个复杂——不会傻 sleep,而是基于事件触发(Watch 机制);不会直接读写,而是通过 Informer 缓存。但骨架就是这个。
前置阅读
本章是 Controller 系列的入口。如果你对 K8S 的基本概念还模糊,建议先看:
- 控制器模式与CRD — Starter 系列第 12 章,讲了 Controller 的概念和 CRD 的关系,是本章的直接前置
- 工作负载管理 — Starter 系列第 5 章,Deployment/StatefulSet/DaemonSet 都是内置 Controller,先理解它们再看自定义 Controller 会顺畅很多
思考题
问题 1:空调的例子中,如果你把温度设到 16 度,但房间窗户全开着——空调会一直制冷。这在 K8S Controller 里对应什么场景?
问题 2:声明式和命令式的区别,用一个你日常工作中的例子说明。
解答
问题 1:对应"Controller 拼命调谐但永远达不到期望状态"。比如你设了 replicas: 3,但节点资源不够,Pod 一直 Pending。Controller 会不停尝试创建——这跟空调一直制冷但温度降不下来是一回事。区别是 K8S Controller 有退避机制(back-off),不会无限疯狂重试。
问题 2:比如发版。命令式是你 SSH 到每台机器上手动 systemctl restart。声明式是你写一个 Deployment YAML 说"我要 v2 镜像跑 3 个副本",Controller 自己滚动更新。前者你管过程,后者你只管结果。
下一章
下一章我们拆开 Controller 的引擎——控制循环——看看 Informer 和 Workqueue 是怎么让这个循环转起来的。