controller-manager学习三部曲之二:源码学习

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

本篇概览

  • 作为《controller-manager学习三部曲》系列的第二篇,前面通过shell脚本找到了程序的入口,接下来咱们来学习controller-manager的源码
  • 学习源码虽然收获巨大,然而耗时耗力,为了让学习变得轻松,这里提前梳理一下,以确保主方向正确
  1. 查看启动命令:了解真实kubernetes环境下controll-manager的启动命令,知道会传入那些参数
  2. 入口:搞清楚代码从哪里开始看,然后分析如何处理输入的参数
  3. 初始化配置:自身有大量默认参数,还有启动时输入的大量参数,使用这些信息进行初始化配置
  4. 深入理解启动逻辑:先学习启动controller的大框架,然后重点分析两个重要方法,创建上下文的CreateControllerContext,调用每个controller启动的StartControllers
  • 总的来说,复杂的controller-manager就是由这些部分组成

查看启动命令

  • 找个现成的kubernetes系统,看一下真正运行的controller-manager的启动命令是啥样的
  • 以我自己测试用的kubernetes环境为例,先查看pod名
kubectl get pods -n kube-system
NAME                           READY   STATUS    RESTARTS       AGE
coredns-78fcd69978-jztff       1/1     Running   6 (35d ago)    125d
coredns-78fcd69978-ts7gq       1/1     Running   6 (35d ago)    125d
etcd-hedy                      1/1     Running   6 (35d ago)    125d
kube-apiserver-hedy            1/1     Running   7 (35d ago)    125d
kube-controller-manager-hedy   1/1     Running   11 (30h ago)   125d
kube-proxy-2qx6k               1/1     Running   6              125d
kube-scheduler-hedy            1/1     Running   11 (30h ago)   125d
  • 可见controller-manager的pod名是kube-controller-manager-hedy,执行以下命令即可查看看pod的详细信息
kubectl describe pod kube-controller-manager-hedy -n kube-system
  • 上述命令会输出大量信息,这里只展示我们最关心的内容,即controller-manager的启动命令
    Command:
      kube-controller-manager
      --allocate-node-cidrs=true
      --authentication-kubeconfig=/etc/kubernetes/controller-manager.conf
      --authorization-kubeconfig=/etc/kubernetes/controller-manager.conf
      --bind-address=0.0.0.0
      --client-ca-file=/etc/kubernetes/pki/ca.crt
      --cluster-cidr=100.64.0.0/10
      --cluster-name=kubernetes
      --cluster-signing-cert-file=/etc/kubernetes/pki/ca.crt
      --cluster-signing-key-file=/etc/kubernetes/pki/ca.key
      --controllers=*,bootstrapsigner,tokencleaner
      --experimental-cluster-signing-duration=876000h
      --feature-gates=TTLAfterFinished=true,EphemeralContainers=true
      --kubeconfig=/etc/kubernetes/controller-manager.conf
      --leader-elect=true
      --port=0
      --requestheader-client-ca-file=/etc/kubernetes/pki/front-proxy-ca.crt
      --root-ca-file=/etc/kubernetes/pki/ca.crt
      --service-account-private-key-file=/etc/kubernetes/pki/sa.key
      --service-cluster-ip-range=10.96.0.0/22
      --use-service-account-credentials=true
  • 分析上述信息得到以下结论:
  1. 没有用到子命令
  2. 有多个标志(flag),所有参数都是flag参数,没有命令参数(命令、子命令名、标志、命令参数、标志参数这些都是cobra的概念)
  • 接下来可以看代码了

入口

  • 看过启动命令后,寻找入口代码也就简单了:找到cobra的命令function定义即可
  • 先看main函数,在kubernetes/cmd/kube-controller-manager/controller-manager.go文件中
func main() {
	command := app.NewControllerManagerCommand()
	code := cli.Run(command)
	os.Exit(code)
}
  • 进入NewControllerManagerCommand方法中,整个方法的代码如下图,我将其分为三部分,请按照序号来阅读(只要是2和3的顺序不要弄错了)
    在这里插入图片描述
  • 第一部分就是第一行代码,如下,为每个内置controller创建默认配置文件
