一、为什么需要 Operator?

在云原生的世界里,StatefulSet、Deployment 等原语已经能够很好地处理无状态应用。但当我们需要运维一个有状态服务时——比如部署一套高可用的 Kafka 集群、管理 PostgreSQL 的主从切换、自动修复 etcd 的成员故障——仅仅靠声明式的 YAML 配置就力不从心了。

Kubernetes Operator 模式正是为了解决这个问题而诞生的。它的核心思想是:用代码封装人类运维专家的知识,让 Kubernetes 能够自动化地完成复杂应用的部署、扩缩容、升级、备份、故障恢复等 Day 2 运维操作。

Operator 最初由 CoreOS 团队在 2016 年提出,经过十年的演进,它已经成为云原生领域管理有状态应用的事实标准。从 Prometheus Operator 到 TiDB Operator,从 Cert-Manager 到 Istio Operator,几乎所有生产级有状态云原生项目都提供了自己的 Operator。

二、核心概念:CRD + Controller = 控制器模式

Kubernetes Operator 本质上是自定义资源(CRD)控制器(Controller)的组合。

2.1 自定义资源定义(CRD)

CRD 允许你扩展 Kubernetes API,定义领域专属的资源。例如,你可以定义一个 PostgreSQLCluster 资源,用来描述一个 PostgreSQL 集群的完整拓扑:

apiVersion: database.example.com/v1
kind: PostgreSQLCluster
metadata:
  name: orders-db
spec:
  version: "16.3"
  replicas: 3
  storageSize: 100Gi
  backup:
    enabled: true
    schedule: "0 2 * * *"
    retention: "7d"
  resources:
    cpu: "4"
    memory: 8Gi
  postgresql:
    maxConnections: 200
    sharedBuffers: "2GB"

这样的资源声明让运维人员可以用声明式的方式管理数据库集群,而不需要关心底层是怎么创建 PVC、配置 Patroni、设置流复制的。

2.2 控制器与调谐循环

Controller 是 Operator 的大脑。它实现了一个无限循环,不断地将"期望状态"与"实际状态"进行对比,并采取行动使两者趋于一致。这个循环被称为 Reconciliation Loop(调谐循环)

┌─────────────────────────────────────────────────────┐
│                Reconciliation Loop                   │
│                                                     │
│  观察 (Watch) → 对比 (Diff) → 行动 (Act) → 重复    │
│                                                     │
│  1. Watch CRD 事件(Add/Update/Delete)              │
│  2. 读取期望状态(spec)                              │
│  3. 获取实际状态(集群拓扑)                          │
│  4. 计算差值并执行变更                                │
│  5. 更新 status 子资源                                │
│  6. 等待下一个事件或定期重试                          │
└─────────────────────────────────────────────────────┘

关键原则是幂等性:调谐循环可能因为任何原因被重复触发(重启、网络抖动、重新排队),所以你的 reconcile 函数必须保证多次调用的结果一致。

三、Operator SDK 工具链概览

目前构建 Operator 主流有三个工具链,各有侧重:

工具链语言适用场景
KubebuilderGo纯 Go 生态,CNCF 项目推荐标准
Operator SDK (Go)Go集成 OLM 打包,支持 ansible/helm
Operator SDK (Ansible)Ansible运维团队无需写 Go,用 Playbook 即可
KUDOYAML极简声明式 Operator,适合简单应用
Java Operator SDKJavaJava 团队生态

本文以 Kubebuilder 为例,因为它是目前云原生社区最推荐的标准工具链,CNCF 项目 kubevela、cluster-api 等都使用它。

四、实战:用 Kubebuilder 构建 PostgreSQL 集群 Operator

4.1 环境准备与项目初始化

# 安装 kubebuilder
curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)
chmod +x kubebuilder && mv kubebuilder /usr/local/bin/

# 初始化项目
mkdir pg-operator && cd pg-operator
kubebuilder init --domain example.com --repo github.com/example/pg-operator

# 创建 API(CRD + Controller)
kubebuilder create api --group database --version v1 --kind PostgreSQLCluster

这个命令会生成:

  • api/v1/postgresqlcluster_types.go — CRD Go 类型定义
  • controllers/postgresqlcluster_controller.go — 控制器骨架
  • config/crd/bases/ — CRD YAML 定义
  • config/rbac/ — RBAC 权限配置
  • config/samples/ — 示例 CR 资源

