Kubernetes编程——client-go基础—— Informer 和缓存

Informer[ɪnˈfɔrmɚ] 和缓存

https://github.com/kubernetes/kubernetes/tree/release-1.27/staging/src/k8s.io/client-go/informers

  k8s 客户端接口中包含一个名叫 Watch 的动作,它提供了对集群对象变化(添加、删除或更新)进行响应的接口。Informer 在 Watch 的基础上对常见的使用场景提供了一个更高层的编程接口,包括:内存缓存以及通过名字对内存中的对象或属性进行查找的功能。

1、Informers

我理解意思是说:Kubernetes Informers 是 Kubernetes 的一种机制,用于监视集群中资源对象的变化。

Informers 通过向 Kubernetes API Server 发送 HTTP 请求获取资源对象的当前状态,并通过 Watch 机制实时监听对象的变化。当有新的资源对象被创建、更新或删除时,Informers 会接收到通知,并将变化的详情传递给注册的处理器。

Informers 提供了一种高级抽象,用于管理集群中的资源对象。通过使用 Informers,开发者无需手动编写轮询逻辑来检测变化,而是可以通过注册回调函数来响应变化。这使得开发者可以更加专注于业务逻辑的实现,而不需要处理资源对象的管理和变化监测。

Informers 可以用于任何 Kubernetes 资源对象的监测,例如 Pod、Service、Deployment 等。使用 Informers 可以方便地实现自动化的扩展、资源的同步和状态的通知等功能。

在使用 Informers 时,一般需要完成以下步骤:

    1. 创建 Kubernetes 的客户端对象,用于与 API Server 进行通信。
    2. 缩短了对资源对象的访问延迟,提高了应用程序的性能。
    3. 减少了与 API Server 的网络通信,提高了整体的稳定性。

通过使用 Kubernetes Informers,开发者可以更方便地编写自动化的控制器和运维工具,以及实现更高级的应用程序逻辑。

2、Informers 和缓存

我理解意思是说:在 Kubernetes 中,Informers 通常与缓存一起使用,以提高性能和降低对 API Server 的请求频率。

Informers 在初始化时会从 API Server 获取一次完整的资源对象列表,并将其缓存在内存中。随后,在 Informer 启动后,它会通过 Watch 机制监听资源对象的增删改事件,并实时更新缓存资源的对象状态。

缓存是一个本地内存数据结构,保存了资源对象的副本。它可以减少对 API Server 的频繁请求,提高资源对象的访问速度。当需要操作资源对象时, Informers 会先检查本地缓存中是否存在相应的对象,如果存在则直接返回缓存中的副本,而无需请求 API Server。具体而言,Informers 在缓存中实时更新以下对象:

    1. 新增的资源对象:当在 Kubernetes 群集中创建新的资源对象时,比如 Pod、Service、Deployment 等,Informer 会通过 Watch 机制监听到相应的事件,并将新增的对象添加到缓存中。
    2. 更新的资源对象:如果在 Kubernetes 上更新现有的资源对象,比如更新 Pod 的标签、修改 Deployment 的副本数量等,Informer 会监听到相应的事件,并将更新后的对象更新到缓存中。更新可能包括修改的字段、标签、注释等。
    3. 删除的资源对象:当在 Kubernetes 上删除资源对象时,比如删除一个 Pod、Service 等,Informer 会监听到相应的事件,并将被删除的对象从缓存中删除。

Informers 的缓存使用了简单的存储机制,以 Key-Value 对的形式保存资源对象。Key 是资源对象的唯一标识符,Value 是相应对象的副本。 使用 Informers 和缓存,可以实现以下优势:

    1. 减少对 API Server 的请求频率,降低了集群的负载。
    2. 缩短了对资源对象的访问延迟,提高了应用程序的性能。
    3. 减少了与 API Server 的网络通信,提高了整体的稳定性。

需要注意的是,缓存是有限的,它只会保存一定数量的资源对象。如果缓存中的对象已过期或被修改,Informers 将从 API Server 获取最新的对象,并更新到缓存中。因此,在使用 Informers 时,开发者应该意识到缓存可能会存在数据不一致性的问题,需要确保正确处理缓存中的对象更新和失效。

