Operator 的本质:把运维知识写成控制循环
普通 YAML 能描述一组静态对象,但很难表达这些规则:
- 一个业务对象自动派生一个 Namespace;
- 先校验资源预算,再创建任何持久资源;
- Secret 缺失时随机生成,存在时绝不覆盖;
- Gateway 模式才创建 HTTPRoute;
- suspend 时缩容但保留 PVC;
- 删除时等待 Namespace 真正清理完;
- 持续把依赖状态汇总成 Conditions。
Operator 把这类领域运维知识编码进 controller。使用者面对的是 SubPool,底层 Kubernetes 积木由 controller 组织。
CRD:给 Kubernetes 扩展一种新对象
CustomResourceDefinition 告诉 API Server:集群现在支持一种新资源。
本项目安装:
| |
安装 CRD 后,才能执行:
| |
CRD 只定义 API 和校验规则,不会自己创建 StatefulSet。真正的自动化逻辑来自 Operator controller。
Go 类型如何变成 Kubernetes API
api/v1alpha1/subpool_types.go 定义:
| |
Kubebuilder markers 描述 scope、short name、status subresource、打印列和 CEL 校验。生成的 CRD YAML被放在 config/crd/bases/。
Go 类型是 controller 编译时使用的强类型模型,CRD schema 是 API Server 接收对象时使用的运行时契约。修改其中一边却忘记重新生成另一边,会造成代码和集群 API 不一致。
静态校验与动态校验的边界
CRD schema 适合检查:
- exposure mode 枚举;
- 必填字段;
- 名称格式;
- storage.size 不可变;
- resources 必须包含 CPU 和内存。
Operator 适合检查:
- 配置的 ConfigMap 能否读取与解析;
- 两个容器的合计资源是否超过平台包络;
- 派生 Namespace 是否已被别人占用;
- Secret 是否属于当前 SubPool;
- Shared Mihomo 有没有 Ready Endpoint;
- HTTPRoute 和 Gateway status 是否就绪。
静态规则尽早拒绝,动态规则通过 Condition 提供可诊断状态。
Controller Manager、Scheme 与 Client
main.go 创建 controller-runtime Manager,并把这些 API 注册到 Scheme:
- Kubernetes core API;
- Gateway API v1;
- SubPool v1alpha1。
Scheme 让 client 知道 Go 类型与 Group/Version/Kind 的对应关系。没有注册类型,controller 无法正确编码或解码对象。
Manager 统一负责 client/cache、controller 生命周期、metrics、health endpoints、leader election 和信号处理。
Watch 不是每秒全量扫描
Controller 通常通过 list/watch 与本地 cache 感知对象变化。SubPool 创建、spec 更新或受管对象变化时,事件进入 work queue,触发 reconcile。
本项目声明 Owns Namespace、Secret、ConfigMap、ServiceAccount、Service、StatefulSet、ResourceQuota、NetworkPolicy 和 PDB。它们的 owner reference 让 controller-runtime 能把子资源事件映射回 SubPool。
共享 Gateway 和 Mihomo EndpointSlice 不归任何 SubPool 所有,不能通过 Owns 关联。因此 controller 在活动状态下默认每 30 秒 requeue,周期性复核这些共享依赖。
这不是用轮询创建资源,而是为无法直接归属到单个 SubPool 的共享状态提供保守复查。
predicate 为什么过滤 status-only 更新
Controller 更新 SubPool status 本身也会产生对象更新事件。如果每次 status 更新都再次触发 reconcile,可能形成无意义循环。
项目的 predicate 只在以下根对象变化时继续:
- 新建或删除事件;
metadata.generation变化,也就是 spec 更新;- 首次出现 deletionTimestamp。
单纯 status 更新不会触发新的根 reconcile。这能降低 API Server 和 work queue 噪声。
一次 reconcile 的固定顺序
活动 SubPool 的流程可以概括为:
| |
先校验再创建很重要。无效请求不会留下 Namespace、PVC 或 Secret 等持久半成品。
Reconcile 必须幂等
幂等表示:对同样的期望和实际状态重复执行,最终结果不变,也不会每次制造新资源。
项目的 ensureManagedObject 使用这种模式:
| |
这比“每次先删再建”安全得多。Service 的 ClusterIP 等 API Server 分配字段必须保留,不能拿一份新对象整体覆盖。
只管理明确拥有的字段
Controller 同步稳定 labels 和 owner references,并按 Kind 更新特定 spec。
以 Service 为例,它只同步:
- type;
- ports;
- selector。
ClusterIP、IP family 等由 API Server 分配或默认的字段被保留。
Metadata 中的额外 labels 也被保留,只覆盖 Operator 自己期望的键。这让其他合法系统可以添加自己的 label,而不会被每轮 reconcile 清掉。
“SubPool 是唯一期望来源”不意味着粗暴覆盖对象所有字节,而是要明确 controller 对哪些字段拥有写入权。
所有权冲突为什么必须阻塞
若同名对象已存在但不由当前 SubPool 控制,项目不会自动加 owner reference。否则可能出现:
| |
因此 Namespace、Secret 和其他受管资源都会检查 metav1.IsControlledBy。Namespace 还额外检查 managed label 和 Pool label。
冲突会写入稳定 reason,例如 NamespaceConflict、CredentialConflict 或 ManagedResourceConflict。
Finalizer:删除前的待办清单
SubPool 使用:
| |
正常活动对象先加入 finalizer。用户执行 delete 后,API Server 设置 deletionTimestamp,但在 finalizer 清空前保留对象。
Operator 删除路径:
| |
如果同名 Namespace 不属于当前 SubPool,Operator 不删除它,只移除自己的 finalizer。这保持所有权边界。
为什么不要手工移除 finalizer
直接移除 finalizer 会让 SubPool 消失,但它负责清理的 Namespace 可能继续存在,形成孤儿资源、PVC 和 Secret。
若删除卡住,应检查:
DeletionBlocked的 reason 和 message;- Namespace 的 finalizers;
- PVC/PV 与 CSI 状态;
- 第三方 controller 是否还在运行;
- API 删除是否收到 Forbidden 或其他错误。
修复负责收尾的依赖,比强行撕掉“待办清单”更安全。
suspend 是期望状态,不是删除流程
spec.suspend=true 时:
- StatefulSet replicas 改为 0;
- HTTPRoute 删除;
- Namespace、PVC 和 Secret 保留;
- Conditions 明确使用
Suspended; - ProxyGatewayReady 设为 Unknown,因为暂停期间不需要评估代理。
这说明 Operator API 不只有 CRUD。一个业务资源可以拥有清晰的生命周期状态,而不需要通过手工改底层对象实现暂停。
Status 更新也要避免噪声
Condition 只有在 status、reason、message 或 observedGeneration 变化时才替换,并保留未变化条件的 lastTransitionTime。
如果每 30 秒 reconcile 都刷新时间,会让监控以为状态持续变化,也产生大量无意义 API 写入。稳定的 transition time 才能回答“这个问题从什么时候开始”。
阅读 Operator 代码的推荐路线
| |
Operator 并不是 Kubernetes 的魔法插件。它只是一个认真遵守幂等、所有权和异步状态语义的程序:不断比较愿望与现实,并把差距一点点收敛掉。