之前我们了解了kube-scheduler,kube-scheduler其实经历了两个阶段,第一个阶段是predict阶段,把不符合要求的节点过滤掉。第二个阶段是priority,按照你的调度需求,把满足需求的备选节点打分,分数高的排在前面,最后从备选节点队列里面,第一个节点,也就是分值最高的节点,作为真正调度的节点,最后通过bind的方法,把节点和pod产生绑定的关系。
kube-scheduler可以理解为和控制器没有任何的差异,它一边要watch pod和节点的这种变化状态,另外一方面根据用户给定的pod request这些信息来做一个决策,最后一个结果回写到apiserver里面,所以它就是一个控制闭环,它也是一个control loop,所以kube-scheduler其实是controller manager里面一个控制器的特例,它是有专门职责的,就专门做资源的分配。
apiserver本身是没有逻辑的rest server,整个集群动起来,谁是驱动力呢?那么就是controller manager,它是整个集群的大脑,你把期望的状态给我,由我集群内部一直去解读你的期望,并且去做一些配置,让真实状态符合你的期望。这些都由controller manager去做。
控制器框架/工作流程
之前讲apiserver的时候,讲过一个code generator的项目,针对任何的api的对象,可以通过code generator这个项目去生成代码,会生成deepcopy,conversion这些通用的方法,这些通用方法就是和api相关的,会生成clientset,clientset用来定义可以通过什么样的方式来访问这个对象,比如create,delete,get,list,watch,这些都是client set去支持的,这也是通过代码生成器生成的。
同时为了支持控制器的逻辑,上面图中的框架,code generator会同时生成两个框架。一个叫informer一个叫lister,这两个统称为controller interface,分别干嘛用的呢?
informer(有事件推送给我)
informer:当我们去APIServer去获取任何对象的时候,可以两种方式去获取,一种是get,一种是watch,get -w,可以看到客户端去访问apiserver对象的时候可以通过长连接的,get pod然后加上watch的参数,其实就告诉APIServer把当前集群里面的满足查询条件的所有对象先返回给我,并且加上-watch,就是连接不断,保持长连接。然后你有更加新的变化,你把它推给我,这是watch的一个机制。
针对watch list这两种不同的操作,其实控制器的interface其实就分为了lister和informer两个interface,一个lister就是将当前的状态发给我,informer更多的是你有事件推给我。
所以informer是一个消息通知机制的接口,针对informer的话,通常就会有三种不同的event,一种是add event,一种是update event,一种是delete event。
add event是这个对象第一次出现的时候,它就是一个add event,update就是之前对象存在,但是你现在要变更属性,那就是update,然后delete是这个对象消失,从etcd当中删除。
handler
针对我们的控制器,如果配合这两个interface呢,就是我们要去注册这些事件的handler,处理handler,通常怎么做呢,就去写一些handler,Addfunc handler,Deletefunc handler,Updatefunc handler。这些handler通常做什么呢?
大部分的控制器,不是绝对,因为发过来的event是一个完整的对象,那么大多数控制器会去拿这个对象的keyfunc,所谓的keyfunc就是这个对象的namespace加name,那么就使得这个对象全局唯一了,然后拿到这个对象,它把对象的的key,namespace+name,放到一个队列里面。
这个队列叫做RateLimiting Interface,在其之上是线程往队列里面塞数据,下面就一堆Worker线程。
这个worker线程通常是可以配置的。比如k8s的大部分的worker可能是5个并发线程,5个worker一起来干活。它从队列里面取数据,所以这就是标准的生产者和消费者模型,整个k8s都在使用这种模式。
这个worker获取到这个对象之后,从队列里面取出来的数值只是一个key,只是一个字串,它没有这个对象的完整信息,他要去获取完整的信息,往往可以通过lister接口去获取,lister这边会在本地缓存对象,所以worker去取对象完整信息的时候,它是从本地的缓存当中取获取的,而不会将请求发送给apiserver。
所以apiserver过来的数据就是基于长连接推送数据,所以下面worker里面,真正干活的逻辑不会去apiserver里面再重新获取数据。
最后提一下,为什么将key放在队列里面,而不是将完整对象放在队列里面?假设一个对象频繁的变更,那么对列需要的内存空间会比较大,其次每次对象都不一样,针对刚才所说的这种事件频繁的变更,那么下面的控制器就会去多次去处理多次变更的对象。它是将key放在这里面了,不管对象发生了多少次变更,它们的key是一样的,即使你发生了10次变更,它推送到队列里面其实也是一个事件。这是这个队列本身它提供了一个唯一性确认,如果一个key存在了,你再筛选的话也不会筛选多份,所以不管有多少个event,其实这里面是一个key。
那么一个key再由worker获取的时候,worker本身要去lister里面获取它最真实的状态。比如1秒钟变化10次,它拿到的是最后一个状态,所以中间反复变化已经被忽略掉了,因为k8s最终讲究一致性,worker去处理的时候,真正去做配置管理的时候,其实是需要一些时间的,如果每个事件变更我都要去处理一次,那么worker就累死了。
通过这种机制,指引队列一个key,使得一个对象频繁变更,我们其实最终拿到的是它的最终状态。你不需要告诉我中间过程有多少次需求的变化,我只要在处理你的时候,我看看你最新的希望是什么,满足你的最新期望就ok了,之前的期望就不作数了,上面就是整个控制器的框架
Informer的内部机制
informer本身会和apiserver产生一个长连接,会做list和watch的操作,apiserver这端都是一个一个的rest调用,它返回给客户端都是序列化好的字符串,这些字符串其实和go语言的这些k8s对象还没有产生一一绑定的关系,所以在informer这一端,就有一个reflector,这个reflect就是通过反射机制去解析这些json tag,reflect其实是知道json文件对应的其对象的,它里面每个属性的对应关系它都是知道的,所以通过reflector就将序列化的json字符串变成了一个一个的go语言的对象。
然后在informer这端,也就是客户端这边,维护了一个data fifo的队列,先进先出队列,前面反序列化之后就往队列里面添加。
下面部分就是从队列当中获取
取出来的对象会往两个地方放,第一会将完整的对象放在indexer里面,这边会去存储对象和键的这样一个thread safe store,它是线程安全的safe store。
同时对象在添加的时候,event就会去触发这个事件,然后就需要去注册event的handler,就是某个对象添加了,某个对象删除了,某个对象修改了。
然后发生了这个event,handler里面逻辑就是,如果发生了event,我就要将key放到workquenue里面。
还有就是一些后台的线程,worker的process,一直去取键值,最终它会从indexer里面直接把,也就是从thread safe store里面把真正的对象取出来,然后去完成配置管理。
上面就是shareinformer内部实现的一个机制。
所有的k8s控制器,包括调度器,其实大部分做这些配置管理的组件,都是以这个框架为基础搭建起来的。