11. Controller 模式不止于 K8S
回到第 1 章
第 1 章用空调开了头——你设 26 度,空调不停把室温往 26 度推。K8S Controller 是这个模式在集群管理里的实现。
现在回头看,你写的自定义 Controller——不管是第 7 章裸写的还是第 8 章 Kubebuilder 生成的——干的事可以抽象成三步:
1. 读期望状态 (从某个地方拿到"系统应该是什么样")
2. 读实际状态 (从目标系统观察到"现在是什么样")
3. 调谐 (发现差异就驱动变更,让实际向期望靠拢)
K8S 给这套模式提供了具体的脚手架——etcd 存期望、API Server 暴露 Watch、Informer 缓存、Workqueue 调度。但这套脚手架不是模式本身。
打个比方:流水线是亨利·福特发明的生产模式。汽车工厂用流水线,手机工厂也用,服装工厂也用。你不能说"流水线就是造汽车的"——它是一种组织生产的方式,造什么取决于你往线上放什么。
Controller 同理。
模式的三个要素
把 K8S 的具体实现剥掉,Controller 模式剩下三个要素:
| 要素 | 含义 | K8S 里的对应 |
|---|---|---|
| 期望状态 | 你想要系统变成什么样 | CRD 的 spec 字段 |
| 观察机制 | 怎么知道当前状态 | Informer / Watch API Server |
| 调谐动作 | 发现差异后做什么 | 创建/更新/删除 K8S 资源 |
只要一个需求能套进这三个要素,就能用 Controller 思路解决。
非 K8S 场景的 Controller
场景 1:GitOps
ArgoCD、FluxCD 是 K8S 生态的 Controller,但思路不局限于 K8S。
期望状态:Git 仓库里的 YAML
观察机制:轮询 Git / Webhook
调谐动作:kubectl apply 把 YAML 下发到集群
这里期望在 Git 里,不在 etcd 里。观察靠 Git 的 webhook 或定时 pull,不靠 API Server Watch。调谐是 kubectl apply,不是 client-go 的 Create。
本质上跟 K8S Controller 一样——"我要的样子在 Git,集群现状跟 Git 不一致就推过去"。
场景 2:Terraform
期望状态:.tf 文件里声明的资源
观察机制:调云厂商 API 查当前资源
调谐动作:调云厂商 API 创建/更新/删除
Terraform 的 plan 就是"读期望、读实际、算差异",apply 就是"调谐"。差别在于它是手动触发,不是持续循环。但模式一样——声明式期望 + 差异驱动。
如果给 Terraform 加个定时器,让它每隔几分钟自动 plan && apply,它就是一个完整的 Controller——只不过管的是云资源,不是 K8S 资源。
场景 3:证书续期
期望状态:"所有证书剩余有效期 > 30 天"
观察机制:定时扫描证书文件,读过期时间
调谐动作:调 ACME API 续签 + reload 服务
cert-manager 就是这么干的。它的 CRD 里写 duration: 2160h(90 天),renewBefore: 720h(提前 30 天续)。Controller 每隔一段时间扫证书,发现快过期了就调 ACME 续签,签完 reload 控制面。
期望是"30 天内别过期",实际是"现在还剩 X 天",差异是"X < 30",调谐是"续签"。
场景 4:消息积压治理
期望状态:"队列深度 < 1000"
观察机制:定时查 RabbitMQ / Kafka 的队列长度
调谐动作:扩容 consumer 副本数
KEDA 干的就是这个。它 Watch 队列指标,发现积压就调 HPA 扩容消费者。期望状态是"队列深度 < 1000",不是 K8S 资源的字段——是 KEDA 的 ScaledObject spec 里写的阈值。
场景 5:配置同步
期望状态:CMDB 里登记的服务配置
观察机制:轮询 CMDB / 订阅变更
调谐动作:把配置下发到 Nginx / Istio / 业务进程
很多公司的配置中心就是这个模式——CMDB 是 single source of truth,Controller 把 CMDB 的配置同步到各个下游系统。期望在 CMDB,实际在各下游系统,调谐是下发配置。
一个最小通用 Controller 骨架
把 K8S 的具体依赖去掉,Controller 的骨架就这么点:
type Controller struct {
dataSource ExpectationSource // 期望从哪来
targetSystem ActualSource // 实际从哪读
reconciler Reconciler // 怎么调谐
}
func (c *Controller) Run(stopCh <-chan struct{}) {
// 1. 订阅期望变化
events := c.dataSource.Watch(stopCh)
// 2. 也可以定时触发
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-events:
c.reconcile()
case <-ticker.C:
c.reconcile()
case <-stopCh:
return
}
}
}
func (c *Controller) reconcile() error {
desired, err := c.dataSource.Get()
if err != nil {
return err
}
actual, err := c.targetSystem.Observe()
if err != nil {
return err
}
diff := computeDiff(desired, actual)
if diff.Empty() {
return nil // 一致,啥也不干
}
return c.reconciler.Apply(diff)
}
dataSource 在 K8S 里是 API Server,在 GitOps 里是 Git 仓库,在配置中心是 CMDB。targetSystem 在 K8S 里也是 API Server(通过 client-go 操作资源),在 Terraform 里是云厂商 API,在证书续期里是证书文件 + ACME。
骨架不变,填的料不同,就是不同领域的 Controller。
Controller 模式的价值
为什么这个模式值得抽出来?
价值 1:自愈
Controller 不停地转,系统偏离期望状态会被自动纠正。人不用介入。
传统运维是命令式——出问题人去修复。Controller 是声明式——人描述"应该什么样",Controller 负责保持。
打比方:传统运维像保安巡逻,发现问题处理;Controller 像恒温系统,温度偏了自动调。
价值 2:水平扩展
期望状态写在中心化存储里(etcd / Git / CMDB),Controller 可以多副本跑(Leader Election 保证只有一个干活)。一个 Controller 实例管几千个资源,扛不住就加副本。
命令式脚本做不到——脚本跑在哪台机器上,状态就在哪。机器挂了脚本也挂。
价值 3:审计和可追溯
期望状态是声明式数据,存在版本化系统里(Git、etcd 的 resourceVersion)。谁改了什么、什么时候改的、改之前什么样,全有记录。
命令式操作——你 SSH 上去改了配置,没人知道你改了什么。
价值 4:组合性
Controller 之间通过"资源"协作——一个 Controller 的输出是另一个的输入。
Deployment Controller ──创建 Pod──→ EndpointSlice Controller ──更新端点──→ kube-proxy
每个 Controller 只管自己的资源,通过 Watch 别人的输出协作。像流水线上每个工位只干一道工序,整体串起来是一条完整的生产线。
什么时候不该用 Controller
模式不是银弹。这些场景用 Controller 是过度设计:
一次性任务——部署一次完事,不需要持续调谐。写个脚本就行。
强交互场景——需要人实时决策的操作。Controller 的 Reconcile 是无状态的,不适合"问用户要不要继续"这种交互。
低频变更——一年改一次的东西,搞个 Controller 持续 Watch 太重。手动 apply 一次完事。
复杂状态机——状态转移逻辑很复杂的场景。Controller 的 Reconcile 每次从头算,不记历史状态。如果你的系统是"根据上一步走到哪决定下一步干什么",状态机模式更合适。
从 K8S 学到的
写完这个系列,回头看 K8S 给我们上的最大一课:
不要写"怎么做",要写"要什么"。
你写 replicas: 3,不说"去创建 3 个 Pod"。Controller 自己算该创建几个、怎么创建、创建在哪。系统偏离了,Controller 自己纠正。你只描述目标状态。
这个思想不限于 K8S。你管什么系统——数据库、云资源、证书、配置——只要能描述"我想要什么样",就能用 Controller 思路让系统自动保持那个样子。
K8S 给了一套成熟的实现参考——Informer 缓存、Workqueue 去重、Reconcile 幂等、Leader Election 高可用。这套实现你可以照搬到非 K8S 场景,也可以简化(比如不需要 Watch,定时轮询就行;不需要 Leader Election,单实例够用)。
模式是道,K8S 是术。道不变,术可换。
系列总结
回顾这个系列走过的路:
| 章 | 主题 | 你学到了什么 |
|---|---|---|
| 1 | 什么是 Controller | 声明式 + 调谐循环的本质 |
| 2 | 理解控制循环 | Informer / Workqueue / Reconcile 原理 |
| 3 | Deployment 控制循环 | ReplicaSet 套娃、滚动更新的数学逻辑 |
| 4 | StatefulSet 控制循环 | ControllerRevision 版本管理、有序身份机制 |
| 5 | Service 控制循环 | Service / EndpointSlice / kube-proxy 协作 |
| 6 | GVK 与 GVR | K8S API 的寻址方式 |
| 7 | 编写 CRD | 教 K8S 认识新资源 |
| 8 | 最精简 Controller | 裸 client-go 实现的底层机制 |
| 9 | 使用 Kubebuilder | 用框架省掉样板代码 |
| 10 | 准备部署 | 镜像、Leader Election、健康检查、指标 |
| 11 | 不止于 K8S | 抽离出通用模式,看 K8S 外的应用 |
从概念到原理、从内置到自定义、从裸写到框架、从开发到部署、最后从 K8S 抽离到通用模式——希望这条线能帮你建立对 Controller 的完整认知。
前置阅读
本章是系列的收尾,回头看前面所有章节都是前置:
- 什么是Controller — 第 1 章的空调比喻是本章的起点
- 最精简Controller — 第 7 章的骨架代码是本章通用骨架的 K8S 具体化
- 控制器模式与CRD — Starter 第 12 章,整个系列的起点
思考题
问题 1:你工作中有没有一个场景,现在是用脚本/手动操作维护的,但其实可以用 Controller 思路改造?描述它的期望状态、观察机制、调谐动作。
问题 2:Controller 模式跟状态机模式的根本区别是什么?
问题 3:K8S 的 Informer + Workqueue 这套机制,搬到非 K8S 场景需要做什么改造?
解答
问题 1:这个问题没有标准答案,但思路是——找那些"你定期手动对账"的场景。比如:定期检查某系统的配置跟文档是否一致、定期清理过期资源、定期同步两个系统的状态。这些都可以抽象成"期望在 A、实际在 B、定期 reconcile"的 Controller。
问题 2:Controller 每轮 Reconcile 从头算——读期望、读实际、调谐,不记"上次做到哪了"。状态机有明确的当前状态,根据当前状态和输入决定转移到哪个新状态。Controller 适合"目标导向"的场景(我要 3 个 Pod),状态机适合"流程导向"的场景(订单从待支付→已支付→已发货)。混用的话——Controller 外层调谐,内部复杂流程用状态机管理子状态。
问题 3:Informer 依赖 K8S API Server 的 Watch 接口——非 K8S 场景要自己实现事件源。如果目标系统支持 webhook(Git、CMDB),用 webhook 替代 Watch;如果不支持,定时轮询。Workqueue 可以直接复用——它跟 K8S 无关,就是个 Go 的限速队列。Reconcile 的幂等要求不变——不管事件源是什么,Reconcile 都要幂等。
这个系列到此结束。从空调讲到 K8S 再讲到通用模式,希望你以后看到任何"声明式 + 调谐"的系统,都能一眼看穿它的 Controller 本质。