3、共享 Informer

使用 Informer 同样也会对 API 服务器带来一定的开销,一个程序对每个 GroupVersionKind(GVR) 只生成一个 Informer。在 Kubernetes 中,共享 Informer 工厂是一种将 Informer 实例进行复用的机制,以减少对 API 服务器的开销。共享 Informer 工厂可以在应用程序中创建和管理多个 Informer 实例,并确保每个 GroupVersionResource(GVR)只生成一个 Informer。共享 Informer 工厂的工作原理如下:

    1. 应用程序中的共享 Informer 工厂负责创建和管理 Informer 实例,并将它们与相应的 GVR 进行关联。
    2. 当应用程序需要监听某个 GVR 的资源对象时,共享 Informer 工厂首先检查是否已经存在与该 GVR 关联的 Informer 实例。
    3. 如果存在与 GVR 关联的 Informer,共享 Informer 工厂将返回该实例,并复用其已建立的连接和缓存。
    4. 如果不存在与 GVR 关联的 Informer,共享 Informer 工厂将创建一个新的 Informer 实例,并与该 GVR 进行关联。这样可以确保每个 GVR 只有一个对应的 Informer。

通过使用共享 Informer 工厂,可以避免重复创建和管理大量的 Informer 实例,从而减少对 API 服务器的开销。这种机制可以显著提高性能,并减轻集群负载。同时,共享 Informer 工厂还可以处理 Informer 实例之间的事件分发和状态更新,使应用程序能够及时响应和处理资源对象的变化。

总之,共享 Informer 工厂是一种可提供 Informer 实例复用的机制,通过减少对 API 服务器的连接和资源消耗,从而降低了应用程序的开销,并提高了性能和可伸缩性。

请使用共享 Informer 工厂来创建 Informer,而不要手工创建 Informer。使用工厂来创建的额外开销非常小,但如果不用工厂来创建共享 Informer,大概率会在某个地方发生为相同资源创建多个 Watch 连接的问题。

下面是一个示例代码,演示了如何使用 Kubernetes 的共享 Informer 工厂来处理添加、更新和删除事件。

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"

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

