Kubernetes Operator与CRD生产开发实战

Operator 的价值不是“把脚本放进容器”,而是把领域知识实现为持续工作的控制循环:观察期望状态和实际状态,执行幂等动作,记录状态,并在失败后安全重试。设计良好的 Operator 能让安装、扩容、备份、升级和故障恢复具有一致入口;设计不当则会制造一个拥有高权限、不断重试且难以停下的自动化风险源。

本文面向准备使用 Kubebuilder、controller-runtime 或 Operator SDK 开发生产控制器的团队。示例聚焦设计方法和运行保障,具体依赖版本与生成命令应以当前官方文档为准。

一、先判断是否真的需要Operator

适合 Operator 的场景:

  • 对象有持续生命周期,而不是一次性执行;
  • 需要根据外部系统或多个Kubernetes资源持续收敛;
  • 扩缩容、升级、备份、恢复等领域动作有明确状态机;
  • 多个团队反复执行同类操作,需要统一安全边界;
  • 需要把状态、条件和事件暴露给平台与自动化系统。

若需求只是部署一组静态资源,Helm或Kustomize通常更简单;若只是定时运行命令,CronJob可能足够;若外部系统已有成熟API和工作流平台,也不必强行映射为CRD。

二、从用户API而不是控制器代码开始

CRD 是长期兼容的产品接口。先写使用示例和状态输出,再写Go代码:

apiVersion: database.example.com/v1alpha1
kind: DatabaseCluster
metadata:
  name: orders-db
spec:
  version: "16.4"
  replicas: 3
  storage:
    size: 500Gi
    className: fast-rwo
  backup:
    schedule: "0 2 * * *"
    retention: 14
  maintenance:
    window: "Sun 02:00-04:00"

spec 表达用户期望,status 表达控制器观察到的现实。不要让用户填写可以推导的内部名称、Pod IP或临时步骤;不要把密码写在spec。字段一旦发布就很难删除,应为默认值、校验、升级路径和弃用策略预留空间。

三、设计可机器判断的Status和Conditions

状态至少包含:

  • observedGeneration:控制器已处理的spec版本;
  • conditions:Available、Progressing、Degraded等条件;
  • 当前与期望版本、当前副本和就绪副本;
  • 最近成功备份、失败原因和可重试时间;
  • 外部资源标识,但不包含密钥。

条件应使用 True/False/Unknown,包含 reason、可读 message 和转换时间。同一类型保留最新条件,不要每次循环无限追加。调用方只有在 observedGeneration == metadata.generation 时,才能确认状态对应当前spec。

四、Reconcile必须幂等

协调循环不能假设事件只到达一次,也不能依赖固定顺序。典型结构:

func (r *DatabaseClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    var db databasev1alpha1.DatabaseCluster
    if err := r.Get(ctx, req.NamespacedName, &db); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    if !db.DeletionTimestamp.IsZero() {
        return r.reconcileDelete(ctx, &db)
    }

    if err := r.ensureFinalizer(ctx, &db); err != nil {
        return ctrl.Result{}, err
    }
    if err := r.reconcileService(ctx, &db); err != nil {
        return r.fail(ctx, &db, "ServiceError", err)
    }
    if err := r.reconcileStatefulSet(ctx, &db); err != nil {
        return r.fail(ctx, &db, "WorkloadError", err)
    }
    return r.reconcileStatus(ctx, &db)
}

ensure 的含义是读取现状、计算差异、只修改必要字段。外部API调用应携带幂等键;创建前先查询,超时后不能直接假设失败,否则重试可能创建重复资源。

五、建立明确状态机

复杂流程不要依赖“当前有哪些资源”隐式推断全部阶段。可设计:Pending→Provisioning→Ready→Upgrading→Ready,失败进入Degraded但保留可恢复信息。每个迁移步骤应定义:

  • 进入条件和前置检查;
  • 可重复执行的动作;
  • 成功证据;
  • 超时和重试策略;
  • 中断后的恢复点;
  • 回滚条件和不可逆边界。