s, err := options.NewKubeControllerManagerOptions()
  • 这个NewKubeControllerManagerOptions方法也值得一看,如下图,就是为所有controller创建了保存配置信息的对象,都放在数据结构KubeControllerManagerOptions中返回
    在这里插入图片描述

  • 再回到NewControllerManagerCommand,该看第二部分了,这里的namedFlagSets需要注意,可以这么理解:每个controller都有一个flag集合(里面是多个flag),所以多个controller就有多个flag集合,全部存放在namedFlagSets中,通过controller的name来存取

// 准备一个flag集合,注意,flag是cobra中的概念
fs := cmd.Flags()
// 每个controller都有一个flag集合(里面是多个flag),所以多个controller就有多个flag集合,全部存放在namedFlagSets中,通过controller的name来存取
namedFlagSets := s.Flags(KnownControllers(), ControllersDisabledByDefault.List())
verflag.AddFlags(namedFlagSets.FlagSet("global"))
globalflag.AddGlobalFlags(namedFlagSets.FlagSet("global"), cmd.Name(), logs.SkipLoggingConfigurationFlags())
registerLegacyGlobalFlags(namedFlagSets)
for _, f := range namedFlagSets.FlagSets {
	fs.AddFlagSet(f)
}
  • 上面的代码还有一处需要注意,就是s.Flags方法中为每个controller都创建了里面创建了那么多flag,到底用在哪里了?如下图,在fs.AddFlagSet方法中,根据实际输入的controller-name取出s.Flags中对应的controller对象,放入cmd.fs中,然后就成为cmd的启动参数的一部分
    在这里插入图片描述

  • 上诉代码完成了输入flag和数据结构对象的关联

  • 接下来就是第三部分:创建cobra的Command对象,了解cobra的读者应该清楚,命令响应逻辑就在这个Command的参数中,这里亦是如此,通过RunE参数传入了整个应用的启动方法,也就是说进程启动后,就会运行RunE定义的方法

RunE: func(cmd *cobra.Command, args []string) error {
	// 如果启动命令传入了"--version",就打印版本信息然后退出进程
	verflag.PrintAndExitIfRequested()
	
	// 验证日志服务的设置并使之生效(如格式、文件目录等)
	if err := logsapi.ValidateAndApply(s.Logs, utilfeature.DefaultFeatureGate); err != nil {
		return err
	}
	// 打印所有flag的名字和值(这里的flag是cobra中的概念)
	cliflag.PrintFlags(cmd.Flags())
	
	// 配置初始化:注册controller,校验配置,生成配置对象,生成客户端对象clientSet
	// 这里有些重要的逻辑,稍后会详细说明
	c, err := s.Config(KnownControllers(), ControllersDisabledByDefault.List())
	if err != nil {
		return err
	}
	// 监控指标配置
	utilfeature.DefaultMutableFeatureGate.AddMetrics()
	// controller-manager的业务逻辑启动
	return Run(context.Background(), c.Complete())
}
  • 上述就是controller-manager的启动代码,尽管已经添加了详细的注释,仍有两处重点需要展开说明:
  1. s.Config(KnownControllers(), ControllersDisabledByDefault.List()) :这里面涉及到一些重要的初始化逻辑和数据结构
  2. return Run(context.Background(), c.Complete()):具体的启动逻辑在这里面,会启动各controller

重要:初始化配置

  • 接下来展开第一个重点,也就是下图黄色箭头所指的这行
    在这里插入图片描述
  • 先看KnownControllers方法,这里面调用了NewControllerInitializers方法,这个NewControllerInitializers很重要,也是咱们开发controller时非常值得借鉴的代码,它返回了一个map,key是一个controller的名字,value是这个controller的初始化方法,例如deployment的controller,其初始化方法是startDeploymentController,这里只用到了key,所有的key代表所有的controller名
