operator-sdk流程分析

本文详细介绍了Kubernetes控制器的初始化、Manager的创建以及Controller的启动过程。首先,通过Zap日志库设置logger,接着配置并创建Manager,包括设置监控端口、feature gate、leader选举和webhook选项。然后,使用NewManager创建Manager,初始化MapperProvider、cache和client。最后,启动MemcachedReconciler控制器,并通过Manager开始运行。整个流程涉及事件源、缓存同步、worker线程以及Reconcile方法的调用,确保资源的管理和协调。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述

1.初始化logger

// use zap logging cli options
	//set the controller-runtime logger 
	opts := zap.Options{}
	//add the zap flagset to the operator’s command line flags
	opts.BindFlags(flag.CommandLine)
	//定义 监控暴露的端口
	flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
	//定义 feature gate 开关
	flag.StringVar(&featureGatesString, "feature-gates", "", "Feature gate to enable, format is a command separated list enabling features, for instance RunAsNonRoot=false")
	//是否开启leader 选举
	flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
		"Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.")
	//是否启用webhook
	flag.BoolVar(&skipWebhookConfig, "skip-webhook-config", true,
		"When set, don't setup webhook TLS certificates. Useful in OpenShift where this step is handled already.")
	flag.Parse()

	// create logger using zap cli options
	// for instance --zap-log-level=debug
	logger := zap.New(zap.UseFlagOptions(&opts))

	ctrl.SetLogger(logger)

2.初始化manager

watchNamespace := os.Getenv(watchNamespaceEnvVar)
	//Options are the arguments for creating a new Manager
	mgrOpts := ctrl.Options{
		Scheme:             scheme,
		Namespace:          watchNamespace,
		MetricsBindAddress: metricsAddr,
		LeaderElection:     enableLeaderElection,
		Port:               9443,
		CertDir:            certDir,
	}

mgrOpts 作为实例化manager的参数

	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), mgrOpts)
	if err != nil {
		setupLog.Error(err, "unable to create manager")
		os.Exit(1)
	}
// NewManager returns a new Manager for creating Controllers.
	NewManager = manager.New
