Skip to main content

1. 什么是 Controller

先忘掉 K8S​

问你一个问题:你家里的空调是怎么工作的?

你拧到 26 度,空调就开始吹冷风。吹到 26 度,停。温度升到 27 度,又吹。

你有没有想过,空调根本不知道"26 度"是什么意思——它只是不停地在做一件事:测量当前温度,跟目标温度比,高了就制冷,低了就停。

这就是 Controller。空调是一个温度 Controller。

你设定的 26 度 ──→ 空调 Controller ──→ 实际室温
↑ │
└──── 传感器读数 ─────┘

这个模式到处都是:

场景期望状态谁在调谐实际状态
空调26°C压缩机启停室温
定速巡航120 km/h油门开合车速
恒温水壶55°C加热棒水温
GitOpsGit 里的 YAMLArgoCD集群里的资源

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 干的活:

  1. 读期望状态:从 etcd 里看你要几个 Pod
  2. 读实际状态:看现在集群里到底有几个
  3. 算差异:3 - 2 = 少了 1 个
  4. 调谐:创建 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 ControllerPod 副本数spec.replicas
ReplicaSet ControllerPod 副本数(被 Deployment 包着)spec.replicas
StatefulSet Controller有状态 Pod 的顺序和身份spec.replicas + 序号
DaemonSet Controller每个节点跑一个节点数
Job Controller跑完就退出的任务spec.completions
CronJob Controller定时任务spec.schedule
Endpoint ControllerService 背后的 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 是怎么让这个循环转起来的。