func NewControllerInitializers(loopMode ControllerLoopMode) map[string]InitFunc {
	controllers := map[string]InitFunc{}

	// All of the controllers must have unique names, or else we will explode.
	register := func(name string, fn InitFunc) {
		if _, found := controllers[name]; found {
			panic(fmt.Sprintf("controller name %q was registered twice", name))
		}
		controllers[name] = fn
	}

	register("endpoint", startEndpointController)
	register("endpointslice", startEndpointSliceController)
	register("endpointslicemirroring", startEndpointSliceMirroringController)
	register("replicationcontroller", startReplicationController)
	register("podgc", startPodGCController)
	register("resourcequota", startResourceQuotaController)
	register("namespace", startNamespaceController)
	register("serviceaccount", startServiceAccountController)
	register("garbagecollector", startGarbageCollectorController)
	register("daemonset", startDaemonSetController)
	register("job", startJobController)
	register("deployment", startDeploymentController)
	register("replicaset", startReplicaSetController)
	register("horizontalpodautoscaling", startHPAController)
	register("disruption", startDisruptionController)
	register("statefulset", startStatefulSetController)
	register("cronjob", startCronJobController)
	register("csrsigning", startCSRSigningController)
	register("csrapproving", startCSRApprovingController)
	register("csrcleaner", startCSRCleanerController)
	register("ttl", startTTLController)
	register("bootstrapsigner", startBootstrapSignerController)
	register("tokencleaner", startTokenCleanerController)
	register("nodeipam", startNodeIpamController)
	register("nodelifecycle", startNodeLifecycleController)
	if loopMode == IncludeCloudLoops {
		register("service", startServiceController)
		register("route", startRouteController)
		register("cloud-node-lifecycle", startCloudNodeLifecycleController)
		// TODO: volume controller into the IncludeCloudLoops only set.
	}
	register("persistentvolume-binder", startPersistentVolumeBinderController)
	register("attachdetach", startAttachDetachController)
	register("persistentvolume-expander", startVolumeExpandController)
	register("clusterrole-aggregation", startClusterRoleAggregrationController)
	register("pvc-protection", startPVCProtectionController)
	register("pv-protection", startPVProtectionController)
	register("ttl-after-finished", startTTLAfterFinishedController)
	register("root-ca-cert-publisher", startRootCACertPublisher)
	register("ephemeral-volume", startEphemeralVolumeController)
	if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.APIServerIdentity) &&
		utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StorageVersionAPI) {
		register("storage-version-gc", startStorageVersionGCController)
	}
	if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.DynamicResourceAllocation) {
		register("resource-claim-controller", startResourceClaimController)
	}
	if utilfeature.DefaultFeatureGate.Enabled(kubefeatures.LegacyServiceAccountTokenCleanUp) {
		register("legacy-service-account-token-cleaner", startLegacySATokenCleaner)
	}

	return controllers
}
  • 这个NewControllerInitializers方法看起来就很重要,刚才的调用只用到了返回值的key,也就是所有controller的名字,稍后还会用到这个方法
  • 现在展开上图黄色箭头所指的s.Config方法内部,如下所示,最终得到了数据结构kubecontrollerconfig.Config的实例,这里面有各controller的配置信息,以及client-go的客户端对象
func (s KubeControllerManagerOptions) Config(allControllers []string, disabledByDefaultControllers []string) (*kubecontrollerconfig.Config, error) {
	// 对每个controller的配置进行校验
	if err := s.Validate(allControllers, disabledByDefaultControllers); err != nil {
		return nil, err
	}
	
	// 如果有必要就创建自签证书
	if err := s.SecureServing.MaybeDefaultWithSelfSignedCerts("localhost", nil, []net.IP{netutils.ParseIPSloppy("127.0.0.1")}); err != nil {
		return nil, fmt.Errorf("error creating self-signed certificates: %v", err)
	}

	// 为创建client-go的客户端对象做准备:创建restclient.Config
	kubeconfig, err := clientcmd.BuildConfigFromFlags(s.Master, s.Generic.ClientConnection.Kubeconfig)
	if err != nil {
		return nil, err
	}
	kubeconfig.DisableCompression = true
	kubeconfig.ContentConfig.AcceptContentTypes = s.Generic.ClientConnection.AcceptContentTypes
	kubeconfig.ContentConfig.ContentType = s.Generic.ClientConnection.ContentType
	kubeconfig.QPS = s.Generic.ClientConnection.QPS
	kubeconfig.Burst = int(s.Generic.ClientConnection.Burst)
	
	// 创建cliet-go库的客户端对象,有了它,就能对kubernetes的资源进行读写和监听了,非常重要
	client, err := clientset.NewForConfig(restclient.AddUserAgent(kubeconfig, KubeControllerManagerUserAgent))
	if err != nil {
		return nil, err
	}
	
	// 事件广播对象
	eventBroadcaster := record.NewBroadcaster()
	eventRecorder := eventBroadcaster.NewRecorder(clientgokubescheme.Scheme, v1.EventSource{Component: KubeControllerManagerUserAgent})

	// 创建配置对象
	c := &kubecontrollerconfig.Config{
		Client:           client,
		Kubeconfig:       kubeconfig,
		EventBroadcaster: eventBroadcaster,
		EventRecorder:    eventRecorder,
	}
	// 更新配置对象的信息(s的配置设置到c)
	if err := s.ApplyTo(c); err != nil {
		return nil, err
	}
	s.Metrics.Apply()

	return c, nil
}
  • 至此,配置相关的算是过了一遍,接下来该看启动的代码了,也就是下图黄色箭头所示
    在这里插入图片描述

