转子:https://juejin.im/post/5bc9d8c75188255c9b13ebf1
github上k8s有着不同分支,注意必须和版本匹配:https://github.com/kubernetes/client-go
本介绍
Kubernetes官方从2016年8月份开始,将Kubernetes资源操作相关的核心源码抽取出来,独立出来一个项目Client-go,作为官方提供的Go client。Kubernetes的部分代码也是基于这个client实现的,所以对这个client的质量、性能等方面还是非常有信心的。
client-go是一个调用kubernetes集群资源对象API的客户端,即通过client-go实现对kubernetes集群中资源对象(包括deployment、service、ingress、replicaSet、pod、namespace、node等)的增删改查等操作。大部分对kubernetes进行前置API封装的二次开发都通过client-go这个第三方包来实现。
源码简介
What's included
- The
kubernetes
package contains the clientset to access Kubernetes API. - The
discovery
package is used to discover APIs supported by a Kubernetes API server. - The
dynamic
package contains a dynamic client that can perform generic operations on arbitrary Kubernetes API objects. - The
plugin/pkg/client/auth
packages contain optional authentication plugins for obtaining credentials from external sources. - The
transport
package is used to set up auth and start a connection. - The
tools/cache
package is useful for writing controllers.
Client结构
-
RESTClient:RESTClient是最基础的,相当于的底层基础结构,可以直接通过 是RESTClient提供的RESTful方法如Get(),Put(),Post(),Delete()进行交互
- 同时支持Json 和 protobuf
- 支持所有原生资源和CRDs
- 但是,一般而言,为了更为优雅的处理,需要进一步封装,通过Clientset封装RESTClient,然后再对外提供接口和服务
-
Clientset:Clientset是调用Kubernetes资源对象最常用的client,可以操作所有的资源对象,包含RESTClient。需要指定Group、指定Version,然后根据Resource获取
- 优雅的姿势是利用一个controller对象,再加上Informer
-
DynamicClient:Dynamic client 是一种动态的 client,它能处理 kubernetes 所有的资源。不同于 clientset,dynamic client 返回的对象是一个 map[string]interface{},如果一个 controller 中需要控制所有的 API,可以使用dynamic client,目前它在 garbage collector 和 namespace controller中被使用。
RESTClient
RESTClient 封装了指定资源URL的通用Kubernetes API的访问姿势
Clientset
/Users/meitu/Documents/work_Meitu/goDev/Applications/src/k8s.io/client-go/kubernetes/clientset.go
Clientset 是一系列的clients的group组合,注意每个group在一个Clientset中只包含一个版本。
Clientset包含了appsV1、coreV1,这中间包含了RESTClient,因此Clientset是基于RESTClient的。
dynamic client
dynamic client针对的是所有资源,但是只支持Json;
主要源码路径在:/Users/meitu/Documents/work_Meitu/goDev/Applications/src/k8s.io/client-go/dynamic
type ResourceInterface interface {
Create(obj *unstructured.Unstructured, options metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error)
Update(obj *unstructured.Unstructured, options metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error)
UpdateStatus(obj *unstructured.Unstructured, options metav1.UpdateOptions) (*unstructured.Unstructured, error)
Delete(name string, options *metav1.DeleteOptions, subresources ...string) error
DeleteCollection(options *metav1.DeleteOptions, listOptions metav1.ListOptions) error
Get(name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error)
List(opts metav1.ListOptions) (*unstructured.UnstructuredList, error)
Watch(opts metav1.ListOptions) (watch.Interface, error)
Patch(name string, pt types.PatchType, data []byte, options metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error)
}
Informer
Client-go包中一个相对较为高端的设计在于Informer的设计,我们知道我们可以直接通过Kubernetes API交互,但是考虑一点就是交互的形式,Informer设计为List/Watch的方式。Informer在初始化的时先通过List去从Kubernetes API中取出资源的全部object对象,并同时缓存,然后后面通过Watch的机制去监控资源,这样的话,通过Informer及其缓存,我们就可以直接和Informer交互而不是每次都和Kubernetes API交互。——有点类似线程池。
Informer另外一块内容在于提供了事件handler机制,并会触发回调,这样上层应用如Controller就可以基于回调处理具体业务逻辑。因为Informer通过List、Watch机制可以监控到所有资源的所有事件,因此只要给Informer添加ResourceEventHandler 实例的回调函数实例取实现OnAdd(obj interface{}) OnUpdate(oldObj, newObj interface{}) 和 OnDelete(obj interface{})
这三个方法,就可以处理好资源的创建、更新和删除操作
Kubernetes中都是各种controller的实现,各种controller都会用到Informer。
对象资源的操作接口
默认的每一种资源对象都有一个interface,封装了对象的CURD方法和list/watch方法
如 Deployment(/文件路径Users/meitu/Documents/work_Meitu/goDev/Applications/src/k8s.io/client-go/kubernetes/typed/apps/v1/deployment.go
):
type DeploymentInterface interface {
Create(*v1.Deployment) (*v1.Deployment, error)
Update(*v1.Deployment) (*v1.Deployment, error)
UpdateStatus(*v1.Deployment) (*v1.Deployment, error)
Delete(name string, options *metav1.DeleteOptions) error
DeleteCollection(options *metav1.DeleteOptions, listOptions metav1.ListOptions) error
Get(name string, options metav1.GetOptions) (*v1.Deployment, error)
List(opts metav1.ListOptions) (*v1.DeploymentList, error)
Watch(opts metav1.ListOptions) (watch.Interface, error)
Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1.Deployment, err error)
DeploymentExpansion
}
如Service(文件路径/Users/meitu/Documents/work_Meitu/goDev/Applications/src/k8s.io/client-go/kubernetes/typed/core/v1/service.go
)
// ServiceInterface has methods to work with Service resources.
type ServiceInterface interface {
Create(*v1.Service) (*v1.Service, error)
Update(*v1.Service) (*v1.Service, error)
UpdateStatus(*v1.Service) (*v1.Service, error)
Delete(name string, options *metav1.DeleteOptions) error
Get(name string, options metav1.GetOptions) (*v1.Service, error)
List(opts metav1.ListOptions) (*v1.ServiceList, error)
Watch(opts metav1.ListOptions) (watch.Interface, error)
Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1.Service, err error)
ServiceExpansion
}
这也就是说明,在Kubernetes中,所有对象资源的操作方式都是统一的,有个interface当做虚基类,包含资源的所有操作方法,然后各个子类继承然后实现它们,子类中的实现定义会针对不同的资源有不同诠释。
client-go中的设计思想
client-go 和 controller的架构设计
主要是两大块:
- client-go组件本身
- controller上层控制器
上图相对较为复杂,有很多细节,我自己结合源码的理解如下:
client-go组件
-
Reflector:通过Kubernetes API监控Kubernetes的资源类型
- 采用List、Watch机制
- 可以Watch任何资源包括CRD
- 添加object对象到FIFO队列,然后Informer会从队列里面取数据
-
Informer:controller机制的基础
- 循环处理object对象
- 从Reflector取出数据,然后将数据给到Indexer去缓存
- 提供对象事件的handler接口
- 循环处理object对象
-
Indexer:提供object对象的索引,是线程安全的,缓存对象信息
ontroller组件
-
Informer reference: controller需要创建合适的Informer才能通过Informer reference操作资源对象
-
Indexer reference: controller创建Indexer reference然后去利用索引做相关处理
-
Resource Event Handlers:Informer会回调这些handlers
-
Work queue: Resource Event Handlers被回调后将key写到工作队列
- 这里的key相当于事件通知,后面根据取出事件后,做后续的处理
-
Process Item:从工作队列中取出key后进行后续处理,具体处理可以通过Indexer reference
-
controller可以直接创建上述两个引用对象去处理,也可以采用工厂模式,官方都有相关示例
Factory:
在 client-go 中提供了一个 SharedInformerFactory 来简化 informer 的构建,具体代码在:
https://github.com/kubernetes/client-go/blob/v3.0.0/informers/factory.go#L54
Lister
Lister 是用来帮助我们访问本地 cache 的一个组件。
Workqueue
Workqueue 是一个简单的 queue 提供了以下的特性:
-
公平性:每个item 按顺序处理。
-
严格性:一个 item 不会被并发地多次处理,而且一个相同的 item 被多次加入 queue 的话也只会处理一次。
-
支持多个生产者和消费者:它允许一个正在被处理的 item 再次加入队列。
我们建议使用 RateLimitingQueue,它相比普通的 workqueue 多了以下的功能:
-
限流:可以限制一个 item 被 reenqueued 的次数。
-
防止 hot loop:它保证了一个 item 被 reenqueued 后,不会马上被处理。
Workqueue helper:
这里有一个 workqueue 的封装,来简化 queue 的操作,代码在以下位置:
https://github.com/caicloud/loadbalancer-controller/blob/master/pkg/util/controller/helper.go
控制流总结
我们来总结一个控制器的整体工作流程。
1. 创建一个控制器
-
为控制器创建 workqueue
-
创建 informer, 为 informer 添加 callback 函数,创建 lister
2. 启动控制器
-
启动 informer
-
等待本地 cache sync 完成后, 启动 workers
3. 当收到变更事件后,执行 callback
-
等待事件触发
-
从事件中获取变更的 Object
-
做一些必要的检查
-
生成 object key,一般是 namespace/name 的形式
-
将 key 放入 workqueue 中
4. worker loop
-
等待从 workqueue 中获取到 item,一般为 object key
-
用 object key 通过 lister 从本地 cache 中获取到真正的 object 对象
-
做一些检查
-
执行真正的业务逻辑
-
处理下一个 item
到这里已经讲完了一个完整的 Kubernetes 的 Controller 的构建过程。但是还想要多啰嗦几句关于 kubernetes 的设计原则和 API 习俗,它们是指导我们写出更加可靠的 Controller 的白皮书。
为 Kubernetes 拓展一个功能,实现一个 controller 是简单的。
但是设计一个系统,抽象出其中的设计哲学,更加值得我们学习和深思。
下面这个项目可以视为 controller 的一个例子:
https://github.com/caicloud/loadbalancer-controller
func main() {
klog.InitFlags(nil)
flag.Parse()
// set up signals so we handle the first shutdown signal gracefully
stopCh := signals.SetupSignalHandler()
cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig)
if err != nil {
klog.Fatalf("Error building kubeconfig: %s", err.Error())
}
kubeClient, err := kubernetes.NewForConfig(cfg)
if err != nil {
klog.Fatalf("Error building kubernetes clientset: %s", err.Error())
}
exampleClient, err := clientset.NewForConfig(cfg)
if err != nil {
klog.Fatalf("Error building example clientset: %s", err.Error())
}
kubeInformerFactory := kubeinformers.NewSharedInformerFactory(kubeClient, time.Second*30)
exampleInformerFactory := informers.NewSharedInformerFactory(exampleClient, time.Second*30)
controller := NewController(kubeClient, exampleClient,
kubeInformerFactory.Apps().V1().Deployments(),
exampleInformerFactory.Samplecontroller().V1alpha1().Foos())
// notice that there is no need to run Start methods in a separate goroutine. (i.e. go kubeInformerFactory.Start(stopCh)
// Start method is non-blocking and runs all registered informers in a dedicated goroutine.
kubeInformerFactory.Start(stopCh)
exampleInformerFactory.Start(stopCh)
if err = controller.Run(2, stopCh); err != nil {
klog.Fatalf("Error running controller: %s", err.Error())
}
}
func init() {
flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
flag.StringVar(&masterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.")
}