kubernetes控制器(一)

Clientset 是调用 Kubernetes 资源对象最常用的客户端,可以操作所有的资源对象,下面我们使用clientset获取集群中的pod和deployment消息

package main

import (
	"context"
	"flag"
	"fmt"
	"os"
	"path/filepath"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
)

func main() {
	var err error
	var config *rest.Config
	var kubeconfig *string

	if home := homeDir(); home != "" {
		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "kubeconfig file")
	} else {
		kubeconfig = flag.String("kubeconfig", "", "kubeconfig file")
	}
	flag.Parse()

	//使用servicecount 创建集群配置
	//InClusterConfig返回一个配置对象,该对象使用kubernetes提供给pod的服务帐户
	//它适用于希望在kubernetes上运行的pod内运行的客户端。如果从不在kubernetes环境中运行的进程调用,
	//它将返回ErrNotInCluster。
	if config, err = rest.InClusterConfig(); err != nil {
		//BuildConfigFromFlags是一个辅助函数,它从主url或kubecconfig文件路径构建配置。它们作为集群组件的命令行标志传入。
		//警告应该反映这种用法。如果masterUrl和kubeconfigPath都没有传入,则返回到inClusterConfig。
		//如果inClusterConfig失败,我们回退到默认配置。
		if config, err = clientcmd.BuildConfigFromFlags("", *kubeconfig); err != nil {
			panic(err.Error())
		}

	}

	//创建clientset

	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		panic(err.Error())
	}

	//使用clientset获取deployment, kube-system下的pod
	deployment, err := clientset.AppsV1().Deployments("default").List(context.Background(), metav1.ListOptions{})
	if err != nil {
		panic(err)
	}

	pods, err := clientset.CoreV1().Pods("kube-system").List(context.Background(), metav1.ListOptions{})
	if err != nil {
		panic(err)
	}

	for i, pod := range pods.Items {
		fmt.Printf("%d -> %s", i, pod.Name)
	}

	for i, deploy := range deployment.Items {
		fmt.Printf("%d -> %s\n", i, deploy.Name)
	}

}

func homeDir() string {
	if h := os.Getenv("HOME"); h != "" {
		return h
	}
	return os.Getenv("USERPROFILE")
}

下面我们就分析这个图
在这里插入图片描述一个控制器每次需要获取对象的时候都要访问 APIServer,这会给系统带来很高的负载,Informers 的内存缓存就是来解决这个问题的,此外 Informers 还可以几乎实时的监控对象的变化,而不需要轮询请求,这样就可以保证客户端的缓存数据和服务端的数据一致,就可以大大降低 APIServer 的压力了。
在这里插入图片描述如上图展示了 Informer 的基本处理流程:

  • 以 events 事件的方式从 APIServer 获取数据
  • 提供一个类似客户端的 Lister 接口,从内存缓存中 get 和 list 对象
  • 为添加、删除、更新注册事件处理程序

此外 Informers 也有错误处理方式,当长期运行的 watch 连接中断时,它们会尝试使用另一个 watch 请求来恢复连接,在不丢失任何事件的情况下恢复事件流。如果中断的时间较长,而且 APIServer 丢失了事件(etcd 在新的 watch 请求成功之前从数据库中清除了这些事件),那么 Informers 就会重新 List 全量数据。

而且在重新 List 全量操作的时候还可以配置一个重新同步的周期参数,用于协调内存缓存数据和业务逻辑的数据一致性,每次过了该周期后,注册的事件处理程序就将被所有的对象调用,通常这个周期参数以分为单位,比如10分钟或者30分钟。

注意:重新同步是纯内存操作,不会触发对服务器的调用。

Informers 的这些高级特性以及超强的鲁棒性,都足以让我们不去直接使用客户端的 Watch() 方法来处理自己的业务逻辑,而且在 Kubernetes 中也有很多地方都有使用到 Informers。但是在使用 Informers 的时候,通常每个 GroupVersionResource(GVR)只实例化一个 Informers,但是有时候我们在一个应用中往往有使用多种资源对象的需求,这个时候为了方便共享 Informers,我们可以通过使用共享 Informer 工厂来实例化一个 Informer。

共享 Informer 工厂允许我们在应用中为同一个资源共享 Informer,也就是说不同的控制器循环可以使用相同的 watch 连接到后台的 APIServer,例如,kube-controller-manager 中的控制器数据量就非常多,但是对于每个资源(比如 Pod),在这个进程中只有一个 Informer。
我们来创建一个用于获取 Deployment 的共享 Informer,代码如下所示:

package main

import (
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"time"

	v1 "k8s.io/api/apps/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/client-go/informers"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/cache"
	"k8s.io/client-go/tools/clientcmd"
)