func main() {
    // 加载 Kubernetes 配置文件
    kubeconfig := "/root/.kube/config"
    config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
    if err != nil {
        panic(err)
    }

    // 创建 Kubernetes 客户端
    clientset, err := kubernetes.NewForConfig(config)
    if err != nil {
        panic(err)
    }

    // 创建共享 Informer 工厂
    // 使用 informer.NewSharedInformerFactory 方法创建一个共享 Informer 工厂。
    // 参数 clientset 是一个 kubernetes.Clientset 类型的对象,它用于与 Kubernetes API 进行通信,执行各种操作。
    // 这个 clientset 对象会被共享 Informer 工厂使用来与 Kubernetes API 进行交互。
    // 参数 time.Second*30 是一个时间间隔,用于指定共享 Informer 工厂在同步资源时的刷新间隔。
    // 在这个例子中,我们将刷新间隔设置为 30 秒,这意味着共享 Informer 工厂将每 30 秒从 Kubernetes API 中同步一次资源的状态。
    informerFactory := informers.NewSharedInformerFactory(clientset, time.Second*30)

    // 创建 Pod Informer
    // 在这行代码中,我们首先通过 informerFactory.Core() 来获取 CoreV1Informer,这是用于创建 Kubernetes 核心(Core)资源的 Informer 接口。
    // 然后,我们通过调用 V1() 来获取 v1Informer,这是 Kubernetes Core API 的 V1 版本 Informer。
    // 接下来,我们调用 Pods() 来创建 Pod 的 Informer,表示我们想要监听 Pod 资源的变化。
    // 通过创建 Pod 的 Informer,我们可以轻松地监听 Kubernetes 集群中 Pod 资源的添加、更新和删除事件,并定义相应的处理函数来处理这些事件。
    podInformer := informerFactory.Core().V1().Pods()

    // 定义添加事件处理函数
    // podInformer.Informer().AddEventHandler 方法用于将事件处理程序(event handler)添加到 Pod Informer 中。
    // cache.ResourceEventHandlerFuncs 结构体,并在其中定义了三个事件处理函数:AddFunc、UpdateFunc 和 DeleteFunc。
    podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
        // AddFunc 用于处理 Pod 添加事件。当有新的 Pod 被创建时,这个函数会被调用,并传入新增的 Pod 对象作为参数。
        // 在这个示例中,我们将该 Pod 的命名空间和名称打印出来,以表示该 Pod 已经被添加。
        AddFunc: func(obj interface{}) {
            // 处理 Pod 添加事件
            pod := obj.(*v1.Pod)
            fmt.Printf("Pod added: %s/%s\n", pod.Namespace, pod.Name)
        },

        // UpdateFunc 用于处理 Pod 更新事件。当已有的 Pod 发生更新时,这个函数会被调用,并传入旧的 Pod 对象和新的 Pod 对象作为参数。
        // 在这个示例中,我们将新的 Pod 的命名空间和名称打印出来,以表示该 Pod 已经被更新。
        UpdateFunc: func(oldObj, newObj interface{}) {
            // 处理 Pod 更新事件
            oldPod := oldObj.(*v1.Pod)
            newPod := newObj.(*v1.Pod)
            fmt.Printf("Pod updated: %s/%s\n", newPod.Namespace, newPod.Name)
        },
        // DeleteFunc 用于处理 Pod 删除事件。当 Pod 被删除时,这个函数会被调用,并传入被删除的 Pod 对象作为参数。
        // 在这个示例中,我们将被删除的 Pod 的命名空间和名称打印出来,以表示该 Pod 已经被删除。
        DeleteFunc: func(obj interface{}) {
            // 处理 Pod 删除事件
            pod := obj.(*v1.Pod)
            fmt.Printf("Pod deleted: %s/%s\n", pod.Namespace, pod.Name)
        },
    })

    // 创建了一个类型为 chan struct{} 的 stopCh 变量,用于控制 Informer 的启动和停止。当我们想要停止 Informer 时,可以向 stopCh 发送一个关闭信号。
    stopCh := make(chan struct{})

    // 这行代码用于启动 Informer。
    informerFactory.Start(stopCh)

    // 创建了一个类型为 chan os.Signal 的 signalCh 变量,用于接收操作系统的信号。
    signalCh := make(chan os.Signal, 1)

    //使用 signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM) 方法来注册我们感兴趣的信号类型。
    // 在这个例子中,我们注册了 SIGINT 和 SIGTERM 信号,分别表示用户发送的终止信号和进程终止信号。
    signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
    
    //<-signalCh 这行代码会阻塞当前线程,直到接收到一个信号。一旦收到信号,代码将从阻塞中恢复,继续执行后续的逻辑。
    <-signalCh

    // 停止 Informer
    close(stopCh)
}

为什么要同时使用 stopCh 和 signalCh 呢?

我理解意思是说:stopCh 是一个由程序自己创建的通道,用于主动地停止 Informer。当程序收到停止信号时,可以通过关闭 stopCh 来触发停止信号,进而停止 Informer 的运行。

而 signalCh 是一个由操作系统创建的信号通道,用于接收操作系统发出的信号。通过在 signalCh 上阻塞等待信号,可以实现在程序接收到操作系统的信号时,优雅地关闭程序,并进行一些必要的清理操作。

通过同时使用 stopCh 和 signalCh,可以使程序有两种方式来停止 Informer。当我们在终端中按下 Ctrl+C 时,会发送一个 SIGINT 信号,操作系统会将这个信号发送给程序。接着,signalCh 上的阻塞会从阻塞中解除,代码会执行后续的逻辑,包括关闭 stopCh,触发 stopCh 上的信号,以停止 Informer。而如果程序收到其他信号,比如 SIGTERM,程序也会以同样的方式处理。

posted @ 2023-06-29 11:10  左扬  阅读(737)  评论(0编辑  收藏  举报
levels of contents