启动逻辑分析

  • 整个Run方法的内容很多,除了启动controller,还有安全处理,以及leader迁移等逻辑,这里咱们只聚焦重点:选主和controller启动
  • 首先要看的是选主逻辑,注意下面的中文注释
// 如果无需选主(例如固定一个实例),这里直接调用run启动controller了,并提前返回
// 不过从启动命令的参数中有leader-elect=true,表示需要选主
if !c.ComponentConfig.Generic.LeaderElection.LeaderElect {
	run(ctx, saTokenControllerInitFunc, NewControllerInitializers)
	return nil
}
// 如果涉及到选主,也就是几个controller-manager进程,只有一个启动controller,就要准备一个独一无二的身份,这里是主机名+UUID
id, err := os.Hostname()
if err != nil {
	return err
}

id = id + "_" + string(uuid.NewUUID())
  • 身份确定后就是选主逻辑,注意是在一个新的协程中执行的选主,当前协程并未阻塞,如果选主成功,会执行run方法来启动controller
go leaderElectAndRun(ctx, c, id, electionChecker,
	c.ComponentConfig.Generic.LeaderElection.ResourceLock,
	c.ComponentConfig.Generic.LeaderElection.ResourceName,
	leaderelection.LeaderCallbacks{
		// OnStartedLeading会在选主成功后执行
		OnStartedLeading: func(ctx context.Context) {
			// NewControllerInitializers在前面分析过,会生成一个map,
			// key是controller名,value是controller的初始化方法
			initializersFunc := NewControllerInitializers
			if leaderMigrator != nil {
				// If leader migration is enabled, we should start only non-migrated controllers
				//  for the main lock.
				initializersFunc = createInitializersFunc(leaderMigrator.FilterFunc, leadermigration.ControllerNonMigrated)
				logger.Info("leader migration: starting main controllers.")
			}
			run(ctx, startSATokenController, initializersFunc)
		},
		// OnStoppedLeading会在失去leader身份时执行,klog.FlushAndExit内部会结束进程
		OnStoppedLeading: func() {
			logger.Error(nil, "leaderelection lost")
			klog.FlushAndExit(klog.ExitFlushTimeout, 1)
		},
	})
  • 又见到了熟悉的NewControllerInitializers方法,这一次它被作为第三个参数传入run(如果leaderMigrator非空,就会调用createInitializersFunc,里面还是调用了NewControllerInitializers)
  • 也就是说run方法通过第三个参数,知道了所有controller应该如何初始化
  • 接下来就是最核心的启动逻辑了