func main() {
	var err error
	var config *rest.Config
	var kubeconfig *string

	if home := homeDir(); home != "" {
		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "kubeconfig file")
	} else {
		kubeconfig = flag.String("kubeconfig", "", "kubeconfig file")
	}
	flag.Parse()

	if config, err = rest.InClusterConfig(); err != nil {
		if config, err = clientcmd.BuildConfigFromFlags("", *kubeconfig); err != nil {
			panic(err.Error())
		}
	}

	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		panic(err)
	}

	//初始化informer factory
	informerFactory := informers.NewSharedInformerFactory(clientset, time.Second*30)

	//对deployment监听
	deploymentInformer := informerFactory.Apps().V1().Deployments()

	//创建informer
	informer := deploymentInformer.Informer()

	//创建lister
	deployLister := deploymentInformer.Lister()

	//注册事件处理程序
	informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc:    onAdd,
		UpdateFunc: onUpdate,
		DeleteFunc: onDelete,
	})

	stopper := make(chan struct{})
	defer close(stopper)

	//启动informer, list & watch
	informerFactory.Start(stopper)

	//等待所有启动的informer 的缓存被同步
	informerFactory.WaitForCacheSync(stopper)

	//从本地缓存中获取default中的所有deployment 列表
	deployments, err := deployLister.Deployments("default").List(labels.Everything())
	if err != nil {
		panic(err)
	}

	for i, deploy := range deployments {
		fmt.Printf("%d -> %s\n", i, deploy.Name)
	}
	<-stopper
}

func onUpdate(old, new interface{}) {
	oldDeploy := old.(*v1.Deployment)
	newDeploy := new.(*v1.Deployment)
	fmt.Println("update deployment", oldDeploy.Name, newDeploy.Name)
}

func onAdd(obj interface{}) {
	deploy := obj.(*v1.Deployment)
	fmt.Println("add a deployment:", deploy.Name)
}

func onDelete(obj interface{}) {
	deploy := obj.(*v1.Deployment)
	fmt.Println("delete a deployment", deploy.Name)
}

func homeDir() string {
	if h := os.Getenv("HOME"); h != "" {
		return h
	}
	return os.Getenv("USERPROFILE")
}

这是因为我们首先通过 Informer 注册了事件处理程序,这样当我们启动 Informer 的时候首先会将集群的全量 Deployment 数据同步到本地的缓存中,会触发 AddFunc 这个回调函数,然后我们又在下面使用 Lister() 来获取 default 命名空间下面的所有 Deployment 数据,这个时候的数据是从本地的缓存中获取的,所以就看到了上面的结果,由于我们还配置了每30s重新全量 List 一次,所以正常每30s我们也可以看到所有的 Deployment 数据出现在 UpdateFunc 回调函数下面,我们也可以尝试去删除一个 Deployment,同样也会出现对应的 DeleteFunc 下面的事件。
下面图片是informer内部机制
在这里插入图片描述Reflector(反射器)

Reflector 用于监控(Watch)指定的 Kubernetes 资源,当监控的资源发生变化时,触发相应的变更事件,例如 Add 事件、Update 事件、Delete 事件,并将其资源对象存放到本地缓存 DeltaFIFO 中。

DeltaFIFO

DeltaFIFO 是一个生产者-消费者的队列,生产者是 Reflector,消费者是 Pop 函数,FIFO 是一个先进先出的队列,而 Delta 是一个资源对象存储,它可以保存资源对象的操作类型,例如 Add 操作类型、Update 操作类型、Delete 操作类型、Sync 操作类型等。

Indexer

Indexer 是 client-go 用来存储资源对象并自带索引功能的本地存储,Informer(sharedIndexInformer) 从 DeltaFIFO 中将消费出来的资源对象存储至 Indexer。Indexer 与 Etcd 集群中的数据保持完全一致。这样我们就可以很方便地从本地存储中读取相应的资源对象数据,而无须每次从远程 APIServer 中读取,以减轻服务器的压力。

这里理论知识太多,直接去查看源码显得有一定困难,我们可以用一个实际的示例来进行说明,比如现在我们删除一个 Pod,一个 Informers 的执行流程是怎样的:

  1. 首先初始化 Informer,Reflector 通过 List 接口获取所有的 Pod 对象
  2. Reflector 拿到所有 Pod 后,将全部 Pod 放到 Store(本地缓存)中
  3. 如果有人调用 Lister 的 List/Get 方法获取 Pod,那么 Lister 直接从 Store 中去拿数据
  4. Informer 初始化完成后,Reflector 开始 Watch Pod 相关的事件
  5. 此时如果我们删除 Pod1,那么 Reflector 会监听到这个事件,然后将这个事件发送到 DeltaFIFO 中
  6. DeltaFIFO 首先先将这个事件存储在一个队列中,然后去操作 Store 中的数据,删除其中的 Pod1
  7. DeltaFIFO 然后 Pop 这个事件到事件处理器(资源事件处理器)中进行处理
  8. LocalStore 会周期性地把所有的 Pod 信息重新放回 DeltaFIFO 中去