升级数据库、迁移存储等操作应由显式字段或审批触发,不要因为镜像默认值变化就自动执行破坏性升级。

六、正确使用OwnerReference与Finalizer

集群内从属对象可设置 controller owner reference,让垃圾回收器在父对象删除后清理。跨命名空间对象和外部云资源不能只依赖它,需要 finalizer 执行删除前动作。

finalizer流程:

  1. 对象正常且没有finalizer时先添加;
  2. 发现 deletionTimestamp 后停止创建新资源;
  3. 幂等清理外部资源并验证结果;
  4. 清理成功后移除finalizer;
  5. 永久失败时暴露条件、事件和人工处置方法。

外部服务长时间不可用会导致对象卡在Terminating。必须提供受审计的强制接管流程,并明确强制移除finalizer可能遗留的资源。

七、避免更新冲突与字段争夺

多个控制器、GitOps和用户可能同时修改对象。建议:

  • 使用Patch或Server-Side Apply,只拥有必要字段;
  • 更新status时使用status子资源;
  • 冲突后重新读取对象并重新计算,而非覆盖最新数据;
  • 不改写用户未授权给Operator管理的字段;
  • 子资源名称和标签稳定,便于恢复关联;
  • 使用哈希注解触发配置变化,而不是无条件更新。

若每次reconcile都修改时间戳或无意义字段,会形成自触发热循环,拖垮API Server。

八、Watch、索引和队列设计

主资源变化会触发协调,从属资源和外部事件也可能触发。只监听真正影响期望状态的事件,并使用predicate过滤status或无关字段变化。

需要反向查找时建立字段索引,例如从Secret找到引用它的所有自定义资源,避免每次全量List。工作队列应保留指数退避;已知等待条件可用 RequeueAfter,但不要把控制器变成秒级轮询器。大规模集群需测量单次协调耗时、队列深度和API调用量。

九、校验、默认值与转换Webhook

简单字段约束优先使用OpenAPI schema:枚举、范围、长度和必填关系。跨字段复杂校验再考虑CEL或验证webhook。默认值应稳定、可解释,避免升级控制器后同一旧清单得到不同结果。

当API从v1alpha1演进到v1beta1或v1时:

  • 选择一个storage version;
  • 定义双向转换并覆盖所有字段;
  • 验证往返转换不会丢数据;
  • 先让控制器同时理解新旧版本;
  • 迁移存量对象后再停止旧版本服务;
  • 监控仍在调用旧版本的客户端。

转换webhook属于API读取和写入路径,应按控制面关键服务建设高可用、证书和故障降级。

十、权限必须按控制器实际动作收敛

生成的RBAC标记只是起点。检查控制器是否真的需要list全部Secret、修改所有Namespace或创建ClusterRole。按API组、资源、动词和命名空间缩小权限;外部云权限使用工作负载身份,避免静态长效密钥。

分离:

  • 控制器运行权限;
  • 用户创建和修改CR的权限;
  • 高风险动作如恢复、主从切换、删除备份的审批权限;
  • 只读观察和审计权限。

Controller Pod本身启用非root、只读根文件系统、seccomp、网络策略和镜像签名验证。

十一、Leader Election与高可用

生产通常运行多个副本并启用leader election,避免单实例故障。leader切换后,新的实例会重新处理队列,因此所有动作必须幂等。租约参数不能只追求快速切换:API延迟或短暂网络抖动可能造成频繁换主。

多个副本不等于容量自动提升;同一控制器通常只有leader执行协调。需要提高吞吐时,调整最大并发协调数、索引和外部API限流,并验证目标系统能够承受并发。

十二、外部系统调用的可靠性

