把一个无状态的 Web 服务搬进 Kubernetes 很容易,写一个 Deployment 加一个 Service 就完事了。但如果要管理的是 MySQL 主从集群、Kafka 集群或者 etcd,事情就完全不一样了:每个实例有自己持久化的数据,节点之间有明确的角色分工,故障恢复需要先选出新主库再重搭从库,这些操作原本都需要经验丰富的 DBA 按手册一步步执行。Operator 模式的出现,就是要把这些运维知识写成代码,交给 Kubernetes 自动完成。

为什么有状态应用需要 Operator 模式
Kubernetes 原生的 Workload 资源中,Deployment 和 ReplicaSet 面向的是无状态应用,所有副本可以随意替换。StatefulSet 虽然解决了稳定网络标识和持久存储的问题,让每个 Pod 拥有固定的名字(如 mysql-0、mysql-1)和独立的 PVC,但它只解决了“部署形态”的问题,没有解决“运维逻辑”的问题。
举个例子,一个三节点的 MySQL 主从集群,当主库 Pod 所在节点宕机时,StatefulSet 只会重建这个 Pod,但它不会知道新启动的 mysql-0 需要重新执行数据同步,更不会触发主从切换、通知所有客户端更新连接地址。这些领域知识(Domain Knowledge)超出了内置控制器的能力范围,必须由了解这个应用的人来编码实现,这正是 Operator 存在的意义。
Operator 本质上是两类组件的组合:一是 CRD(CustomResourceDefinition),用来在 Kubernetes 中定义一种新的资源类型,比如 MySQLCluster;二是一个自定义控制器,它持续监听这类资源的变更,并对比“期望状态”和“实际状态”,不断执行操作让实际状态逼近期望状态。这个思路和内置控制器完全一致,只是控制的对象换成了特定应用。
Operator 的核心工作原理
理解 Operator 的关键在于理解控制循环(Reconcile Loop)。控制器进程会通过 Watch 机制订阅 CRD 实例以及相关依赖资源(Pod、Service、ConfigMap 等)的变化事件,一旦发现变化就触发调谐函数。调谐函数不关心事件的具体内容,而是每次都完整地读取期望状态,观察集群的实际状态,然后决定需要执行的动作。
func (r *MySQLClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// 1. 读取期望状态:获取 CRD 中声明的 MySQLCluster 实例
var cluster appv1.MySQLCluster
if err := r.Get(ctx, req.NamespacedName, &cluster); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 2. 观察实际状态:检查当前集群里已经存在的 StatefulSet
var sts appsv1.StatefulSet
err := r.Get(ctx, req.NamespacedName, &sts)
// 3. 计算差异并执行操作:副本数不一致则扩容
if err != nil || *sts.Spec.Replicas != cluster.Spec.Replicas {
if err := r.reconcileStatefulSet(ctx, &cluster); err != nil {
return ctrl.Result{}, err
}
}
// 4. 检查各节点健康状态,必要时触发主从切换
if err := r.checkAndFailover(ctx, &cluster); err != nil {
return ctrl.Result{}, err
}
// 5. 定期重新调谐,保证能发现节点宕机等无事件的变化
return ctrl.Result{RequeueAfter: 30 * time.Second}, nil
}这种“水平触发”而非“边缘触发”的设计让 Operator 具备很强的自愈能力。就算控制器重启错过了一些事件,下一次调谐依然会把状态修正回来。同时要注意,调谐函数必须写成幂等的,因为同一个事件可能触发多次调谐,任何操作都要能安全地重复执行。
另一个重要概念是 Finalizer。当用户删除 CRD 实例时,Kubernetes 会给对象打上 deletion timestamp,此时如果对象上存在 Finalizer 字段,删除会被阻塞,直到控制器清理完外部资源(比如删除云上的数据库实例、释放对象存储中的备份文件)后主动移除 Finalizer,对象才会真正删除。这保证了有状态应用的数据不会因为一次误删操作而丢失。
如何开发一个 Operator:主流方案对比
目前开发 Operator 主要有三条路线,选择时需要结合团队技术栈和维护成本综合考虑。
第一条是使用 Go 配合 Kubebuilder 或 Operator SDK(底层是 controller-runtime)。这是最正统的方案,性能好、社区生态成熟,Prometheus Operator、Etcd Operator 等知名项目都是这么做的。缺点是需要掌握 Go 语言和 Kubernetes API 的编程模型,学习曲线偏陡。
第二条是使用 Kubernetes Java Framework 或 Python 的 Kopf。以 Kopf 为例,写一个简单的调谐函数只需要几十行 Python 代码,非常适合团队以 Python 为主的场景:
import kopf
@kopf.on.create('ipipp.com', 'v1', 'mysqlclusters')
def create_fn(spec, name, namespace, logger, **kwargs):
replicas = spec.get('replicas', 1)
# 创建 StatefulSet,镜像是运维知识的一部分
stroage_size = spec.get('storage', '10Gi')
logger.info(f"Creating MySQL cluster {name} with {replicas} replicas")
yield create_statefulset(name, namespace, replicas, stroage_size)
# 等待 Pod 就绪后初始化主从复制关系
yield setup_replication(name, namespace, replicas)第三条是使用声明式的 Helm Operator 或 Kustomize Controller,直接复用现有的 Helm Chart。这种方式几乎不用写代码,但表达能力有限,只能覆盖部署和升级,无法实现复杂的主从切换、备份恢复等逻辑,适合作为过渡方案。
| 方案 | 语言 | 开发效率 | 表达能力 | 适用场景 |
|---|---|---|---|---|
| Kubebuilder / Operator SDK | Go | 中等 | 最强 | 生产级、复杂运维逻辑 |
| Kopf / Java Framework | Python / Java | 高 | 较强 | 团队技术栈匹配 |
| Helm Operator | YAML | 最高 | 弱 | 简单部署与升级 |
一个有状态应用 Operator 应该具备的能力
参考 OperatorHub 上成熟项目的经验,一个合格的数据库类 Operator 通常要覆盖以下能力闭环。首先是集群生命周期管理,包括按序创建实例、配置发现服务(Headless Service)和读写分离 Service。其次是故障处理,控制器需要通过探针或心跳判断节点健康度,主库故障时自动提升从库为新主,并重写其余从库的复制指向,整个过程通常要求在一分钟内完成。
再次是备份与恢复。常见的做法是定义一个 Backup CR,控制器收到创建请求后,执行逻辑备份或基于快照的物理备份,把结果上传到对象存储,并在 CR 的 Status 中记录备份位置和时间,方便后续按时间点恢复。最后是版本升级,有状态应用的升级往往有严格的顺序要求,比如 Kafka 要先逐个升级 Follower 再升级 Leader,Operator 可以把这些顺序逻辑固化在调谐流程中,配合就绪探针确保每个阶段稳定后再进入下一阶段。
值得一提的是,社区的 Cloud Native Operational Velocity 标准(曾称 Operator Capability Levels)把 Operator 能力分成了五级,从基本安装、无缝升级,到备份恢复、自动故障转移,一直到最高的自动扩缩容。这个分级可以作为评估第三方 Operator 成熟度的参考标准,选型时至少应要求达到第三级。
落地时的常见坑与最佳实践
第一个常见的坑是调谐函数中执行了耗时很长的操作,比如在调谐过程中同步等待几百兆的数据恢复完成。这会阻塞工作队列,导致其他事件处理延迟。正确做法是把长任务交给 Job 或独立的 goroutine 异步执行,调谐函数只负责创建任务并检查任务状态,必要时通过 RequeueAfter 轮询结果。
第二个坑是对终态的判断不够严谨。有状态应用的很多操作存在中间状态,例如从库正在同步数据、主从切换进行到一半。如果控制器在这些中间状态上做出错误判断,可能引发脑裂。建议把详细的运行状态写入 CR 的 Status 子资源,并通过 StatusConditions 记录每个阶段的进展,既方便排查问题,也能让控制器自己基于状态做决策。
最后,权限控制一定要遵循最小权限原则。Operator 通常需要 RBAC 权限来操作 Pod、PVC、Service 等资源,开发阶段图方便直接绑定 cluster-admin 角色是常见的隐患。生产环境应该按资源类型逐项声明所需的权限,并尽量把管理范围限制在特定 Namespace 内。此外,记得为控制器配置 Leader Election,避免多副本的控制器同时操作集群造成状态混乱。
总体来看,Operator 模式并不神秘,它只是把“运维知识代码化”这一理念嫁接在 Kubernetes 声明式 API 和控制循环之上。对于数据库、消息中间件这类复杂的有状态应用,引入成熟的 Operator 或者针对内部系统自研一个,往往是提升运维自动化水平最有效的路径。
Kubernetes Operator有状态应用自定义控制器修改时间:2026-09-11 06:36:39