指南

当你在编写控制器时,有一些准则将有助于确保你得到你需要的结果和。

  • 一次只操作一个元素。如果你使用了 workqueue.Interface,你可以将某个资源的变化排成队列,随后将它们弹到多个 "worker " gofuncs 中,并保证没有两个 gofuncs 会同时对同一个元素进行操作。

  • 很多控制器必须触发掉多个资源(我需要 “如果Y发生变化就检查X”),但几乎所有的控制器都可以根据关系将这些资源折叠成一个 "检查这个X "的队列。例如,ReplicaSet 控制器需要对一个 Pod 被删除做出反应,但它通过找到相关的 ReplicaSets 并对这些进行排队来实现。

  • 资源之间的随机排序。当控制器排队关闭多种类型的资源时,无法保证这些资源之间的排序。

  • 不同的 watches 是独立更新的。即使有 "创建的资源A/X "和 "创建的资源B/Y "的客观排序,你的控制器也可以观察到 "创建的资源B/Y "和 “创建的资源A/X”。

  • 级别驱动,而不是边缘驱动。就像有一个 shell 脚本不是一直在运行一样,你的控制器可能会在再次运行之前关闭不确定的时间。

  • 如果一个 API 对象出现的标记值为 true,你不能指望已经看到它从false 变成 true,只能说你现在观察到它是 true。即使是 API watch 也会受到这个问题的影响,所以要确保你不指望看到变化,除非你的控制器也在对象的状态中标记它最后做出决定的信息。

  • 使用 SharedInformers。SharedInformers 提供了回调函数来接收特定资源的添加、更新和删除的通知,它们还提供了访问共享缓存和确定缓存何时启动的便利功能。

    使用 https://git.k8s.io/kubernetes/staging/src/k8s.io/client-go/informers/factory.go 中的工厂方法来确保你和其他人共享同一个缓存实例。

    这样我们就大大减少了 APIServer 的连接以及重复的序列化、重复的反序列化、重复的缓存等成本。
    你可能会看到其他机制,比如反射器和 DeltaFIFO 驱动控制器。这些都是旧的机制,我们后来用它们来构建 SharedInformers,你应该避免在新控制器中使用它们。

package main

import (
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"time"

	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/fields"
	"k8s.io/apimachinery/pkg/util/runtime"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/cache"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/workqueue"
	"k8s.io/klog/v2"
)

//定义一个controller

type Controller struct {
	indexer  cache.Indexer
	queue    workqueue.RateLimitingInterface
	informer cache.Controller
}

func NewController(queue workqueue.RateLimitingInterface, indexer cache.Indexer, informer cache.Controller) *Controller {
	return &Controller{
		indexer:  indexer,
		informer: informer,
		queue:    queue,
	}
}

func (c *Controller) processNextItem() bool {
	//等到工作队列中有一个新元素
	key, quit := c.queue.Get()
	if quit {
		return false
	}

	defer c.queue.Done(key)

	//调用包含业务逻辑的方法
	err := c.syncToStdout(key.(string))

	//如果在执行业务逻辑期间出现错误,处理错误
	if err != nil {
		c.handleErr(err, key)
	}
	return true
}

func (c *Controller) syncToStdout(key string) error {
	//从本地存储中获取key对应的对象
	obj, exists, err := c.indexer.GetByKey(key)
	if err != nil {
		klog.Errorf("fetching object with key %s from store failed with %v", key, err)
		return err
	}

	if !exists {
		fmt.Printf("pod %s, does not exists\n", key)
	} else {
		fmt.Printf("sync/add/update for pod %s\n", obj.(*v1.Pod).GetName())
	}

	return nil
}

//检查是否发生错误, 并确保我们稍后重试
func (c *Controller) handleErr(err error, key interface{}) {
	if err == nil {
		// 忘记每次成功同步时 key 的#AddRateLimited历史记录。
		// 这样可以确保不会因过时的错误历史记录而延迟此 key 更新的以后处理。
		c.queue.Forget(key)
		return
	}

	//如果出现了问题,此控制器重试5次

	if c.queue.NumRequeues(key) < 5 {
		klog.Infof("error syncing pod %v: %v", key, err)
		//重新加入key 到限速队列, 稍后将再次处理该key
		c.queue.AddRateLimited(key)
		return
	}

	//Forget表示一个项目已经完成,正在被重试。不管是烫发失败还是成功,我们都会阻止速率限制器跟踪它。
	//这只会清除' rateLimiter ',你仍然需要在队列上调用' Done '。
	c.queue.Forget(key)

	//HandlerError是一个方法,当非面向用户的代码段不能返回错误并且需要指出它已被忽略时调用。
	//调用此方法比记录错误更可取——默认行为是记录,但错误可能被发送到远程服务器进行分析。
	runtime.HandleError(err)
	klog.Infof("dropping pod %q out of the queue: %v", key, err)
}

