0. 前言

在看 kube-scheduler 组件的过程中遇到了 kube-scheduler 对于 client-go 的调用,泛泛的理解调用过程总有种隔靴搔痒的感觉,于是调转头先把 client-go 理清楚在回来看 kube-scheduler

为什么要看 client-go,并且要深入到原理,源码层面去看。很简单,因为它很重要。重要在两方面:

  1. kubernetes 组件通过 client-gokube-apiserver 交互。
  2. client-go 简单,易用,大部分基于 Kubernetes 做二次开发的应用,在和 kube-apiserver 交互时会使用 client-go

当然,不仅在于使用,理解层面,对于我们学习代码开发,架构等也有帮助。

1. client-go 客户端对象

client-go 支持四种客户端对象,分别是 RESTClientClientSetDynamicClientDiscoveryClient

组件或者二次开发的应用可以通过这四种客户端对象和 kube-apiserver 交互。其中,RESTClient 是最基础的客户端对象,它封装了 HTTP Request,实现了 RESTful 风格的 APIClientSet 基于 RESTClient,封装了对于 ResourceVersion 的请求方法。DynamicClient 相比于 ClientSet 提供了全资源,包括自定义资源的请求方法。DiscoveryClient 用于发现 kube-apiserver 支持的资源组,资源版本和资源信息。

每种客户端适用的场景不同,主要是对 HTTP Request 做了层层封装,具体的代码实现可参考 client-go 客户端对象

2. informer 机制

仅仅封装 HTTP Request 是不够的,组件通过 client-gokube-apiserver 交互,必然对实时性,可靠性等有很高要求。试想,如果 ETCD 中存储的数据和组件通过 client-goETCD 获取的数据不匹配的话,那将会是一个非常严重的问题。

如何实现 client-go 的实时性,可靠性?client-go 给出的答案是:informer 机制。

                client-go informer 流程图

informer 机制的核心组件包括:

  • Reflector: 主要负责两类任务:

    1. 通过 client-go 客户端对象 list kube-apiserver 资源,并且 watch kube-apiserver 资源变更。
    2. 作为生产者,将获取的资源放入 Delta FIFO 队列。
  • Informer: 主要负责三类任务:
    1. 作为消费者,将 Reflector 放入队列的资源拿出来。
    2. 将资源交给 indexer 组件。
    3. 交给 indexer 组件之后触发回调函数,处理回调事件。
  • Indexer: indexer 组件负责将资源信息存入到本地内存数据库(实际是 map 对象),该数据库作为缓存存在,其资源信息和 ETCD 中的资源信息完全一致(得益于 watch 机制)。因此,client-go 可以从本地 indexer 中读取相应的资源,而不用每次都从 kube-apiserver 中获取资源信息。这也实现了 client-go 对于实时性的要求。

接下来从源码角度看各个组件的处理流程,力图做到知其然,知其所以然。

2 informer 源码分析

直接阅读 informer 源码是非常晦涩难懂的,这里通过 informer 的代码示例开始学习:

package main

import (
"log"
"time" v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/clientcmd"
) func main() {
// 解析 kubeconfig
config, err := clientcmd.BuildConfigFromFlags("", "/root/.kube/config")
if err != nil {
panic(err)
} // 创建 ClientSet 客户端对象
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
panic(err)
} stopCh := make(chan struct{})
defer close(stopCh) // 创建 sharedInformers
sharedInformers := informers.NewSharedInformerFactory(clientset, time.Minute)
// 创建 informer
informer := sharedInformers.Core().V1().Pods().Informer() // 创建 Event 回调 handler
informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
mObj := obj.(v1.Object)
log.Printf("New Pod Added to Store: %s", mObj.GetName())
},
UpdateFunc: func(oldObj, newObj interface{}) {
oObj := oldObj.(v1.Object)
nObj := newObj.(v1.Object)
log.Printf("%s Pod Updated to %s", oObj.GetName(), nObj.GetName())
},
DeleteFunc: func(obj interface{}) {
mObj := obj.(v1.Object)
log.Printf("Pod Deleted from Store: %s", mObj.GetName())
},
}) // 运行 informer
informer.Run(stopCh)
}

执行结果如下:

# go run informer.go
2023/12/14 12:00:26 New Pod Added to Store: prometheus-alertmanager-0
2023/12/14 12:01:26 prometheus-alertmanager-0 Pod Updated to prometheus-alertmanager-0

上述代码示例分为三部分:创建 informer,创建 informerEventHandler,运行 informer。下面,通过这三部分流程介绍 client-go 的核心组件。