启动所有controller

  • 负责启动controller的是run方法,如下所示,逻辑上很简单:先准备一个通用的context,再得到所有的controller的初始化方法,逐一执行即可完成启动,这里面有两个重点,CreateControllerContext和StartControllers,后面会重点讲到
	run := func(ctx context.Context, startSATokenController InitFunc, initializersFunc ControllerInitializersFunc) {
		// 为启动controller准备context,里面存了多中公共对象,给各个controller用
		controllerContext, err := CreateControllerContext(logger, c, rootClientBuilder, clientBuilder, ctx.Done())
		if err != nil {
			logger.Error(err, "Error building controller context")
			klog.FlushAndExit(klog.ExitFlushTimeout, 1)
		}
		// initializersFunc即NewControllerInitializers方法,
		// 可以返回所有controller及其初始化方法
		controllerInitializers := initializersFunc(controllerContext.LoopMode)
		// StartControllers中会遍历controllerInitializers的返回值,对每个controller执行初始化和启动
		if err := StartControllers(ctx, controllerContext, startSATokenController, controllerInitializers, unsecuredMux, healthzHandler); err != nil {
			logger.Error(err, "Error starting controllers")
			klog.FlushAndExit(klog.ExitFlushTimeout, 1)
		}
		
		// 启动所有informer
		controllerContext.InformerFactory.Start(stopCh)
		controllerContext.ObjectOrMetadataInformerFactory.Start(stopCh)
		close(controllerContext.InformersStarted)

		<-ctx.Done()
	}
  • 主方法看过,算是对启动有了大致了解,再来看看细节,首先是创建通用上下文的CreateControllerContext,如下,生成了各类informer,client,都封装在context中,后面的controller可以使用
func CreateControllerContext(logger klog.Logger, s *config.CompletedConfig, rootClientBuilder, clientBuilder clientbuilder.ControllerClientBuilder, stop <-chan struct{}) (ControllerContext, error) {
	versionedClient := rootClientBuilder.ClientOrDie("shared-informers")
	sharedInformers := informers.NewSharedInformerFactory(versionedClient, ResyncPeriod(s)())

	metadataClient := metadata.NewForConfigOrDie(rootClientBuilder.ConfigOrDie("metadata-informers"))
	metadataInformers := metadatainformer.NewSharedInformerFactory(metadataClient, ResyncPeriod(s)())

	// If apiserver is not running we should wait for some time and fail only then. This is particularly
	// important when we start apiserver and controller manager at the same time.
	if err := genericcontrollermanager.WaitForAPIServer(versionedClient, 10*time.Second); err != nil {
		return ControllerContext{}, fmt.Errorf("failed to wait for apiserver being healthy: %v", err)
	}

	// Use a discovery client capable of being refreshed.
	discoveryClient := rootClientBuilder.DiscoveryClientOrDie("controller-discovery")
	cachedClient := cacheddiscovery.NewMemCacheClient(discoveryClient)
	restMapper := restmapper.NewDeferredDiscoveryRESTMapper(cachedClient)
	go wait.Until(func() {
		restMapper.Reset()
	}, 30*time.Second, stop)

	availableResources, err := GetAvailableResources(rootClientBuilder)
	if err != nil {
		return ControllerContext{}, err
	}

	cloud, loopMode, err := createCloudProvider(logger, s.ComponentConfig.KubeCloudShared.CloudProvider.Name, s.ComponentConfig.KubeCloudShared.ExternalCloudVolumePlugin,
		s.ComponentConfig.KubeCloudShared.CloudProvider.CloudConfigFile, s.ComponentConfig.KubeCloudShared.AllowUntaggedCloud, sharedInformers)
	if err != nil {
		return ControllerContext{}, err
	}

	ctx := ControllerContext{
		ClientBuilder:                   clientBuilder,
		InformerFactory:                 sharedInformers,
		ObjectOrMetadataInformerFactory: informerfactory.NewInformerFactory(sharedInformers, metadataInformers),
		ComponentConfig:                 s.ComponentConfig,
		RESTMapper:                      restMapper,
		AvailableResources:              availableResources,
		Cloud:                           cloud,
		LoopMode:                        loopMode,
		InformersStarted:                make(chan struct{}),
		ResyncPeriod:                    ResyncPeriod(s),
		ControllerManagerMetrics:        controllersmetrics.NewControllerManagerMetrics("kube-controller-manager"),
	}
	controllersmetrics.Register()
	return ctx, nil
}
  • 上述代码中,有一小段值得注意,注释也值得一看,就是同步等待的逻辑,这里面又会引出一个重要的知识点:用channel实现轮询同步等待,学习并发的读者可以输入研究,一定受益匪浅
	// If apiserver is not running we should wait for some time and fail only then. This is particularly
	// important when we start apiserver and controller manager at the same time.
	if err := genericcontrollermanager.WaitForAPIServer(versionedClient, 10*time.Second); err != nil {
		return ControllerContext{}, fmt.Errorf("failed to wait for apiserver being healthy: %v", err)
	}
  • 关于同步等待并非本文主题,就不多说了,上面的WaitForAPIServer经过层层展开到了waitForWithContext,简单的分析一下