4.2 定义 CRD Spec 与 Status

// api/v1/postgresqlcluster_types.go

package v1

import (
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// PostgreSQLClusterSpec 定义期望状态
type PostgreSQLClusterSpec struct {
    // +kubebuilder:validation:Enum=14;15;16
    Version string `json:"version"`

    // +kubebuilder:default=1
    // +kubebuilder:validation:Minimum=1
    // +kubebuilder:validation:Maximum=9
    Replicas int32 `json:"replicas"`

    // +kubebuilder:validation:Pattern=`^\d+(Gi|Mi)$`
    StorageSize string `json:"storageSize,omitempty"`

    Resources ResourceRequirements `json:"resources,omitempty"`

    Backup *BackupSpec `json:"backup,omitempty"`

    Postgresql PostgresqlConfig `json:"postgresql,omitempty"`
}

type BackupSpec struct {
    Enabled bool   `json:"enabled"`
    Schedule string `json:"schedule,omitempty"`
    Retention string `json:"retention,omitempty"`
}

type PostgresqlConfig struct {
    MaxConnections  int `json:"maxConnections,omitempty"`
    SharedBuffers   string `json:"sharedBuffers,omitempty"`
}

// PostgreSQLClusterStatus 定义实际状态
type PostgreSQLClusterStatus struct {
    // +patchMergeKey=type
    // +patchStrategy=merge
    Conditions []metav1.Condition `json:"conditions,omitempty"`

    ReadyReplicas int32 `json:"readyReplicas,omitempty"`

    Phase ClusterPhase `json:"phase,omitempty"`

    LastBackupTime *metav1.Time `json:"lastBackupTime,omitempty"`
}

type ClusterPhase string

const (
    PhasePending   ClusterPhase = "Pending"
    PhaseCreating  ClusterPhase = "Creating"
    PhaseRunning   ClusterPhase = "Running"
    PhaseDegraded  ClusterPhase = "Degraded"
    PhaseUpgrading ClusterPhase = "Upgrading"
)

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Namespaced,shortName=pg
// +kubebuilder:printcolumn:name="Version",type=string,JSONPath=".spec.version"
// +kubebuilder:printcolumn:name="Replicas",type=integer,JSONPath=".spec.replicas"
// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=".status.phase"
// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=".metadata.creationTimestamp"
type PostgreSQLCluster struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`

    Spec   PostgreSQLClusterSpec   `json:"spec,omitempty"`
    Status PostgreSQLClusterStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true
type PostgreSQLClusterList struct {
    metav1.TypeMeta `json:",inline"`
    metav1.ListMeta `json:"metadata,omitempty"`
    Items           []PostgreSQLCluster `json:"items"`
}

func init() {
    SchemeBuilder.Register(&PostgreSQLCluster{}, &PostgreSQLClusterList{})
}

注意上方的 // +kubebuilder:... 注释标记(称为 Markers),它们指导代码生成器自动生成 CRD YAML、RBAC 规则等。+kubebuilder:subresource:status 会启用 /status 子资源端点,使得 status 更新独立于 spec 更新,避免冲突。

4.3 实现 Reconcile 逻辑

// controllers/postgresqlcluster_controller.go

package controllers

import (
    "context"
    "fmt"

    "k8s.io/apimachinery/pkg/api/errors"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/runtime"
    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/client"
    "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    "sigs.k8s.io/controller-runtime/pkg/log"

   appsv1 "k8s.io/api/apps/v1"
    corev1 "k8s.io/api/core/v1"

    databasev1 "github.com/example/pg-operator/api/v1"
)

const (
    finalizerName = "postgresqlcluster.database.example.com/finalizer"
)

type PostgreSQLClusterReconciler struct {
    client.Client
    Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=database.example.com,resources=postgresqlclusters,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=database.example.com,resources=postgresqlclusters/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=database.example.com,resources=postgresqlclusters/finalizers,verbs=update
// +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups="",resources=services;configmaps;persistentvolumeclaims,verbs=get;list;watch;create;update;patch;delete

func (r *PostgreSQLClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := log.FromContext(ctx)

    // 1. 获取 CR 实例
    pgCluster := &databasev1.PostgreSQLCluster{}
    if err := r.Get(ctx, req.NamespacedName, pgCluster); err != nil {
        if errors.IsNotFound(err) {
            // CR 已被删除,跳过
            return ctrl.Result{}, nil
        }
        return ctrl.Result{}, err
    }

    // 2. 处理 Finalizer(删除保护)
    if !pgCluster.DeletionTimestamp.IsZero() {
        // 正在执行删除逻辑
        return r.reconcileDelete(ctx, pgCluster)
    }

    if !controllerutil.ContainsFinalizer(pgCluster, finalizerName) {
        controllerutil.AddFinalizer(pgCluster, finalizerName)
        if err := r.Update(ctx, pgCluster); err != nil {
            return ctrl.Result{}, err
        }
        return ctrl.Result{Requeue: true}, nil
    }

    // 3. 更新 Phase 为 Creating
    if pgCluster.Status.Phase == "" {
        pgCluster.Status.Phase = databasev1.PhaseCreating
        if err := r.Status().Update(ctx, pgCluster); err != nil {
            return ctrl.Result{}, err
        }
    }

    // 4. 创建/更新 ConfigMap(PostgreSQL 配置)
    if err := r.reconcileConfigMap(ctx, pgCluster); err != nil {
        return ctrl.Result{}, err
    }

    // 5. 创建/更新 Headless Service(网络身份)
    if err := r.reconcileService(ctx, pgCluster); err != nil {
        return ctrl.Result{}, err
    }

    // 6. 创建/更新 StatefulSet(核心工作负载)
    if err := r.reconcileStatefulSet(ctx, pgCluster); err != nil {
        return ctrl.Result{}, err
    }

    // 7. 检查备份 CronJob
    if pgCluster.Spec.Backup != nil && pgCluster.Spec.Backup.Enabled {
        if err := r.reconcileBackup(ctx, pgCluster); err != nil {
            return ctrl.Result{}, err
        }
    }

    // 8. 更新状态
    if err := r.updateStatus(ctx, pgCluster); err != nil {
        return ctrl.Result{}, err
    }

    log.Info("Reconciled PostgreSQLCluster", "name", pgCluster.Name, "phase", pgCluster.Status.Phase)
    return ctrl.Result{}, nil
}

// reconcileStatefulSet 调谐 StatefulSet 到期望状态
func (r *PostgreSQLClusterReconciler) reconcileStatefulSet(
    ctx context.Context, pgCluster *databasev1.PostgreSQLCluster) error {

    sts := &appsv1.StatefulSet{}
    err := r.Get(ctx, client.ObjectKey{
        Name:      pgCluster.Name,
        Namespace: pgCluster.Namespace,
    }, sts)

    if errors.IsNotFound(err) {
        // 创建新的 StatefulSet
        desired := r.desiredStatefulSet(pgCluster)
        if err := controllerutil.SetOwnerReference(pgCluster, desired, r.Scheme); err != nil {
            return err
        }
        return r.Create(ctx, desired)
    } else if err != nil {
        return err
    }

    // 更新已有的 StatefulSet(handle 版本升级)
    needsUpdate := false
    if *sts.Spec.Replicas != pgCluster.Spec.Replicas {
        sts.Spec.Replicas = &pgCluster.Spec.Replicas
        needsUpdate = true
    }
    // 检查镜像版本是否变更
    // ...

    if needsUpdate {
        return r.Update(ctx, sts)
    }
    return nil
}

// updateStatus 从实际资源中采集状态
func (r *PostgreSQLClusterReconciler) updateStatus(
    ctx context.Context, pgCluster *databasev1.PostgreSQLCluster) error {

    sts := &appsv1.StatefulSet{}
    err := r.Get(ctx, client.ObjectKeyFromObject(pgCluster), sts)
    if err != nil {
        return err
    }

    pgCluster.Status.ReadyReplicas = sts.Status.ReadyReplicas

    // 判断 Phase
    if sts.Status.ReadyReplicas == pgCluster.Spec.Replicas {
        pgCluster.Status.Phase = databasev1.PhaseRunning
        pgCluster.Status.Conditions = []metav1.Condition{
            {
    Type:   "Ready",
    Status: metav1.ConditionTrue,
    Reason: "AllReplicasReady",
    Message: fmt.Sprintf("%d/%d replicas ready",
        sts.Status.ReadyReplicas, pgCluster.Spec.Replicas),
    LastUpdateTime:  metav1.Now(),
    },
        }
    } else {
        pgCluster.Status.Phase = databasev1.PhaseDegraded
    }

    return r.Status().Update(ctx, pgCluster)
}

// reconcileDelete 处理 Finalizer 和清理逻辑
func (r *PostgreSQLClusterReconciler) reconcileDelete(
    ctx context.Context, pgCluster *databasev1.PostgreSQLCluster) (ctrl.Result, error) {

    log := log.FromContext(ctx)
    log.Info("Deleting PostgreSQLCluster", "name", pgCluster.Name)

    // 1. 执行预删除操作:上传最后的全量备份等
    // ...

    // 2. 移除 Finalizer,允许 Kubernetes 级联删除所有子资源
    controllerutil.RemoveFinalizer(pgCluster, finalizerName)
    if err := r.Update(ctx, pgCluster); err != nil {
        return ctrl.Result{}, err
    }
    return ctrl.Result{}, nil
}

func (r *PostgreSQLClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
    return ctrl.NewControllerManagedBy(mgr).
        For(&databasev1.PostgreSQLCluster{}).
        Owns(&appsv1.StatefulSet{}).
        Owns(&corev1.Service{}).
        Owns(&corev1.ConfigMap{}).
        WithOptions(controller.Options{
            MaxConcurrentReconciles: 3,
        }).
        Complete(r)
}

4.4 生成 CRD YAML 与部署

# 生成 CRD 清单(基于 types.go 中的 Markers)
make manifests

# 生成代码(DeepCopy 方法等)
make generate

# 本地验证(需要 Docker)
kind create cluster
make install  # 将 CRD 安装到集群
make run      # 在本地运行 Controller

# 构建 Docker 镜像并部署
make docker-build docker-push IMG=registry.example.com/pg-operator:v0.1.0
make deploy IMG=registry.example.com/pg-operator:v0.1.0

# 创建示例资源
kubectl apply -f config/samples/database_v1_postgresqlcluster.yaml

五、生产级 Operator 最佳实践

5.1 Finalizer 与级联删除

Finalizer 是 Operator 的重要安全网。当 CR 被删除请求时,Kubernetes 不会立即删除它,而是设置 DeletionTimestamp,等你的 Controller 完成所有清理逻辑、移除 Finalizer 后,才会真正删除。这在诸如"删除前上传备份"、"从集群协调组中注销"等场景至关重要。

5.2 OwnerReference 与垃圾回收

所有 Operator 创建的子资源(StatefulSet、Service、ConfigMap)都应该设置 OwnerReference,指向父 CR。这样当 CR 被删除时,Kubernetes GC 会自动清理所有子资源,无需手动遍历删除。使用 controller-runtime 的 SetOwnerReference()SetControllerReference() 即可完成。

5.3 状态管理:Status Subresource + Conditions

生产 Operator 使用 Subresource Status,并用 conditions 数组描述资源状态,遵循 Kubernetes API 惯例:

status:
  conditions:
  - type: Ready
    status: "True"
    reason: AllReplicasReady
    message: "3/3 replicas ready"
    lastTransitionTime: "2026-09-19T08:00:00Z"
  - type: BackupRunning
    status: "True"
    reason: CronJobActive
    lastTransitionTime: "2026-09-19T07:00:00Z"
  phase: Running
  readyReplicas: 3
  lastBackupTime: "2026-09-19T02:00:00Z"

这种模式的优点是被其他 Controller 或 kubectl 可以直观判断资源状态,kubectl get pg 也能直接看到。

5.4 重试策略与指数退避

Service 创建后 DNS 名称短暂不可用、镜像拉取慢、PVC 绑定延迟——这些都会让 reconcile 失败。Controller-runtime 默认使用指数退避重试,你也可以用 ctrl.Result{RequeueAfter: time.Minute} 做优雅的定时重试。

5.5 Leader Election 与高可用部署

生产环境 Operator 应多副本部署,但只有一个活跃的 Controller 在执行 reconcile。Controller-runtime 内置了 Leader Election 支持,基于 Kubernetes Lease 对象:

# 部署配置中启用 leader election
args:
  - --leader-elect
  - --leader-elect-namespace=pg-operator-system

5.6 可观测性:Metrics + Events

Controller-runtime 自动暴露 Prometheus 指标(reconcile 次数、耗时、错误率),你还需要通过 Recorder 发送 Kubernetes Event,让 kubectl describe 能展示有意义的事件历史:

func (r *PostgreSQLClusterReconciler) reconcileStatefulSet(...) error {
    ...
    r.Recorder.Eventf(pgCluster, corev1.EventTypeNormal,
        "StatefulSetCreated", "Created StatefulSet %s", sts.Name)
    ...
}

// 暴露自定义指标
reconcileTotal = prometheus.NewCounterVec(prometheus.CounterOpts{
    Name: "pg_operator_reconcile_total",
    Help: "Total number of reconciles",
}, []string{"result"})

5.7 并发控制

通过 MaxConcurrentReconciles 控制同时处理多少个资源,避免在大量 CR 场景下压垮 API Server 或底层基础设施。默认值为 1,对大多数场景够用,但对高吞吐场景需要调高。

六、测试:envtest 与集成测试

Kubebuilder 内置了 envtest 支持——它会临时下载 kube-apiserver 和 etcd 二进制文件,在一个真实 API Server(但没有真实节点)的环境中进行测试:

func TestReconcileStatefulSetCreation(t *testing.T) {
    // 使用 envtest 环境
    testEnv := &envtest.Environment{
        CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
    }
    cfg, _ := testEnv.Start()
    defer testEnv.Stop()

    scheme := runtime.NewScheme()
    databasev1.AddToScheme(scheme)

    k8sClient, _ := client.New(cfg, client.Options{Scheme: scheme})
    reconciler := &PostgreSQLClusterReconciler{
        Client: k8sClient,
        Scheme: scheme,
    }

    // 创建 CR
    pg := &databasev1.PostgreSQLCluster{
        ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
        Spec:       databasev1.PostgreSQLClusterSpec{Version: "16", Replicas: 2},
    }
    k8sClient.Create(context.TODO(), pg)

    // 触发 Reconcile
    _, err := reconciler.Reconcile(context.TODO(),
        ctrl.Request{NamespacedName: types.NamespacedName{Name: "test", Namespace: "default"}})

    // 验证 StatefulSet 被创建
    sts := &appsv1.StatefulSet{}
    err = k8sClient.Get(context.TODO(), types.NamespacedName{
        Name: "test", Namespace: "default"}, sts)
    Expect(err).NotTo(HaveOccurred())
    Expect(*sts.Spec.Replicas).To(Equal(int32(2)))
}

七、Operator Lifecycle Manager(OLM)

对于需要分发给其他团队的 Operator,OLM 提供了标准化的安装、升级、权限管理流程。OLM 使用 ClusterServiceVersion(CSV) 描述 Operator 的元数据,并自动生成 UI 界面。

使用 operator-sdk 工具链可以直接打包 OLM Bundle:

# 生成 OLM Bundle 元数据
make bundle IMG=registry.example.com/pg-operator:v0.1.0

# 构建 Bundle 镜像
make bundle-build bundle-push BUNDLE_IMG=registry.example.com/pg-operator-bundle:v0.1.0

# 在集群中安装(需要 OLM)
operator-sdk run bundle registry.example.com/pg-operator-bundle:v0.1.0

八、与已有云原生技术栈的集成

在生产级 Operator 部署中,它将自然与你已有的云原生工具链集成:

  • Prometheus + Grafana:Operator 暴露 metrics 端点,由 ServiceMonitor 自动抓取
  • Helm:Operator 内部可以不使用自己的 Helm Chart,但 OLM 提供了更高级的生命周期管理
  • ArgoCD / Flux:GitOps 管理 CR 资源,Operator 响应变化并执行实际操作
  • Istio Service Mesh:有状态服务的 mTLS、流量镜像、灰度发布都可以通过 Operator 自动化关联
  • Velero:备份恢复策略可以由 Operator 在创建集群时自动配置

九、总结

Kubernetes Operator 模式将"运维专家的知识"编码为可执行的自动化逻辑,是管理有状态云原生应用的终极武器。它的核心并不复杂——就是一个不断调谐期望状态与实际状态的控制循环——但在工程实践中涉及的 CRD 设计、Finalizer 管理、状态观测、高可用部署、测试验证等细节非常多。

作为 2026 年云原生开发者的必备技能,理解并掌握 Operator 模式不仅能帮助你构建更健壮的云平台基础设施,也能让你在设计任何复杂分布式系统时拥有"控制器思维":持续观察、自动调谐、始终向期望状态收敛。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部