2.1 创建 informer

创建 informer 分为两步。

1)创建工厂 sharedInformerFactory

// sharedInformers factory
sharedInformers := informers.NewSharedInformerFactory(clientset, time.Minute) // client-go/informers/factory.go
func NewSharedInformerFactory(client kubernetes.Interface, defaultResync time.Duration) SharedInformerFactory {
return NewSharedInformerFactoryWithOptions(client, defaultResync)
} func NewSharedInformerFactoryWithOptions(client kubernetes.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory {
factory := &sharedInformerFactory{
client: client,
namespace: v1.NamespaceAll,
defaultResync: defaultResync,
informers: make(map[reflect.Type]cache.SharedIndexInformer),
startedInformers: make(map[reflect.Type]bool),
customResync: make(map[reflect.Type]time.Duration),
} // Apply all options
for _, opt := range options {
factory = opt(factory)
} return factory
}

sharedInformerFactory 实现了 SharedInformerFactory 接口,该工厂负责创建 informer

2)创建 informer

// 创建 informer
informer := sharedInformers.Core().V1().Pods().Informer() // 调用 Core 方法
func (f *sharedInformerFactory) Core() core.Interface {
return core.New(f, f.namespace, f.tweakListOptions)
} func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
} // 调用 V1 方法
func (g *group) V1() v1.Interface {
return v1.New(g.factory, g.namespace, g.tweakListOptions)
} func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
} // 调用 Pods 方法
func (v *version) Pods() PodInformer {
return &podInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
}

经过层层构建创建 podInformer 对象,该对象实现了 PodInformer 接口,调用接口的 Informer 方法创建 informer 对象:

func (f *podInformer) Informer() cache.SharedIndexInformer {
return f.factory.InformerFor(&corev1.Pod{}, f.defaultInformer)
}

podInformer.Informer 实际调用的是 sharedInformerFactory.InformerFor

func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer {
f.lock.Lock()
defer f.lock.Unlock() // 反射出资源对象 obj 的 type
informerType := reflect.TypeOf(obj) // 读取并判断资源对象的 informer
informer, exists := f.informers[informerType]
if exists {
return informer
} ... // 调用 newFunc 创建 informer
informer = newFunc(f.client, resyncPeriod) // 将 type:informer 加入到 factory 的 informers 中
f.informers[informerType] = informer return informer
}

InformerFor 方法可以看出,sharedInformerFactory 的 share 体现在同一个资源类型共享 informer

这么设计在于,每个 informer 包括一个 ReflectorReflector 通过访问 kube-apiserver 实现 ListAndWatch 操作。共享 informer 实际是共享 Reflector,这种共享机制将减少 Reflector 对于 kube-apiserver 的访问,降低 kube-apiserver 的负载,节约资源。

继续看,创建 informernewFunc 函数做了什么:

informer = newFunc(f.client, resyncPeriod)

// client-go/informers/core/v1/pod.go
func (f *podInformer) defaultInformer(client kubernetes.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredPodInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
} func NewFilteredPodInformer(client kubernetes.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
&cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.CoreV1().Pods(namespace).List(context.TODO(), options)
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.CoreV1().Pods(namespace).Watch(context.TODO(), options)
},
},
&corev1.Pod{},
resyncPeriod,
indexers,
)
}

newFunc 实际调用的是 NewFilteredPodInformer 函数,在函数内创建 cache.ListAndWatch 对象,对象中包括 ListFuncWatchFunc 回调函数,回调函数内调用 ClientSet 实现 list 和 watch 资源对象。

继续看 cache.NewSharedIndexInformer

// client-go/tools/cache/shared_informer.go
func NewSharedIndexInformer(lw ListerWatcher, exampleObject runtime.Object, defaultEventHandlerResyncPeriod time.Duration, indexers Indexers) SharedIndexInformer {
return NewSharedIndexInformerWithOptions(
lw,
exampleObject,
SharedIndexInformerOptions{
ResyncPeriod: defaultEventHandlerResyncPeriod,
Indexers: indexers,
},
)
} func NewSharedIndexInformerWithOptions(lw ListerWatcher, exampleObject runtime.Object, options SharedIndexInformerOptions) SharedIndexInformer {
realClock := &clock.RealClock{} return &sharedIndexInformer{
indexer: NewIndexer(DeletionHandlingMetaNamespaceKeyFunc, options.Indexers),
processor: &sharedProcessor{clock: realClock},
listerWatcher: lw,
objectType: exampleObject,
objectDescription: options.ObjectDescription,
resyncCheckPeriod: options.ResyncPeriod,
defaultEventHandlerResyncPeriod: options.ResyncPeriod,
clock: realClock,
cacheMutationDetector: NewCacheMutationDetector(fmt.Sprintf("%T", exampleObject)),
}
}