// 第二个入参wait,返回值是个channel,wait方法里面有定时器,每一秒向返回的channel写数据,超时了就关闭
func waitForWithContext(ctx context.Context, wait waitWithContextFunc, fn ConditionWithContextFunc) error {
	waitCtx, cancel := context.WithCancel(context.Background())
	defer cancel()
	c := wait(waitCtx)
	for {
		select {
		// 由于c在wait方法中每秒被写一次,所以下面这个case每秒执行一次
		case _, open := <-c:
			// 这里的fn在外面传入的是远程请求api-server
			ok, err := runConditionWithCrashProtectionWithContext(ctx, fn)
			if err != nil {
				return err
			}
			// 这表示api-server能正常响应,也就是在超时时间内拿到了想要的结果
			if ok {
				return nil
			}
			// 如果c被关闭,就证明已经超时了
			if !open {
				return ErrWaitTimeout
			}
		case <-ctx.Done():
			// returning ctx.Err() will break backward compatibility, use new PollUntilContext*
			// methods instead
			return ErrWaitTimeout
		}
	}
}

StartControllers分析

  • 前面说到run方法中最重要的是CreateControllerContext和StartControllers,现在该看这个StartControllers了,注意它的第四个参数controllers就是咱们的老朋友NewControllerInitializers,这里面有所有controller的初始化方法
  • StartControllers是本篇最重要的内容,此方法代码也挺多,咱们只看最关键的,也就是遍历controller的初始化方法集合的处理逻辑,似乎很容易,因为咱们对controllers已经了如指掌,下面这个循环其实就是将NewControllerInitializers返回的所有controller初始化方法都执行一遍(可能会条件过滤掉一些)
  • TODO:下面的代码应该再加上一些注释
	// 遍历初始化方法集合
	for controllerName, initFn := range controllers {
		if !controllerCtx.IsControllerEnabled(controllerName) {
			logger.Info("Warning: controller is disabled", "controller", controllerName)
			continue
		}

		time.Sleep(wait.Jitter(controllerCtx.ComponentConfig.Generic.ControllerStartInterval.Duration, ControllerStartJitter))
		
		logger.V(1).Info("Starting controller", "controller", controllerName)
		// 执行initFn,就完成了controller的初始化和启动,initFn具体是什么呢?那要看NewControllerInitializers方法中的内容
		ctrl, started, err := initFn(klog.NewContext(ctx, klog.LoggerWithName(logger, controllerName)), controllerCtx)
		if err != nil {
			logger.Error(err, "Error starting controller", "controller", controllerName)
			return err
		}
		// 启动失败则继续下一个
		if !started {
			logger.Info("Warning: skipping controller", "controller", controllerName)
			continue
		}
		check := controllerhealthz.NamedPingChecker(controllerName)
		if ctrl != nil {
			// check if the controller supports and requests a debugHandler
			// and it needs the unsecuredMux to mount the handler onto.
			if debuggable, ok := ctrl.(controller.Debuggable); ok && unsecuredMux != nil {
				if debugHandler := debuggable.DebuggingHandler(); debugHandler != nil {
					basePath := "/debug/controllers/" + controllerName
					unsecuredMux.UnlistedHandle(basePath, http.StripPrefix(basePath, debugHandler))
					unsecuredMux.UnlistedHandlePrefix(basePath+"/", http.StripPrefix(basePath, debugHandler))
				}
			}
			// 如果当前controller支持健康检查,就放入check切片,后面统一注册
			if healthCheckable, ok := ctrl.(controller.HealthCheckable); ok {
				if realCheck := healthCheckable.HealthChecker(); realCheck != nil {
					check = controllerhealthz.NamedHealthChecker(controllerName, realCheck)
				}
			}
		}
		controllerChecks = append(controllerChecks, check)

		logger.Info("Started controller", "controller", controllerName)
	}
	// 注册健康检查,类似gin注册路由,每个path对应一个controller的健康检查路径,这样外部就能通过这个path来确定controller是否健康
	healthzHandler.AddHealthChecker(controllerChecks...)
  • 至此,controller-manager的源码分析就完成了,可见主要工作是准备配置,确保每个controller完成启动,接下来的文章,咱们再深入一个典型的controller,了解kubernetes自己的controller是如何启动的