// New returns a new Manager for creating Controllers.
func New(config *rest.Config, options Options) (Manager, error) {
	// Set default values for options fields
	options = setOptionsDefaults(options)

	cluster, err := cluster.New(config, func(clusterOptions *cluster.Options) {
		clusterOptions.Scheme = options.Scheme
		clusterOptions.MapperProvider = options.MapperProvider
		clusterOptions.Logger = options.Logger
		clusterOptions.SyncPeriod = options.SyncPeriod
		clusterOptions.Namespace = options.Namespace
		clusterOptions.NewCache = options.NewCache
		clusterOptions.ClientBuilder = options.ClientBuilder
		clusterOptions.ClientDisableCacheFor = options.ClientDisableCacheFor
		clusterOptions.DryRunClient = options.DryRunClient
		clusterOptions.EventBroadcaster = options.EventBroadcaster
	})

在cluster.New函数中初始化了MapperProvider,cache和client

// New constructs a brand new cluster
func New(config *rest.Config, opts ...Option) (Cluster, error) {
	if config == nil {
		return nil, errors.New("must specify Config")
	}

	options := Options{}
	for _, opt := range opts {
		opt(&options)
	}
	options = setOptionsDefaults(options)

	// Create the mapper provider
	mapper, err := options.MapperProvider(config)
	if err != nil {
		options.Logger.Error(err, "Failed to get API Group-Resources")
		return nil, err
	}

	// Create the cache for the cached read client and registering informers
	cache, err := options.NewCache(config, cache.Options{Scheme: options.Scheme, Mapper: mapper, Resync: options.SyncPeriod, Namespace: options.Namespace})
	if err != nil {
		return nil, err
	}

	clientOptions := client.Options{Scheme: options.Scheme, Mapper: mapper}

  • MapperProvider返回RESTMapper,RESTMapper作为GVK到GVR的映射, GVK可以从资源对象json或yaml文件中提取, 通过GVK可以得到GVR, 通过GVR可以拼接出资源的http api请求路径。主要作用是在listWatch时, 根据Schema定义的类型GVK解析出GVR, 向apiserver发起http请求获取资源, 然后watch。 watch基于http的Chunk实现,维护长连接。 mapper主要是在ListAndWatch中使用到
  • 创建cache: 使用默认的cache.New创建, cache中可以储存原生api对象也可以存储crd对象
	cache, err := options.NewCache(config, cache.Options{Scheme: options.Scheme, Mapper: mapper, Resync: options.SyncPeriod, Namespace: options.Namespace})
	// Allow newCache to be mocked
	if options.NewCache == nil {
		options.NewCache = cache.New
	}
// New initializes and returns a new Cache.
func New(config *rest.Config, opts Options) (Cache, error) {
	opts, err := defaultOpts(config, opts)
	if err != nil {
		return nil, err
	}
	im := internal.NewInformersMap(config, opts.Scheme, opts.Mapper, *opts.Resync, opts.Namespace)
	return &informerCache{InformersMap: im}, nil
}
  • 创建client:client 创建了两个一个用于读,一个用于写,用于读的会直接使用上面的 cache,用于写的才会直接和 APIServer 进行交互
	clientOptions := client.Options{Scheme: options.Scheme, Mapper: mapper}

	apiReader, err := client.New(config, clientOptions)
	if err != nil {
		return nil, err
	}

	writeObj, err := options.ClientBuilder.
		WithUncached(options.ClientDisableCacheFor...).
		Build(cache, config, clientOptions)
	if err != nil {
		return nil, err
	}

3.启动controller

	if err = (&controllers.MemcachedReconciler{
		Client: mgr.GetClient(),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "unable to create controller", "controller", "Memcached")
		os.Exit(1)
	}
  • main.go 的方法里面主要是初始化了 Controller 的结构体,然后调用了 SetupWithManager方法
// SetupWithManager sets up the controller with the Manager.
func (r *MemcachedReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&cachev1alpha1.Memcached{}).
		Complete(r)
}
	setupLog.Info("starting manager")
	if err := mgr.Start(ctx); err != nil {
		setupLog.Error(err, "problem running manager")
		os.Exit(1)
	}
func (cm *controllerManager) Start(ctx context.Context) (err error) {
	if err := cm.Add(cm.cluster); err != nil {
		return fmt.Errorf("failed to add cluster to runnables: %w", err)
	}
	cm.internalCtx, cm.internalCancel = context.WithCancel(ctx)

	// This chan indicates that stop is complete, in other words all runnables have returned or timeout on stop request
	stopComplete := make(chan struct{})
	defer close(stopComplete)
	// This must be deferred after closing stopComplete, otherwise we deadlock.
	defer func() {
		stopErr := cm.engageStopProcedure(stopComplete)
		if stopErr != nil {
			if err != nil {
				// Utilerrors.Aggregate allows to use errors.Is for all contained errors
				// whereas fmt.Errorf allows wrapping at most one error which means the
				// other one can not be found anymore.
				err = utilerrors.NewAggregate([]error{err, stopErr})
			} else {
				err = stopErr
			}
		}
	}()

	// initialize this here so that we reset the signal channel state on every start
	// Everything that might write into this channel must be started in a new goroutine,
	// because otherwise we might block this routine trying to write into the full channel
	// and will not be able to enter the deferred cm.engageStopProcedure() which drains
	// it.
	cm.errChan = make(chan error)

	// Metrics should be served whether the controller is leader or not.
	// (If we don't serve metrics for non-leaders, prometheus will still scrape
	// the pod but will get a connection refused)
	if cm.metricsListener != nil {
		go cm.serveMetrics()
	}

	// Serve health probes
	if cm.healthProbeListener != nil {
		go cm.serveHealthProbes()
	}

	go cm.startNonLeaderElectionRunnables()

	go func() {
		if cm.resourceLock != nil {
			err := cm.startLeaderElection()
			if err != nil {
				cm.errChan <- err
			}
		} else {
			// Treat not having leader election enabled the same as being elected.
			cm.startLeaderElectionRunnables()
			close(cm.elected)
		}
	}()

	select {
	case <-ctx.Done():
		// We are done
		return nil
	case err := <-cm.errChan:
		// Error starting or running a runnable
		return err
	}
}
func (cm *controllerManager) startNonLeaderElectionRunnables() {
	cm.mu.Lock()
	defer cm.mu.Unlock()

	cm.waitForCache(cm.internalCtx)

	// Start the non-leaderelection Runnables after the cache has synced
	for _, c := range cm.nonLeaderElectionRunnables {
		// Controllers block, but we want to return an error if any have an error starting.
		// Write any Start errors to a channel so we can return them
		cm.startRunnable(c)
	}
}
func (cm *controllerManager) startRunnable(r Runnable) {
	cm.waitForRunnable.Add(1)
	go func() {
		defer cm.waitForRunnable.Done()
		if err := r.Start(cm.internalCtx); err != nil {
			cm.errChan <- err
		}
	}()
}

最终调用了controller中的start方法

// Start implements controller.Controller
func (c *Controller) Start(ctx context.Context) error {
	// use an IIFE to get proper lock handling
	// but lock outside to get proper handling of the queue shutdown
	c.mu.Lock()
	if c.Started {
		return errors.New("controller was started more than once. This is likely to be caused by being added to a manager multiple times")
	}

	c.initMetrics()

	// Set the internal context.
	c.ctx = ctx

	c.Queue = c.MakeQueue()
	defer c.Queue.ShutDown() // needs to be outside the iife so that we shutdown after the stop channel is closed

	err := func() error {
		defer c.mu.Unlock()

		// TODO(pwittrock): Reconsider HandleCrash
		defer utilruntime.HandleCrash()

		// NB(directxman12): launch the sources *before* trying to wait for the
		// caches to sync so that they have a chance to register their intendeded
		// caches.
		for _, watch := range c.startWatches {
			c.Log.Info("Starting EventSource", "source", watch.src)

			if err := watch.src.Start(ctx, watch.handler, c.Queue, watch.predicates...); err != nil {
				return err
			}
		}

		// Start the SharedIndexInformer factories to begin populating the SharedIndexInformer caches
		c.Log.Info("Starting Controller")

		for _, watch := range c.startWatches {
			syncingSource, ok := watch.src.(source.SyncingSource)
			if !ok {
				continue
			}

			if err := func() error {
				// use a context with timeout for launching sources and syncing caches.
				sourceStartCtx, cancel := context.WithTimeout(ctx, c.CacheSyncTimeout)
				defer cancel()

				// WaitForSync waits for a definitive timeout, and returns if there
				// is an error or a timeout
				if err := syncingSource.WaitForSync(sourceStartCtx); err != nil {
					err := fmt.Errorf("failed to wait for %s caches to sync: %w", c.Name, err)
					c.Log.Error(err, "Could not wait for Cache to sync")
					return err
				}

				return nil
			}(); err != nil {
				return err
			}
		}

		// All the watches have been started, we can reset the local slice.
		//
		// We should never hold watches more than necessary, each watch source can hold a backing cache,
		// which won't be garbage collected if we hold a reference to it.
		c.startWatches = nil

		if c.JitterPeriod == 0 {
			c.JitterPeriod = 1 * time.Second
		}

		// Launch workers to process resources
		c.Log.Info("Starting workers", "worker count", c.MaxConcurrentReconciles)
		for i := 0; i < c.MaxConcurrentReconciles; i++ {
			go wait.UntilWithContext(ctx, func(ctx context.Context) {
				// Run a worker thread that just dequeues items, processes them, and marks them done.
				// It enforces that the reconcileHandler is never invoked concurrently with the same object.
				for c.processNextWorkItem(ctx) {
				}
			}, c.JitterPeriod)
		}

		c.Started = true
		return nil
	}()
	if err != nil {
		return err
	}

	<-ctx.Done()
	c.Log.Info("Stopping workers")
	return nil
}

Reconcile 方法的触发是通过 Cache 中的 Informer 获取到资源的变更事件,然后再通过生产者消费者的模式触发我们自己实现的 Reconcile 方法的。

<think>好的,我现在要回答用户的问题:“为什么k8s要引入operator”。首先,我需要回顾之前关于Operator的讨论,确保这次回答保持一致性。用户之前已经了解了Operator是一个概念,不是单独安装的,而是随中间件部署时对应的Operator实现。所以现在的问题是要解释Kubernetes为什么要引入这个Operator概念。 首先,我需要考虑用户可能的背景。用户可能对Kubernetes的基础有一定了解,知道Deployment、Service等基本资源,但在管理有状态应用时遇到了挑战。Operator的出现是为了解决这些问题,所以需要从Kubernetes的局限性谈起。 接下来,我应该分析用户的问题核心:为什么引入Operator?这涉及到Kubernetes原生资源不足以处理复杂应用的管理,尤其是那些需要维护状态、处理复杂启动顺序或需要特定领域知识的应用。比如数据库、消息队列等中间件,这些应用通常需要备份、恢复、扩缩容等操作,而这些操作如果手动管理会很繁琐且容易出错。 然后,我需要结构化回答,可能分为几个部分:原生资源的不足、Operator解决的问题、Operator带来的好处,以及可能的实际例子。这样用户能清晰理解引入Operator的必要性。 需要注意之前回答的结构,用户可能习惯之前的详细分点,所以保持类似的格式有助于理解。比如分点列出原因,每个原因下有子点详细说明,可能还要比较有无Operator的区别。 同时,要确保使用中文,避免技术术语过于晦涩,必要时用类比。比如将Operator比作专家系统,自动处理应用管理的各种任务,而无需人工干预。 还需要检查是否需要补充Operator的工作原理,比如CRD和控制器模式,但可能用户已经了解,所以可以简要提及,重点放在为什么需要这种模式。 最后,总结Operator的重要性,强调其在自动化运维和扩展Kubernetes能力方面的作用。可能还要提到社区和生态的支持,说明Operator的广泛应用和成熟度。 在整理过程中,要注意用户可能存在的疑问,比如Operator是否增加复杂度,或者是否所有应用都需要Operator。可以适当在回答中提及这些点,但主要围绕问题核心展开。 确保使用之前的格式,如分节、代码块、表格等,但根据当前问题调整。比如使用“###”来分节,用列表列举原因,可能用对比表格显示有无Operator的区别。 最后,检查是否符合系统级指令,如数学公式正确使用$符号,但这个问题可能不涉及数学公式。确保LaTeX语法正确,虽然这里可能用不上。结构清晰,逐步解释,帮助用户理解引入Operator的背景、原因和好处。</think>--- ### **1. 背景:Kubernetes 原生能力的局限性** Kubernetes 原生资源(如 `Deployment`、`StatefulSet`)主要面向**无状态应用**设计,但对**有状态中间件**(如数据库、消息队列)的管理存在显著不足: - **状态管理困难**:中间件需要持久化数据、节点发现、故障恢复等复杂操作。 - **领域知识依赖**:部署 Redis 集群需要理解哨兵模式、分片策略,而原生资源无法封装这些逻辑。 - **运维自动化缺失**:扩缩容、备份恢复、版本升级等操作需手动干预,易出错。 --- ### **2. Operator 的核心价值:将运维知识代码化** Operator 的引入是为了**将人类运维经验转化为可执行的自动化逻辑**,具体解决以下问题: #### **2.1 填补有状态应用的管理空白** | **场景** | **原生 Kubernetes** | **使用 Operator** | |------------------------|----------------------------------|---------------------------------------| | 部署 Redis 集群 | 需手动配置 Pod、Service、持久化卷 | 声明 `RedisCluster` CRD,Operator 自动生成所有资源 | | 数据库主从切换 | 需人工监控并执行脚本 | Operator 自动检测故障并触发切换 | | 滚动升级 Kafka | 需逐步手动更新每个 Pod | Operator 按顺序安全升级,确保服务连续 | #### **2.2 封装领域专属逻辑** - **示例 1(Etcd Operator)**: - 自动处理节点加入/退出、数据一致性保障。 - 备份恢复:定期快照并存储到对象存储(如 S3)。 ```yaml # EtcdCluster CRD 示例 spec: size: 3 backup: interval: 24h maxBackups: 7 storageType: "S3" ``` - **示例 2(Prometheus Operator)**: - 自动生成监控目标配置(ServiceMonitor)。 - 动态调整存储卷大小。 ```yaml # Prometheus CRD 示例 spec: storage: volumeClaimTemplate: resources: requests: storage: 100Gi ``` --- ### **3. Operator 的技术实现原理** #### **3.1 扩展 Kubernetes API(CRD)** - **定义自定义资源**(如 `MySQLCluster`、`KafkaTopic`),允许用户以声明式 API 描述中间件的期望状态。 - **对比原生资源**: ```bash # 原生资源(如 Deployment) kubectl get deployments # 自定义资源(通过 Operator 扩展) kubectl get kafkatopics ``` #### **3.2 控制器模式(Control Loop)** Operator 的核心是一个持续运行的**控制器**,其工作流程如下: 1. **监听 CRD 变化**:当用户创建/修改 `RedisCluster` 等资源时,Operator 被触发。 2. **调和(Reconcile)**: - 对比当前状态(如实际运行的 Pod 数量)与期望状态(CRD 中定义的 `replicas: 5`)。 - 执行操作(扩容 Pod、更新配置)直至两者一致。 3. **处理故障**:自动重试失败操作,避免人工介入。 --- ### **4. 为什么 Kubernetes 必须引入 Operator?** #### **4.1 本质需求:平台能力下沉** - **Kubernetes 的定位**:成为云原生时代的“操作系统”。 - **操作系统类比**: | **操作系统** | **Kubernetes** | **Operator** | |--------------------|-------------------------|------------------------------| | 管理 CPU/内存 | 管理 Pod/Node | 管理数据库、消息队列等 | | 提供设备驱动 | 原生资源(Deployment) | 中间件专属 Operator | #### **4.2 生态发展的必然选择** - **标准化扩展接口**:CRD + Controller 模式为开发者提供了统一的应用管理范式。 - **社区协作**:厂商(如 Red Hat、VMware)和开源项目(如 etcd、Prometheus)共同推动 Operator 生态繁荣。 --- ### **5. 实际效益** #### **5.1 对开发者的价值** - **简化部署**:从数十个 YAML 文件缩减到单个 CRD 声明。 - **降低认知负担**:无需深入学习中间件运维细节。 #### **5.2 对运维团队的价值** - **减少人为错误**:自动化替代手动操作。 - **统一管控**:所有中间件通过相同的 Operator 模式管理。 #### **5.3 对企业架构的价值** - **加速云原生转型**:传统中间件可无缝迁移到 Kubernetes- **成本优化**:通过精细化自动化控制资源利用率。 --- ### **6. 总结:Operator 的意义** - **核心理念**:将**运维专家的经验**转化为**可重复执行的代码**。 - **技术定位**:Kubernetes 从“基础设施编排”升级为“应用感知型平台”的关键组件。 - **未来趋势**:随着 Operator 框架(如 Kubebuilder、Operator SDK)的成熟,复杂应用管理将彻底实现“声明式自动化”。 如果需要深入探讨某个具体 Operator 的实现细节(如 Cassandra Operator 的调优策略),可以进一步讨论! 🚀
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值