NewSharedIndexInformerWithOptions 函数内创建 informer sharedIndexInformer。可以看到,sharedIndexInformer 内包括了 indexer 核心组件。

informer 创建完成。接下来为 informer 添加回调函数 EventHandler

2.2 创建 EventHandler

代码实现如下:

informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
mObj := obj.(v1.Object)
log.Printf("New Pod Added to Store: %s", mObj.GetName())
},
UpdateFunc: func(oldObj, newObj interface{}) {
oObj := oldObj.(v1.Object)
nObj := newObj.(v1.Object)
log.Printf("%s Pod Updated to %s", oObj.GetName(), nObj.GetName())
},
DeleteFunc: func(obj interface{}) {
mObj := obj.(v1.Object)
log.Printf("Pod Deleted from Store: %s", mObj.GetName())
},
})

创建 EventHandlerhandler 中包括三种回调函数:AddFuncUpdateFuncDeleteFunc,三种回调函数分别在资源有增加,变更,删除时触发。

sharedIndexInformer.AddEventHandler 内,将 handler 传递给 sharedIndexInformer.AddEventHandlerWithResyncPeriod 方法,该方法主要创建 listener 对象:

// client-go/tools/cache/shared_informer.go
func (s *sharedIndexInformer) AddEventHandler(handler ResourceEventHandler) (ResourceEventHandlerRegistration, error) {
return s.AddEventHandlerWithResyncPeriod(handler, s.defaultEventHandlerResyncPeriod)
} func (s *sharedIndexInformer) AddEventHandlerWithResyncPeriod(handler ResourceEventHandler, resyncPeriod time.Duration) (ResourceEventHandlerRegistration, error) {
...
listener := newProcessListener(handler, resyncPeriod, determineResyncPeriod(resyncPeriod, s.resyncCheckPeriod), s.clock.Now(), initialBufferSize, s.HasSynced) if !s.started {
return s.processor.addListener(listener), nil
}
...
} // client-go/tools/cache/shared_informer.go
func newProcessListener(handler ResourceEventHandler, requestedResyncPeriod, resyncPeriod time.Duration, now time.Time, bufferSize int, hasSynced func() bool) *processorListener {
ret := &processorListener{
nextCh: make(chan interface{}),
addCh: make(chan interface{}),
handler: handler,
syncTracker: &synctrack.SingleFileTracker{UpstreamHasSynced: hasSynced},
pendingNotifications: *buffer.NewRingGrowing(bufferSize),
requestedResyncPeriod: requestedResyncPeriod,
resyncPeriod: resyncPeriod,
} ret.determineNextResync(now) return ret
} func (p *sharedProcessor) addListener(listener *processorListener) ResourceEventHandlerRegistration {
... p.listeners[listener] = true
... return listener
}

listener 对象包含通道 addChnextCh,以及 handler 等对象。最后将 listener 存入 sharedIndexInformer.sharedProcessor 中。

创建完 informerEventHandler,接下来该运行 informer 了。