你不孤单,欣宸原创一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 数据库+中间件系列
  6. DevOps系列

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/383952.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据库管理-第149期 Oracle Vector DB AI-01(20240210)

数据库管理149期 2024-02-10 数据库管理-第149期 Oracle Vector DB & AI-01&#xff08;20240210&#xff09;1 机器学习2 向量3 向量嵌入4 向量检索5 向量数据库5 专用向量数据库的问题总结 数据库管理-第149期 Oracle Vector DB & AI-01&#xff08;20240210&#xf…

【JVM篇】ThreadLocal中为什么要使用弱引用

文章目录 &#x1f354;ThreadLocal中为什么要使用弱引用⭐总结 &#x1f354;ThreadLocal中为什么要使用弱引用 ThreadLocal可以在线程中存放线程的本地变量&#xff0c;保证数据的线程安全 ThreadLocal是这样子保存对象的&#xff1a; 在每个线程中&#xff0c;存放了一个…

以谷歌浏览器为例 讲述 JavaScript 断点调试操作用法

今天来说个比较实用的东西 用浏览器开发者工具 对 javaScript代码进行调试 我们先创建一个index.html 编写代码如下 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content&…

【每日一题】牛客网——链表分割

&#x1f4da;博客主页&#xff1a;爱敲代码的小杨. ✨专栏&#xff1a;《Java SE语法》 | 《数据结构与算法》 | 《C生万物》 ❤️感谢大家点赞&#x1f44d;&#x1f3fb;收藏⭐评论✍&#x1f3fb;&#xff0c;您的三连就是我持续更新的动力❤️ &#x1f64f;小杨水平有…

使用R语言fifer包进行分层采样

使用R语言fifer包中的stratified()函数用来进行分层采样非常方便&#xff0c;但fifer包已经从CRAN存储库中删除&#xff0c;需要从存档中下载可用的历史版本&#xff0c;下载链接&#xff1a;Index of /src/contrib/Archive/fifer (r-project.org)https://cran.r-project.org/s…

C# WinForm开发系列 - DataGridView

原文地址&#xff1a;https://www.cnblogs.com/peterzb/archive/2009/05/29/1491891.html 1.DataGridView实现课程表 testcontrol.rar 2.DataGridView二维表头及单元格合并 DataGridView单元格合并和二维表头.rar myMultiColHeaderDgv.rar 3.DataGridView单元格显示GIF图片 …

数据结构~~树(2024/2/8)

目录 树 1、定义&#xff1a; 2、树的基本术语&#xff1a; 3、树的表示 树 1、定义&#xff1a; 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树&…

大数据Doris(六十五):基于Apache Doris的数据中台2.0

文章目录 基于Apache Doris的数据中台2.0 一、​​​​​​​架构升级

数据库基础学习笔记

一.基础概念 数据库、数据库管理系统、SQL 主流数据库&#xff1a; mysql的安装&#xff1a;略 mysql图形化界面的安装&#xff1a;略 二.数据模型 1). 关系型数据库&#xff08;RDBMS&#xff09; 概念&#xff1a;建立在关系模型基础上&#xff0c;由多张相互连接的二维表…

01动力云客之环境准备+前端Vite搭建VUE项目入门+引入Element PLUS

1. 技术选型 前端&#xff1a;Html、CSS、JavaScript、Vue、Axios、Element Plus 后端&#xff1a;Spring Boot、Spring Security、MyBatis、MySQL、Redis 相关组件&#xff1a;HiKariCP&#xff08;Spring Boot默认数据库连接池&#xff09;、Spring-Data-Redis&#xff08;S…

【MySQL】-18 MySQL综合-4(MySQL储存引擎精讲+MySQL数据类型简介+MySQL整数类型+MySQL小数类型)

