Controller 和 Operator 没有太大区别,一般称对 k8s 原生资源的控制叫为 Controller,对于自定义资源(CRD)的控制较为 Operator
首先我们通过一个简单的例子了解以下几个概念(类型,API 对象,资源对象)
首先把 k8s 想象成一个支持任意美食制作的生产线,但目前 k8s 只提供一些做基础美食的「厨师」(即为 Pod),若要做高级美食,则需要我们进行「亲自指导」(Operator、Controller ,这两个概念可以理解为一样,都是指导)
现在我想让 k8s 做一种高级的美食 —— 「戚风蛋糕」(一种新的资源)
为了支持「戚风蛋糕」(一种新的资源)制作,那我们要告诉 k8s 该蛋糕配料及做法的**「戚风蛋糕制作表单」(CRD「用户自定义资源定义」)**,为了满足不同用户的需求,因此设置了「多种自定义选项,如甜度、形状等」(资源的参数),总结如下
Apiversion: 甜品组/测试版本
Kind:戚风蛋糕
Metadata:
name:此蛋糕的备注(戚风蛋糕-1,或生日快乐蛋糕等等,注意名字不能重复)
Spec:
形状:XXX # 用户可自定义
甜度:XXX # 用户可自定义
做法:XXX # 用户可自定义、可以视为 image 的启动命令
配料:XXX # 用户可自定义,可以视为 image
当用户按照「上述格式」构建「蛋糕制作表单」(yaml文件),提交给 k8s 时,k8s 会将其记录到他们的**「专用记录本上的某一页」( 该页可以称之为 API 对象 或 CR「用户自定义资源」)**
Apiversion: 甜品组/测试版本
Kind:戚风蛋糕
Metadata:
name:戚风蛋糕编号1
Spec:
形状:圆形
甜度:50%
做法:第一步XX,第二步XX 等等
配料:动物奶油、糖、炼乳
若用户未按照「上述格式」构建「某种蛋糕制作表单」(yaml文件),k8s 便不会理解,也不会进行记录
当记录到「专用记录本上的某一页」后,k8s 的**「主厨的助手在实时监控着」(Operator、Controller 的 Informer),之后告诉了「主厨」(Operator、Controller 的 Control Loop 控制循环)**,主厨发现「专用记录本上的这一页」后,便指导 k8s 提供的「基础厨师」(Pod),进行蛋糕的制作,其中还可以添加用户的「自定义做法,如裱花等」(就是 Pod 种镜像的启动命令之类的)
**删除:**在此过程中,客户可能不想要了,那么 k8s 会「撕毁」(delete)「专用记录本上对应的那一页」,k8s 的「主厨的助手在实时监控着」(Operator、Controller 的 Informer)发现了,通知「主厨」不用做了
更新:在此过程中,客户可能想修改蛋糕的「甜度」(API对象的某一属性),那么 k8s 会「修改」(delete)「专用记录本上对应的那一页」,k8s 的「主厨的助手在实时监控着」(Operator、Controller 的 Informer)发现了,通知「主厨」进行修改,最后形成一个「名称为戚风蛋糕编号1的戚风蛋糕」(资源对象)
Apiversion: 甜品组/测试版本
Kind:蛋糕
Metadata:
name:戚风蛋糕编号1
Spec:
形状:圆形
甜度:70% # 修改了此处
做法:第一步XX,第二步XX 等等
配料:动物奶油、糖、炼乳
新增:在此过程中,客户可能想再要个「戚风蛋糕」(新增一个 API 对象),那么 k8s 会在「专用记录本上新增一页」(Add),k8s 的「主厨的助手在实时监控着」(Operator、Controller 的 Informer)发现了,通知「主厨」再做一个,最后又形成一个「名称为戚风蛋糕编号2的戚风蛋糕」(资源对象)
Apiversion: 甜品组/测试版本
Kind:蛋糕
Metadata:
name:戚风蛋糕编号2
Spec:
形状:圆形
甜度:70% # 修改了此处
做法:第一步XX,第二步XX 等等
配料:动物奶油、糖、炼乳
总结
戚风蛋糕制作表单 —— CRD,Pod 的定义(API 的类型,资源的类型),也可以成为编程语言中的 类型(如 struct 的定义)
用户提交的制作表单 —— 用户提交的 yaml —— 在 k8s 形成 API 对象 (对 struct 的填充)
最后制作出的蛋糕 —— 资源对象 —— 可以理解为 实例 (最后形成的 struct 对象)
制作的过程 —— Controller、Operator
Pod 也是遵循上述流程,一般对于原生资源的控制 成为 Controller
Operator 有很多脚手架(kubuilder、Operator-sdk),就是对 Controller 进行封装,便于编写
(三)Kubernetes 源码剖析之学习Informer机制
Kubernetes Informer 与 Lister 详解
kubernetes shared Informer 源码解析
client-go系列之5—Informer
25 | 深入解析声明式API(二):编写自定义控制器
kubernetes 中 informer 的使用
如何高效掌控K8s资源变化?K8s Informer实现机制浅析
根据流程图来解释一下 Informer 中几个组件的作用:
只看上图左侧部分即可
Informer 主要有两个职责
- 同步资源状态到本地缓存,保证本地缓存的有效性 —— 对应图中 Reflector 包的 List & Watch 机制
- 监听事件,并调用 ResourceEventHandler 中对应的处理函数(AddFunc、UpdateFunc、DeleteFunc)进行处理(
- 如执行某些自定义逻辑逻辑,如与 Prometheus 对接;
- 或者放入到 workqueue 中,等待 Controller 处理)
注意:
- ResourceEventHandler 的三个回调函数 (AddFunc、UpdateFunc、DeleteFunc) 处理后,放入到 workqueue 中的内容只是资源的 namespace 和 name 的组合(也就是 Local Store 中的 key),之后 Controller 处理时,时间上是利用此 key 从 Local Store 中取出具体的资源对象
见图中右侧部分
此处以一个封装 Neutron API 的 CRD 资源 Network 为例
- Controller 从 Informer 获取的只是 API 对象,可以理解为一个表单 —— 就是「期望的状态」
- 实际的状态 —— 要从集群中或调用 api 进行获取
- 调谐 —— 之后进行操作,使「实际的状态」达到「期望的状态」
举个很简单的例子
- 首先我创建一个 deployment ,设置副本数为 2
- deployment Controller 的 Informer 监听到了 dployment 这个 API 对象,得知了「期望状态」要 2 个 pod
- deployment Controller 接下来调用 api 查看「当前的实际状态」,发现有 0 个 pod
- 因此 deployment Controller 调用 pod 的 client,发送创建请求给 apiserver,进行 pod 的创建,创建 2 个 pod,满足要求 —— 调谐
上面过程只是简化 —— 其中还涉及到 replicaset 这种资源及对应的 Controller
每种资源都会有一个对应的控制器
// NewController returns a new student controller
func NewController(
kubeclientset kubernetes.Interface,
studentclientset clientset.Interface,
studentInformer informers.StudentInformer) *Controller {
utilruntime.Must(studentscheme.AddToScheme(scheme.Scheme))
glog.V(4).Info("Creating event broadcaster")
eventBroadcaster := record.NewBroadcaster()
eventBroadcaster.StartLogging(glog.Infof)
eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeclientset.CoreV1().Events("")})
recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: controllerAgentName})
controller := &Controller{
kubeclientset: kubeclientset,
studentclientset: studentclientset,
studentsLister: studentInformer.Lister(),
studentsSynced: studentInformer.Informer().HasSynced,
workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Students"),
recorder: recorder,
}
glog.Info("Setting up event handlers")
// Set up an event handler for when Student resources change
// 正常来说,注册的 `AddEventHandler` 函数监听到资源的变化,会将其更新到本地的 cache,并传送到 workqueue 中
studentInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: controller.enqueueStudent, // 下面有这个函数解释,就是先将对象同步到缓存中,再放入 workqueue 队列
UpdateFunc: func(old, new interface{}) {
oldStudent := old.(*bolingcavalryv1.Student)
newStudent := new.(*bolingcavalryv1.Student)
if oldStudent.ResourceVersion == newStudent.ResourceVersion {
//版本一致,就表示没有实际更新的操作,立即返回
return
}
controller.enqueueStudent(new)
},
DeleteFunc: controller.enqueueStudentForDelete,
})
return controller
}
// 数据先放入缓存,再入队列
func (c *Controller) enqueueStudent(obj interface{}) {
var key string
var err error
// 将对象放入缓存
// 其实这一步骤可以省略 informer 会自动同步缓存 cache 的
if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil {
runtime.HandleError(err)
return
}
// 将key放入队列
c.workqueue.AddRateLimited(key)
}
// 删除操作
func (c *Controller) enqueueStudentForDelete(obj interface{}) {
var key string
var err error
// 从缓存中删除指定对象
key, err = cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
if err != nil {
runtime.HandleError(err)
return
}
//再将key放入队列
c.workqueue.AddRateLimited(key)
}
processNextWorkItem
是从 workqueue
中取数据 keysyncHandler
是利用 key
从cache
获取期望状态,之后再获取实际状态(此代码没有实际编写),之后进行调谐处理,满足期望的状态现在采用 Operator 脚手架进行编写时,只需要编写 Reconcile 函数
其实就是 syncHandler 函数(从缓存获取资源的「期望状态」,从环境中通过 client 获取资源的「实际状态」,之后利用 client 进行资源的调整「增删改」,完成「实际状态」到「期望状态」的转换)
脚手架相当于封装了
- Informer 的 初始化
- Informer 的 AddEventHandler 的回调函数 (AddFunc、UpdateFunc、DeleteFunc)
- workqueue 的 逐个取出 key
func (c *Controller) runWorker() {
for c.processNextWorkItem() {
}
}
// 取数据处理
func (c *Controller) processNextWorkItem() bool {
obj, shutdown := c.workqueue.Get()
if shutdown {
return false
}
// We wrap this block in a func so we can defer c.workqueue.Done.
err := func(obj interface{}) error {
defer c.workqueue.Done(obj)
var key string
var ok bool
if key, ok = obj.(string); !ok {
c.workqueue.Forget(obj)
runtime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", obj))
return nil
}
// 在syncHandler中处理业务
if err := c.syncHandler(key); err != nil {
return fmt.Errorf("error syncing '%s': %s", key, err.Error())
}
c.workqueue.Forget(obj)
glog.Infof("Successfully synced '%s'", key)
return nil
}(obj)
if err != nil {
runtime.HandleError(err)
return true
}
return true
}
// 处理
func (c *Controller) syncHandler(key string) error {
// Convert the namespace/name string into a distinct namespace and name
namespace, name, err := cache.SplitMetaNamespaceKey(key)
if err != nil {
runtime.HandleError(fmt.Errorf("invalid resource key: %s", key))
return nil
}
// 从缓存中取对象
student, err := c.studentsLister.Students(namespace).Get(name)
if err != nil {
// 如果Student对象被删除了,就会走到这里,所以应该在这里加入执行
if errors.IsNotFound(err) {
glog.Infof("Student对象被删除,请在这里执行实际的删除业务: %s/%s ...", namespace, name)
return nil
}
runtime.HandleError(fmt.Errorf("failed to list student by: %s/%s", namespace, name))
return err
}
glog.Infof("这里是student对象的期望状态: %#v ...", student)
glog.Infof("实际状态是从业务层面得到的,此处应该去的实际状态,与期望状态做对比,并根据差异做出响应(新增或者删除)")
c.recorder.Event(student, corev1.EventTypeNormal, SuccessSynced, MessageResourceSynced)
return nil
}