7.10 Operator 与 CRD

一句话

CRD 是 “给 API server 加一种新对象”,Operator 是 “为这个对象写一个专用控制器”;要不要写 Operator,看 “这套运维知识能否写成状态机”,而不是 “别人都有”——写不好就是一个会误删数据的脚本。

CRD:给集群加一个 “名词”

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata: {name: databases.example.com}
spec:
  group: example.com
  scope: Namespaced
  names: {kind: Database, listKind: DatabaseList, plural: databases, singular: database}
  versions:
    - name: v1
      served: true
      storage: true
      subresources: {status: {}}            # 关键:status 独立子资源
      additionalPrinterColumns:              # kubectl get 时显示什么
        - {name: Ready, type: string, jsonPath: .status.conditions[?(@.type=="Ready")].status}
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              required: [version, storageGB]
              properties:
                version:   {type: string, pattern: "^[0-9]+\\.[0-9]+$"}
                storageGB: {type: integer, minimum: 10}

三个必设项,缺一个后患无穷:

设置 为什么
subresources.status 让 controller 改 status 时不会覆盖用户的 spec(否则两边互相覆盖)
additionalPrinterColumns 否则 kubectl get 只能看到名字,运维体验倒退十年
结构化 schema + 校验 没有校验的 CRD 等于 “任何字段都接受”,错误延后到运行期才爆

Operator:把运维动作写成调谐循环

组件 作用 常见错误
Reconcile 读 spec → 读现状 → 算出差异 → 执行 只处理 “创建”,不处理 “外部被改动”
Finalizer 删除前清理外部资源(云盘、DNS、账号) 清理失败不返回错误 → 对象永远 Terminating
ownerReferences 级联删除子对象 子对象在别的命名空间 → 跨命名空间不生效
status.conditions 结构化状态(Ready/Progressing/Degraded) 只写 phase: Running,无法表达 “部分就绪”
事件(Events) 面向人的 “发生了什么” 不写事件,排错只能看日志

能力分级(Operator Framework 的公开模型,用来判断 “值不值得写”):

级别 能做什么 大致工作量
1. Basic Install 一键装起来 小
2. Seamless Upgrade 版本升级与回滚 中
3. Full Lifecycle 备份、恢复、扩缩容、故障切换 大
4. Deep Insights 指标、告警、日志约定 大
5. Auto Pilot 自动调优、自动扩缩、自愈决策 很大

大多数团队需要的是 1–2 级;宣称做到 5 级但没人维护的 Operator,比手工运维更危险(它会在凌晨自动做决定)。

什么时候不需要 Operator

情况 更简单的做法
无状态应用 Deployment + HPA 已经够了
运维逻辑只有 “改配置 + 重启” Helm chart + runbook
中间件用托管服务 厂商已经替你写了 Operator
团队没人能维护控制器 手工 + 定时任务,至少出错时有人看得懂

判据一句话:这套中间件的运维知识,能否写成 “读状态 → 判断 → 动作” 的确定状态机? 能,且重复频率高,才值得;否则就是给自己造一个必须长期维护的服务。

反模式

反模式 后果
CRD 设计成 “万能对象”(塞 60 个字段) 校验失效、版本升级无法兼容、用户不会用
改 CRD schema 时删字段/改类型 存量对象校验失败,集群升级卡住
Finalizer 清理失败却不报错 对象永久 Terminating,只能手工 patch 去掉 finalizer
Operator 里用轮询全量 List 代替 watch apiserver 压力大,收敛慢
控制器把用户 spec 改掉 用户改一次被覆盖一次,最终没人敢改
只写 phase,不写 conditions 无法表达并发状态,上层无法做判断
用 Operator 管 “本该由 Helm 管” 的部署 两套控制器抢同一批对象,互相打架

三条铁律

🔑 CRD 先定 schema 与 status 子资源;没有校验和状态分离的自定义对象,长期一定失控。

🔑 Finalizer 必须 “要么成功清理,要么报错”;静默失败会制造永远删不掉的对象。

🔑 只写你能维护的 Operator;1–2 级能力覆盖大多数场景,5 级的承诺通常没人兑现。

自检

Q1 为什么必须开 subresources.status?

▸ 答案:因为不开时,controller 更新 status 会走整对象更新,与用户更新 spec 冲突(后写覆盖先写),导致 “用户刚改的 spec 被 status 写回旧值”。开了子资源后,status 与 spec 分开写,并发安全,也让 RBAC 能区分 “谁能改状态、谁能改期望”。

Q2 一个自定义对象删不掉,一直 Terminating,怎么处理?

▸ 答案:先看 metadata.finalizers 与 controller 日志:通常是 finalizer 对应的清理逻辑失败却返回了 nil(不再重试),或 controller 已经不在运行。临时解法是 kubectl patch <obj> -p '{"metadata":{"finalizers":null}}' --type=merge(有丢外部资源的风险);根本解法是让 finalizer 在失败时返回错误,持续重试并上报事件。

Q3 团队想给一个 “只有读多写少” 的 Redis 写 Operator,做自动扩缩与故障切换。你会怎么劝?

▸ 答案:先问三件事:① 用托管 Redis 行不行(厂商已写好 Operator,成本最低);② 运维知识是否是确定状态机(哨兵/集群模式的切换逻辑很复杂,写错会脑裂);③ 谁长期维护它。若三者都没有好答案,答案就是 “不写”,用托管服务或标准 chart + runbook,把精力放在业务上。

Q4 CRD 加了一个新版本 v2,怎么保证存量对象不坏?

▸ 答案:三件事:① 不删不改 v1 已有字段(只加可选字段),保证 v1 对象在 v2 schema 下也合法;② 配 conversion webhook 做 v1 ↔ v2 转换,并保证往返无损;③ 先让 served: true 的 v1 与 v2 并存一段时间,等所有客户端升级后再把 v1 从 storage 里退役。跳过任何一步,升级时都会出现 “对象读不出来”。

Q5 为什么说 “Operator 会误删数据” 不是危言耸听?

▸ 答案:因为 Operator 拥有写权限:它能删 PVC、删云盘、改配置、触发主从切换。如果状态机逻辑有漏洞(比如 “副本数变 0 就清理所有卷”),一次配置错误就会执行成 “删除全部数据”。这正是 “只写你能维护的 Operator” 的原因——它的权限与数据库管理员相当,但往往没有同等的测试与演练。

跨篇相关:控制器模式与水平触发见 7.1 声明式 API 与控制器模式;CRD 对象与 ownerReferences 见 7.2 K8s 对象模型 (Pod / ReplicaSet / Deployment / StatefulSet / DaemonSet / Job);Operator 管有状态服务时的存储语义见 7.7 存储卷与持久化;Helm 与 Operator 的职责边界见 7.11 包管理与 Helm;何时不值得引入见 7.12 何时不该用 K8s。