MySQL储存引擎精讲MySQL数据类型简介MySQL整数类型MySQL小数类型 十一 MySQL存储引擎精讲11.1 什么是存储引擎11.2 MySQL 5.7 支持的存储引擎11.3 如何选择 MySQL 存储引擎11.4 MySQL 默认存储引擎 十二 MySQL数据类型简介12.1 MySQL 常见数据类型1) 整数类型2) 日期/时间类型3…

用code去探索理解Llama架构的简单又实用的方法

除了白月光我们也需要朱砂痣 我最近也在反思&#xff0c;可能有时候算法和论文也不是每个读者都爱看&#xff0c;我也会在今后的文章中加点code或者debug模型的内容&#xff0c;也许还有一些好玩的应用demo&#xff0c;会提升这部分在文章类型中的比例 今天带着大家通过代码角度…

Hadoop:认识MapReduce

MapReduce是一个用于处理大数据集的编程模型和算法框架。其优势在于能够处理大量的数据&#xff0c;通过并行化来加速计算过程。它适用于那些可以分解为多个独立子任务的计算密集型作业&#xff0c;如文本处理、数据分析和大规模数据集的聚合等。然而&#xff0c;MapReduce也有…

Github 2024-02-11 开源项目日报Top10

根据Github Trendings的统计&#xff0c;今日(2024-02-11统计)共有10个项目上榜。根据开发语言中项目的数量&#xff0c;汇总情况如下&#xff1a; 开发语言项目数量Python项目4非开发语言项目2C项目1C项目1Solidity项目1JavaScript项目1Rust项目1HTML项目1 免费服务列表 | f…

shell脚本编译与解析

文章目录 shell变量全局变量&#xff08;环境变量&#xff09;局部变量设置PATH 环境变量修改变量属性 启动文件环境变量持久化 ./和. 的区别脚本编写重定向判断 和循环命令行参数传入参数循环读取命令行参数获取用户输入 处理选项处理简单选项处理带值选项 重定向显示并且同时…

【开源】基于JAVA+Vue+SpringBoot的实验室耗材管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 耗材档案模块2.2 耗材入库模块2.3 耗材出库模块2.4 耗材申请模块2.5 耗材审核模块 三、系统展示四、核心代码4.1 查询耗材品类4.2 查询资产出库清单4.3 资产出库4.4 查询入库单4.5 资产入库 五、免责说明 一、摘要 1.1…

为什么说 2023 年是 AI 视频生成的突破年?2024 年的 AI 视频生成有哪些值得期待的地方?

Diffusion Models视频生成-博客汇总 前言&#xff1a;2023年是 AI 视频生成的突破年&#xff0c;AI视频已经达到GPT-2级别了。去年我们取得了长足的进步&#xff0c;但距离普通消费者每天使用这些产品还有很长的路要走。视频的“ChatGPT时刻”何时到来&#xff1f; 目录 前言 …

计算机网络——06分组延时、丢失和吞吐量

分组延时、丢失和吞吐量 分组丢失和延时是怎样发生的 在路由器缓冲区的分组队列 分组到达链路的速率超过了链路输出的能力分组等待排到队头、被传输 延时原因&#xff1a; 当当前链路有别的分组进行传输&#xff0c;分组没有到达队首&#xff0c;就会进行排队&#xff0c;从…

SHA-512在Go中的实战应用: 性能优化和安全最佳实践

SHA-512在Go中的实战应用: 性能优化和安全最佳实践 简介深入理解SHA-512算法SHA-512的工作原理安全性分析SHA-512与SHA-256的比较结论 实际案例分析数据完整性验证用户密码存储数字签名总结 性能优化技巧1. 利用并发处理2. 避免不必要的内存分配3. 适当的数据块大小总结 与其他…

【JavaEE】_传输层协议UDP与TCP

目录 1. 开发中常见的数据组织格式 1.1 XML 1.2 JSON 1.3 Protobuf 2. 端口号 3. UDP协议 4. TCP协议 4.1 特点 4.2 TCP报文格式 4.3 TCP可靠性机制 4.3.1 确认应答机制 4.3.2 超时重传机制 4.3.2.1 丢包的两种情况 4.3.2.2 重传时间 4.3.3 连接管理机制 4.3.3…