// Run 开始watch 同步

func (c *Controller) Run(threadiness int, stopCh chan struct{}) {
	//HandleCrash只是捕获崩溃并记录错误。意味着通过defer调用。可以提供其他特定于上下文的处理程序,
	//在出现紧急情况时将调用这些处理程序。HandleCrash实际上在调用处理程序并记录恐慌消息后崩溃。
	//例如,你可以提供一个或多个额外的处理程序,比如优雅地关闭go例程。
	defer runtime.HandleCrash()

	//停止控制器后关闭队列
	defer c.queue.ShutDown()
	klog.Info("start pod controller")

	// 启动
	go c.informer.Run(stopCh)

	//等待所有相关的缓存同步, 然后再开始处理队列的项目

	//WaitForCacheSync等待缓存填充。如果成功返回true,
	//如果控制器应该关闭则返回false调用者应该选择WaitForNamedCacheSync()
	if !cache.WaitForCacheSync(stopCh, c.informer.HasSynced) {
		runtime.HandleError(fmt.Errorf("time out waiting for caches to sync"))
		return
	}

	for i := 0; i < threadiness; i++ {
		//直到循环直到停止通道关闭,每个周期运行f。
		//Until是JitterUntil的语法糖,带有零抖动因子和滑动= true(这意味着周期的计时器在f完成后开始)。
		go wait.Until(c.runWorker, time.Second, stopCh)
	}

	<-stopCh
	klog.Info("stopping pod controller")

}

func (c *Controller) runWorker() {
	for c.processNextItem() {

	}
}

func initClient() (*kubernetes.Clientset, error) {
	var err error
	var config *rest.Config
	var kubeconfig *string

	if home := homeDir(); home != "" {
		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "kubeconfig 文件的绝对路径")
	} else {
		kubeconfig = flag.String("kubeconfig", "", "kubeconfig 文件的绝对路径")
	}
	flag.Parse()

	if config, err = rest.InClusterConfig(); err != nil {
		if config, err = clientcmd.BuildConfigFromFlags("", *kubeconfig); err != nil {
			panic(err.Error())
		}
	}

	return kubernetes.NewForConfig(config)
}

func homeDir() string {
	if h := os.Getenv("HOME"); h != "" {
		return h
	}

	return os.Getenv("USERPROFILE")
}

func main() {
	clientset, err := initClient()
	if err != nil {
		klog.Fatal(err)
	}

	//创建pod listwatcher
	podListWatcher := cache.NewListWatchFromClient(clientset.CoreV1().RESTClient(), "pods", v1.NamespaceDefault, fields.Everything())

	//创建队列
	//NewRateLimitingQueue构造了一个新的工作队列,具有rateLimited排队能力。如果你不这样做,你可能会永远跟踪失败。NewRateLimitingQueue不发出度量。
	//对于与MetricsProvider一起使用,请使用NewRateLimitingQueueWithConfig并指定一个名称。
	queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())

	// 在 informer 的帮助下,将工作队列绑定到缓存
	// 这样,我们确保无论何时更新缓存,都将 pod key 添加到工作队列中
	// 注意,当我们最终从工作队列中处理元素时,我们可能会看到 Pod 的版本比响应触发更新的版本新

	indexer, informer := cache.NewIndexerInformer(podListWatcher, &v1.Pod{}, 0, cache.ResourceEventHandlerFuncs{
		AddFunc: func(obj interface{}) {
			//MetaNamespaceKeyFunc是一个方便的默认KeyFunc,它知道如何为实现meta.Interface的API对象制作键。
			//该键使用<namespace>/<name>格式,除非<namespace>为空,否则它就是<name>。
			key, err := cache.MetaNamespaceKeyFunc(obj)
			if err == nil {
				queue.Add(key)
			}
		},

		UpdateFunc: func(old interface{}, new interface{}) {
			key, err := cache.MetaNamespaceKeyFunc(new)
			if err == nil {
				queue.Add(key)
			}
		},

		DeleteFunc: func(obj interface{}) {
			key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
			if err == nil {
				queue.Add(key)
			}
		},
	}, cache.Indexers{})

	controller := NewController(queue, indexer, informer)

	indexer.Add(&v1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "mypod",
			Namespace: v1.NamespaceDefault,
		},
	})

	stopCh := make(chan struct{})
	defer close(stopCh)

	go controller.Run(1, stopCh)

	select {}
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值