调用云API、DNS、数据库或工单系统时:

  • 设置连接、请求和总流程超时;
  • 仅对可恢复错误重试,并使用退避与抖动;
  • 使用幂等键和操作ID;
  • 限制每租户并发,防止一个故障对象占满队列;
  • 对429和配额错误单独观测;
  • 将外部操作状态写入status,而非只写日志;
  • 不在内存中保存唯一进度,Pod重启后必须可恢复。

对不可逆操作采用两阶段确认或显式维护窗口。

十三、可观测性设计

建议暴露:

  • reconcile总次数、结果、耗时和错误类型;
  • 工作队列深度、等待时间、重试次数和最老任务年龄;
  • 各状态和条件的自定义资源数量;
  • 外部API延迟、错误、限流和配额;
  • leader状态、缓存同步和webhook延迟;
  • 资源创建、升级、备份和删除的业务SLO。

日志使用资源命名空间、名称、UID、generation、reconcile ID和操作ID关联,但不输出Secret。Event适合用户可见的关键转折,不适合每次循环都写。

十四、测试金字塔

  1. 纯函数单测:默认值、差异计算、状态机和错误分类;
  2. envtest:真实API schema、webhook、status、finalizer和RBAC行为;
  3. 集成测试:真实控制器与模拟外部API;
  4. 临时集群端到端:安装、升级、备份、恢复和卸载;
  5. 故障注入:API超时、Pod重启、leader切换、冲突、限流和部分成功。

测试必须覆盖协调被重复调用、在任意步骤重启、对象在处理中被修改和删除。只验证“最终创建了Pod”远远不够。

十五、发布与升级

CRD通常先于新控制器发布,但必须保证旧控制器不会误解新字段。推荐顺序:

  1. 备份CR和CRD;
  2. 安装向后兼容的CRD;
  3. 部署支持新旧版本的控制器;
  4. 小范围创建新版本对象;
  5. 迁移存量并观察错误和条件;
  6. 完成存储版本迁移;
  7. 最后弃用旧API。

镜像必须可回退,CRD变更和数据迁移通常不能仅靠回滚Deployment解决。每次发布记录兼容矩阵和不可逆步骤。

十六、常见事故与处置

热循环导致API Server压力升高

检查控制器是否每次都更新对象、Watch了自身status、忽略resourceVersion冲突或使用极短RequeueAfter。先降并发或暂停单个控制器,再修复差异判断。

大量对象卡在删除

检查finalizer、外部API和控制器权限。不要批量强删;先恢复控制器或外部依赖,按对象确认遗留资源,再审计移除。

升级后状态不再推进

核对CRD storedVersions、转换webhook、缓存反序列化和新控制器日志。必要时恢复兼容控制器,不要在不清楚转换结果时重写全部对象。

控制器误删资源

立即停止协调或收紧权限,保留审计日志和对象快照,确认OwnerReference、标签选择器和删除状态机。恢复后先以只读或单对象模式验证。

十七、生产验收清单

  • [ ] 使用场景确实需要持续控制循环;
  • [ ] spec/status边界清楚,API有版本与弃用计划;
  • [ ] reconcile、外部调用和删除流程均幂等;
  • [ ] status包含observedGeneration和标准conditions;
  • [ ] finalizer故障有受审计的处置流程;
  • [ ] RBAC和云权限已按最小权限收敛;
  • [ ] webhook、leader election和证书有高可用监控;
  • [ ] 队列、协调耗时、错误和外部依赖可观测;
  • [ ] 任意步骤重启、冲突和重复事件通过测试;
  • [ ] CRD升级、存储迁移和回滚经过演练。

十八、官方参考

一个可信的 Operator 应像优秀值班工程师:反复执行也安全,遇到未知情况会停下并说明原因,操作过程可追溯,恢复路径明确,而不是拥有权限后不停尝试。

Copyright © yan-jian.com 2023 - 2026 · Powered by GitBook更新时间: 2026-10-21 09:44:38

results matching ""

    No results matching ""