第 14 章 可扩展机制:CRD / Operator / Admission
墨:老哥,这章是给”想自己造工具”的你准备的。前面 13 章我们反复看到一句话——K8s 把核心能力依次开放给用户。这章把所有”开放口子”汇总,并手把手让你看清:你完全能写一个自己的控制器,像内置的一样跑在集群里。先说结论:K8s 不是产品,是平台。平台的意思是——它把”造 K8s 的能力”也给了你。
14.1 为什么需要扩展:内置永远不够
墨:K8s 内置了 Deployment/Service/PV… 这么多对象,还不够你用吗? 短期够,长期不够。因为:
- 你的业务有领域对象(如”一个 MySQL 集群""一个 Kafka 主题""一个 AI 训练任务”),它们不是 Pod/Deployment 能直接表达的;
- 这些领域对象有专属运维逻辑(如”扩容 MySQL 要先扩从库、再切换主""备份要在低峰做”),通用控制器不懂;
- 你想把”领域知识”编码进集群,让集群自动按你的规则运维,而不是每次手工操作。
如果 K8s 封闭,你只能”在 K8s 外面”写脚本管这些——又回到命令式、人肉的老路(第 2.5.1)。所以 K8s 选了开放:让你在集群内部定义新对象、写新控制器。这等于把”造 K8s 的机制”交给你。
元设计(meta-design):最强的可扩展性,是让系统能”长出”系统。 K8s 的 API、调度、存储、网络、运行时全是接口,你能在每个接口上挂自己的实现。一个系统若能”扩展自己”,它就从”工具”升维成”平台”。
14.2 CRD:把”数据模型”的扩展权交给你
CRD(CustomResourceDefinition) 让你声明”一种新的资源类型”。比如你要管”MySQL 集群”,定义:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata: { name: mysqlclusters.db.example.com }
spec:
group: db.example.com
names: { kind: MySQLCluster, plural: mysqlclusters }
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema: # 你的对象的字段定义
type: object
properties:
spec:
properties:
replicas: { type: integer }
version: { type: string }
status: # 状态子资源
properties:
readyReplicas: { type: integer }apply 之后,你的集群就多了一类对象 MySQLCluster,你可以 kubectl get mysqlclusters、可以 kubectl apply 一个 MySQLCluster 实例。API Server 自动给它存 etcd、做认证/RBAC/准入/watch——和内置对象一模一样!
墨:你品,这有多狠。 你只定义了一个”结构(schema)“,K8s 就把”存储、鉴权、版本、watch、校验”全部白送给你。你没写一行存储代码,却拥有了一个”持久化、强一致、可订阅”的领域对象。这是 K8s 把”基础设施能力”当成公共服务供你复用。 你定义数据模型,平台提供所有运转机制。
14.2.1 status 子资源(重要)
注意上面 status 字段。K8s 约定:CR 的 spec(期望)由用户写,status(实际)由你的控制器写。二者通过 status 子资源分离(类似内置对象的 spec/status 分离,第 5.10 提过):用户只能改 spec,控制器更新 status。这样既清晰又安全(RBAC 可区分”改 spec”和”改 status”权限)。
14.3 Operator:把”领域知识”编码成控制器
墨:你定义了 MySQLCluster 这个对象,然后呢?它只是躺在 etcd 里的一段数据,谁去”真正建出 MySQL”?
你的 Operator(自定义控制器)。Operator = “CRD(数据模型)+ 自定义控制器(调和逻辑)“。控制器的逻辑就是你这个领域的运维知识:
func reconcile(mysqlCluster *MySQLCluster) {
desired := mysqlCluster.Spec.Replicas
actual := countActualMysqlPods(mysqlCluster)
if actual < desired {
createMysqlPod(...) // 按你的规则建(先主后从等)
}
// 更新 status
mysqlCluster.Status.ReadyReplicas = actual
updateStatus(mysqlCluster)
}Operator 的本质:把你公司里”那个最懂 MySQL 的运维脑子里的知识”,翻译成代码,让集群自动执行。 这才是 Operator 的真价值——不是”自动化”,是”把专家知识固化进系统”。
📦 案例:Prometheus Operator。 没有它,你要手动写 Deployment 跑 Prometheus、手写 ConfigMap 配抓取规则、手动挂盘存数据、手动建 Service。Prometheus Operator 定义了 Prometheus、ServiceMonitor(“要抓哪些 Service”)等 CR。你 apply 一个 ServiceMonitor,Operator 自动把抓取配置注入 Prometheus、自动发现后端 Pod、自动Reload。整套监控的”运维知识”被编码成 CR + 控制器。这就是”领域知识平台化”。
墨:你五年 Go 经验,写这个 Operator 难吗? 不难。用 client-go / controller-runtime / kubebuilder 脚手架,你定义 CRD、写 reconcile 函数,框架帮你处理 watch、informer、重试、状态缓存。你专注”调和逻辑”本身——那才是你的领域价值。一个中等复杂度的 Operator,一个 Go 工程师一两周能写出来。所以我说你来真的能写一个。
14.4 Admission Webhook:把”策略”扩展权交给你(接第 4.5)
第 4 章讲过 Admission Webhook 是”内容级策略”的可编程关卡。从”扩展”角度再强调:它是你往”请求进 etcd 之前”插逻辑的唯一标准口子。
- Mutating:注入 sidecar(Istio)、自动加默认字段、自动打标签;
- Validating:拒绝
:latest镜像、强制资源 requests、强制标签(Kyverno/Gatekeeper,第 13.7/13.10); - 转换(Conversion Webhook):CRD 多版本间的翻译(第 4.6.3)。
墨:Operator 和 Admission Webhook 各管什么? 粗分:Operator 管”让期望变成现实”(调和),Admission Webhook 管”期望能不能进来、进来长啥样”(准入)。一个是”执行层扩展”,一个是”策略层扩展”。你要自动化运维逻辑 → Operator;你要卡住/改写请求 → Webhook。
14.5 聚合层(Aggregated API):把”API 服务”扩展权交给你(接第 4.9)
第 4 章讲过 Aggregated API Server:你能起一个自己的 API Server(实现 gRPC/REST),挂在核心 API Server 后面,复用它的认证/鉴权/发现。适合”你的扩展逻辑太复杂、不适合用 CRD 表达”的场景(如要自定义存储后端、要复杂查询)。
三种扩展口子的定位对比:
- CRD + Operator:最常用,定义新对象+调和逻辑,复用 etcd/API Server,零自建服务;
- Admission Webhook:改/拒请求,策略层;
- Aggregated API:自建 API 服务,适合极复杂/特殊存储,代价最高。
墨:一般的扩展需求,先用哪个? 默认 CRD + Operator(90% 场景够)。需要卡请求用 Webhook。除非你真要”完全不同的存储/查询语义”,才上 Aggregated API。又是”优先用简单内置能力,不够再升级扩展”的取舍。
14.6 边界:什么时候不该扩展
墨:既然能扩展,那我啥都写成 CRD + Operator 行不行? 不行,扩展有成本:
- 复杂度税:每多一个 Operator,就多一个要维护、要确保高可用的组件(它挂了,对应的领域对象就不被调和);
- 调试更难:问题出在内置还是你的 Operator?排障面变宽;
- 过度抽象:有些东西本就是 Deployment + Service 能表达的,硬写成 CRD 反而绕;
- 版本/升级负担:你的 CRD 也要管废弃/兼容(第 4.6)。
何时应扩展(判别法): 只有当”这个领域对象有非平凡的、需要自动化的运维逻辑,且通用对象表达不了”时才 Operator。如果只是”一组固定 Pod + 固定配置”,用 Deployment 足矣,别为酷而 Operator。
📦 反例: 有人把”一个静态 nginx 站点”写成 NginxSite CRD + Operator,Operator 干的不过是”建个 Deployment + Service”——这层抽象纯属负担,直接用 Ingress+Deployment 更清晰。扩展是手段不是目的,当扩展不带来自动化收益时,它就是负债。
14.7 K8s 作为”元平台”的设计哲学(升华)
把全书串起来,你会发现 K8s 的扩展口子是成体系的,且都遵循同一套底层机制:
| 想扩展什么 | K8s 给的口子 | 底层复用 |
|---|---|---|
| 数据模型 | CRD | API Server + etcd + RBAC + watch |
| 自动化逻辑 | Operator(控制器) | 控制循环(第 3 章) |
| 策略/改写 | Admission Webhook | 准入链(第 4 章) |
| 调度决策 | Scheduling Framework 插件 | 调度器(第 6 章) |
| 存储 | CSI | 卷生命周期(第 10 章) |
| 网络 | CNI | Pod 网络(第 9 章) |
| 运行时 | CRI | 容器生命周期(第 8 章) |
| 自定义 API 服务 | Aggregated API | API Server 门面(第 4 章) |
墨:你看出这套设计最厉害的地方了吗?
最厉害的是——所有这些扩展,都复用同一套”声明式 API + 控制循环 + etcd 状态”的基础设施。 你写 Operator,不用自己搞存储、不用自己搞鉴权、不用自己搞 watch——全白送。K8s 把”造分布式系统的底层苦力”做成公共服务,你只填”领域逻辑”。这就是为什么它能长出生态(Istio、Prometheus、Argo、Cert-manager…全是 CRD+Operator)。
系统设计的最高境界之一:把”构建系统的系统”交给用户。 K8s 不试图内置所有功能,它内置”扩展的机制”,让社区用统一机制填功能。这比”内置 N 个功能”活得多、久得多。你做平台时也该想:我是内置功能,还是内置”扩展功能的机制”?前者快死,后者常青。
14.8 一个最小 Operator 的心智模型(给你动手用)
如果你想真写一个,心智模型就三步(框架帮你做剩下的):
- 定义 CRD:你的领域对象的 spec/status 长啥样(YAML schema);
- 写 Reconcile:拿到一个 CR 实例,比较 spec(期望) 和集群实际,做动作缩小 gap(就是第 3 章的控制循环,只不过你的领域);
- 注册 + 跑:用 kubebuilder 生成脚手架,Operator 作为一个 Deployment 跑在集群,watch 你的 CR。
墨:Reconcile 里最易错的是什么? 和第 3.11 思考题 1 一样——并发安全与幂等。多个 reconcile 同时跑、或同对象重复 reconcile,动作必须幂等(“确保有 N 个”而非”再建一个”),且用 resourceVersion 防写冲突。你写的 Operator 也是分布式系统里的一环,得守那套纪律。这正说明:你扩展了 K8s,就要遵守 K8s 的法则——这是它”一致”的原因。
14.9 本章演化线小结
- K8s 开放扩展权,把”造系统的机制”交给用户,从工具升维成平台;
- CRD:定义新资源,API Server 白送存储/RBAC/版本/watch;status 子资源分离 spec(用户)/status(控制器);
- Operator = CRD + 控制器,把领域专家知识固化进集群自动执行;
- 三种扩展口子定位:CRD+Operator(执行层)、Admission Webhook(策略层)、Aggregated API(自建服务,最重);
- 边界:只有”非平凡自动化+通用对象表达不了”才 Operator,别为酷而扩(扩展是负债当无收益);
- 所有扩展复用同一套声明式+控制循环+etcd 基础设施,这是生态繁荣的根;
- 写 Operator 也要守幂等/并发/版本纪律(你是分布式一环)。
14.10 本章思考题
🔧 思考题 1
你要管公司内部的”定时报表任务”(CronReport):每个报表有 SQL、调度时间、产出存哪。你纠结用”Deployment + CronJob”还是”Report CRD + Operator”。请用 14.6 的判别法分析,给出结论和理由。
【参考答案】 判别法:是否有”非平凡、需自动化的运维逻辑”且”通用对象表达不了”?(1) 一个报表任务 ≈ 一个 CronJob(SQL 当参数、调度用 cron 字段、产出上传用容器逻辑)。通用对象(CronJob)完全能表达,没有”通用对象表达不了”的问题。(2) 运维逻辑是否非平凡?如果只是”到点跑 SQL 存结果”,CronJob 原生就干,无额外自动化需求。除非有”报表依赖链(A 跑完 B 才能跑)""失败自动重试并告警到特定渠道""产出自动注册到 BI 系统”等跨对象编排逻辑,才需要 Operator。结论:先用 CronJob(简单、零维护)。只有当出现”多个报表间的依赖/编排/特殊生命周期”这种通用对象表达不了、且要自动化的领域逻辑时,才上 Report CRD + Operator(Operator 管依赖图、重试策略、注册)。教训:别为”有个新概念”就 CRD——先用内置对象,扩展是负债当无收益。这题考你克制扩展欲。
🔧 思考题 2
你写了一个 MySQLCluster Operator。用户 apply 了一个 spec.replicas=3 但集群只有 1 节点(资源不够)。你的 Reconcile 会怎样?operator 该怎么表现才符合”声明式控制循环”的预期?如果用户直接 kubectl edit 把 status.readyReplicas 改成 3(伪造),会发生什么?
【参考答案】 Reconcile 行为:每轮比较 spec(期望3) 和实际(实际0或1,因节点不够调度不上),actual<desired,尝试建 Pod 但被调度器 Pending,status.readyReplicas 保持真实值(如1)。操作员持续 reconcile、持续失败,Pod 一直 Pending,status 如实反映”只有1个就绪”,不伪造。这符合声明式:系统持续朝期望收敛,但期望不可达时如实报告现状(不假装达成)。用户伪造 status:因为 status 是子资源且由 operator 写,若用户有改 status 的 RBAC(通常不该给),他 edit 了 status.readyReplicas=3,但下一轮 Reconcile 会基于真实观测覆盖掉这个值(operator 重新数实际 Pod,写回真实1)。所以伪造 status 无效——这正是 status 由控制器掌握、spec 由用户掌握的分离设计的意义:用户只能改期望,不能骗系统”实际已达成”。且伪造不改实际 Pod,Service/调用方仍只看真实端点。启示:声明式系统的”真理”在实际观测,不在被人改的 status 字段;控制器每轮重写 status 保证 status 不被篡改污染。
🔧 思考题 3
Istio 用 Mutating Admission Webhook 给每个 Pod 注入 Envoy sidecar(第 8.8)。如果从”扩展口子”角度,Istio 为什么选 Admission Webhook 而不是 Operator?它能不能用 Operator 注入 sidecar?对比两种做法。
【参考答案】 Istio 注入 sidecar 是”在 Pod 创建请求进 etcd 前,自动往 spec 里加 sidecar 容器”——这是改写请求内容,正好是 Mutating Admission Webhook 的语义(第 4.5/14.4)。它不能(也不该)用 Operator 做:Operator 是”调和已存在的 CR/资源”,但 sidecar 注入发生在”Pod 还没创建”的请求修改阶段,Operator watch 不到”还没存在的 Pod”去改它。若硬用 Operator:得先让 Pod 创建、再 Operator 改 Pod spec(但运行中的 Pod spec 大多字段不可变,改不了)——所以必须在准入阶段(创建前)改。对比:(1) Webhook:在请求入口改写,Pod 一出生就带 sidecar,干净,且能基于 namespace 标签决定是否注入(灵活);(2) Operator:管已存在资源,不适合”创建前注入”。所以”注入/改写内容”用 Webhook,“管理领域对象生命周期”用 Operator,职责清晰。这题强化你对”Webhook=策略/改写层、Operator=执行/调和层”边界的理解,也说明 Istio 的 sidecar 注入是扩展机制的标准用法。
🔧 思考题 4(进阶)
K8s 把扩展做成”复用同一套声明式+etcd 基础设施”。对比另一种平台思路——“提供 SDK/库让你自己写分布式系统”(如用某个框架从零写)。从”用户认知成本、一致性保证、生态协同”三角度,评这两种平台哲学。
【参考答案】 (1) 认知成本:K8s 方式用户只需学”声明式+调和”一套范式,写 Operator 复用现成基础设施(存储/RBAC/watch 白送),认知集中。SDK 方式用户要从零设计存储、一致性、watch、HA——认知分散、门槛高。(2) 一致性保证:K8s 方式所有扩展跑在同一套 level-triggered 控制循环 + etcd 强一致上,行为可预期、互斥由 resourceVersion 保证,扩展间通过状态板协作而非直接调用,天然一致。SDK 方式每个扩展自己实现一致性,质量参差,易出”两个扩展互相冲突”的坑。(3) 生态协同:K8s 方式所有 CRD/Operator 用统一 API、统一 kubectl、统一 RBAC、统一 watch,能互相组合(Prometheus Operator 的 ServiceMonitor 能被别的 Operator 消费),生态协同强。SDK 方式各自为政,接口不统一,难互操作。代价:K8s 方式受限于”必须套进声明式+CRD 模型”,极特殊的系统(如需自定义一致性模型、超低延迟)可能套不进,这时 SDK/自建更合适。结论:K8s 哲学胜在”用统一底层约束换生态与一致性”,适合”大多数云原生有状态/无状态负载”;SDK 哲学胜在”灵活不受模型限”,适合”特殊系统”。选平台的哲学时,问自己:我要的是”统一的生态”还是”无拘的灵活”?K8s 赌前者,且赌赢了(生态繁荣为证)。
14.11 小结
- K8s 开放扩展权,把”造系统的机制”交用户,从工具升维平台;
- CRD 定义新资源,API Server 白送存储/RBAC/版本/watch;status 子资源分离用户期望与控制器实际;
- Operator = CRD + 控制器,把领域专家知识固化进集群自动执行(你真能写);
- 扩展口子三定位:Operator(执行层)/Webhook(策略层)/Aggregated API(自建服务最重);
- 边界:只有”非平凡自动化+通用表达不了”才 Operator,扩展无收益即是负债;
- 所有扩展复用同一套声明式+控制循环+etcd,是生态繁荣根;写 Operator 也要守幂等/并发/版本纪律。
下一章进资源管理与 QoS——cgroups、requests/limits、OOM、节点驱逐。这块直接决定”你的 Pod 为什么被杀了”,也是第 6、15 章多次埋的”不写 requests 的坑”的总清算。