Kubernetes: client-go 源码剖析(一)的更多相关文章

  1. kubernetes dashboard backend源码剖析

    dashboard架构主要由一个API handler 和 五个manager构成: API handler用来处理来自客户的http请求,不同的path路由到不同的的handler处理,使用的是go ...

  2. 自己实现多线程的socket,socketserver源码剖析

    1,IO多路复用 三种多路复用的机制:select.poll.epoll 用的多的两个:select和epoll 简单的说就是:1,select和poll所有平台都支持,epoll只有linux支持2 ...

  3. 玩转Android之Picasso使用详详详详详详解,从入门到源码剖析!!!!

    Picasso是Squareup公司出的一款图片加载框架,能够解决我们在Android开发中加载图片时遇到的诸多问题,比如OOM,图片错位等,问题主要集中在加载图片列表时,因为单张图片加载谁都会写.如 ...

  4. (升级版)Spark从入门到精通(Scala编程、案例实战、高级特性、Spark内核源码剖析、Hadoop高端)

    本课程主要讲解目前大数据领域最热门.最火爆.最有前景的技术——Spark.在本课程中,会从浅入深,基于大量案例实战,深度剖析和讲解Spark,并且会包含完全从企业真实复杂业务需求中抽取出的案例实战.课 ...

  5. Appuim源码剖析(Bootstrap)

    Appuim源码剖析(Bootstrap) SkySeraph Jan. 26th 2017 Email:skyseraph00@163.com 更多精彩请直接访问SkySeraph个人站点:www. ...

  6. 老李推荐: 第14章2节《MonkeyRunner源码剖析》 HierarchyViewer实现原理-HierarchyViewer架构概述

    老李推荐: 第14章2节<MonkeyRunner源码剖析> HierarchyViewer实现原理-HierarchyViewer架构概述   HierarchyViewer库的引入让M ...

  7. 豌豆夹Redis解决方案Codis源码剖析:Dashboard

    豌豆夹Redis解决方案Codis源码剖析:Dashboard 1.不只是Dashboard 虽然名字叫Dashboard,但它在Codis中的作用却不可小觑.它不仅仅是Dashboard管理页面,更 ...

  8. 豌豆夹Redis解决方案Codis源码剖析:Proxy代理

    豌豆夹Redis解决方案Codis源码剖析:Proxy代理 1.预备知识 1.1 Codis Codis就不详细说了,摘抄一下GitHub上的一些项目描述: Codis is a proxy base ...

  9. jdk源码剖析一:OpenJDK-Hotspot源码包目录结构

    开启正文之前,先说一下源码剖析这一系列,就以“死磕到底”的精神贯彻始终,JDK-->JRE-->JVM(以openJDK代替) 最近想看看JDK8源码,但JDK中JVM(安装在本地C:\P ...

  10. Django----djagorest-framwork源码剖析

    restful(表者征状态转移,面向资源编程)------------------------------------------->约定 从资源的角度审视整个网络,将分布在网络中某个节点的资源 ...

随机推荐

  1. 【Bash】rm -r 与 rmdir 区别

    目录 背景 二者区别 rmdir rm -r rm -rf 测试过程 配置环境 rmdir rm -r rm -rf 参考资料 背景 今天学弟在使用 NVMe-over-TCP 时发现无法卸载 nvm ...

  2. 论文解读(WDGRL)《Wasserstein Distance Guided Representation Learning for Domain Adaptation》

    Note:[ wechat:Y466551 | 可加勿骚扰,付费咨询 ] 论文信息 论文标题:Wasserstein Distance Guided Representation Learning f ...

  3. Solution Set -「CF 1485」

    「CF 1485A」Add and Divide Link. 贪心.枚举 \([b,b+\log_{2}\text{range}]\) 然后取个 \(\min\). #include<cstdi ...

  4. CopyOnWriteArrayList 写时复制思想

    写时复制 conpyOnWrite容器即写时复制容器.往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行Copy,复制出一个新的容器Object[] ...

  5. ModuleNotFoundError: No module named 'qrcode'

    W File "/usr/lib/python3/dist-packages/pip/_internal/resolution/resolvelib/candidates.py", ...

  6. Java也能做OCR!SpringBoot 整合 Tess4J 实现图片文字识别

    前言 今天给大家分享一个SpringBoot整合Tess4j库实现图片文字识别的小案例,希望xdm喜欢. 文末有案例代码的Git地址,可以自己下载了去玩玩儿或继续扩展也行. 话不多说,开整吧. 什么是 ...

  7. 如何基于three.js(webgl)引擎架构,研发一套通过配置就能自动生成的3D机房系统

    序: 这几年观察下来,大部分做物联网三维可视化解决方案的企业或个人, 基本都绕不开3D机房.包括前面也讲过这样的案例<使用webgl(three.js)创建自动化抽象化3D机房,3D机房模块详细 ...

  8. 从输入URL到页面加载完都发生了什么

    1.浏览器的地址栏输入URL并按下回车. 2.浏览器查找当前URL是否存在缓存,并比较缓存是否过期. 3.DNS解析URL对应的IP. 4.根据IP建立TCP连接(三次握手). 5.HTTP发起请求. ...

  9. iframe用的场景

    iframe可用在以下几个场景中: 1:典型系统结构,左侧是功能树,右侧就是一些常见的table或者表单之类的.为了每一个功能,单独分离出来,采用iframe. 2:ajax上传文件. 3:加载别的网 ...

  10. 《最新出炉》系列初窥篇-Python+Playwright自动化测试-22-处理select下拉框-上篇

    1.简介 在实际自动化测试过程中,我们也避免不了会遇到下拉框选择的测试,因此宏哥在这里直接分享和介绍一下,希望小伙伴或者童鞋们在以后工作中遇到可以有所帮助.今天,我们讲下playwright的下拉框怎 ...