介绍
首先我们来了解下Cache的作用:收集pod信息,并提供节点级的聚合信息。其设计目标是为通用调度器提供高效查询。Cache的操作是以pod为中心的,能够基于pod的事件进行增量更新。但是由于事件是通过网络发送的,无法保证所有事件都能够传递,因为使用了Reflector进行list和watch操作,可能会由于延迟或relist出现事件丢失的情况。具体参考pkg/scheduler/internal/cache/interface.go中的说明。
在Cache中有有一个assume的概念,其含义为假定、假设,代表了调度器认为这个pod是已调度状态,这个是在scheduleCycle找到合适的节点之后设置的,但是此时还没有调用bind操作将其绑定到具体的节点上,在运行完绑定之后会将其从assumedPods中移除。由于bind的是异步执行的,所以调度器在启动这个协程之后会开始一个新的调度周期来调度下一个pod,在计算已用资源时,是会包含在assume集合中的pod的。
Cache中的数据时基于事件触发添加和更新的,因此为了保证在一个调度周期执行过程中,用到的数据不会发生变化,保证数据的一致性,scheduler在调度开始会使用当前Cache生成一个节点信息的快照,后续会用这个快照进行计算。
接口
下面首先看下Cache的接口定义,了解一下有哪些方法:
type Cache interface {
// 节点数量
// 测试用
NodeCount() int
// pod数量
// 测试用
PodCount() (int, error)
// 假设pod已调度,并聚合pod的信息到其对应的节点上,加入到assumedPods。
// 调度框架使用
AssumePod(pod *v1.Pod) error
// 完成绑定,设置assumedPod过期时间,后面在执行清理时会在assumedPods中删掉。
// 调度框架使用
FinishBinding(pod *v1.Pod) error
// 删除assumedPod。
// 调度框架使用
ForgetPod(pod *v1.Pod) error
// 添加pod,如果是assumedPod,则会执行确认逻辑,否则则是过期的pod,将其加入到缓存。
// 已调度pod informer使用
AddPod(pod *v1.Pod) error
// 更新pod信息
// 已调度pod informer使用
UpdatePod(oldPod, newPod *v1.Pod) error
// 从缓存中删除pod
// 已调度pod informer使用
RemovePod(pod *v1.Pod) error
// 获取pod
// 除了测试之外暂时未有用到之处
GetPod(pod *v1.Pod) (*v1.Pod, error)
// 判断是否是assumedPod且未过期
// 调度框架使用,未调度pod informer使用
IsAssumedPod(pod *v1.Pod) (bool, error)
// 添加node到缓存
// 节点informer使用
AddNode(node *v1.Node) *framework.NodeInfo
// 更新node
// 节点informer使用
UpdateNode(oldNode, newNode *v1.Node) *framework.NodeInfo
// 删除node
// 节点informer使用
RemoveNode(node *v1.Node) error
// 将当前缓存的快照保存到infoSnapshot中。其中节点信息包含了已调度的pod的聚合信息(包含assumedPod的)。
// 调度框架使用
UpdateSnapshot(nodeSnapshot *Snapshot) error
// 调试用
Dump() *Dump
}
下面通过一张图片来形象的描述Cache在scheduler中的位置:
实现详解
Cache接口的实现为cacheImpl
,下面是详细结构定义说明。
type cacheImpl struct {
// 停止channel
stop <-chan struct{}
// AssumedPods存活/超时时间
ttl time.Duration
// AssumedPods存活清理周期
period time.Duration
// cacheImpl的读写锁
mu sync.RWMutex
// assumedPod key集合
assumedPods sets.String
// pod key到pod状态的映射
podStates map[string]*podState
// node列表
nodes map[string]*nodeInfoListItem
// node列表头结点,指向最近更新的NodeInfo节点
headNode *nodeInfoListItem
// 节点树
nodeTree *nodeTree
// 镜像名到镜像状态的映射
imageStates map[string]*imageState
}
上述定义可以通过下面图片直观表示:
下面将会从4个方面来分析cache的实现,分别为快照部分,pod informer部分,node informer部分和调度部分。
快照
正如在最开始所提到的,在每个调度周期内,需要保证数据的一致行,在调度开始时,需要对cache生成一个快照,这就是UpdateSnapshot
()要做的事情。Snapshot定义为下列数据结构:
type Snapshot struct {
// NodeInfo映射
nodeInfoMap map[string]*framework.NodeInfo
// NodeInfo列表
nodeInfoList []*framework.NodeInfo
// pod设置了亲和性的所在节点的NodeInfo列表
havePodsWithAffinityNodeInfoList []*framework.NodeInfo
// pod设置了强制反亲和性的所在节点的NodeInfo列表
havePodsWithRequiredAntiAffinityNodeInfoList []*framework.NodeInfo
// 所有被使用的pvc集合
usedPVCSet sets.String
generation int64
}
整体逻辑是使用cache中的信息,添加或更新到当前snapshot上,此处会有一些设计,去实现高性能的增量的快照更新。
首先需要说下headNode这个双向链表,这个链表是有顺序的,头结点是最后更新的,尾节点则是最老的。
另外一个就是generation概念,在schedule内部有一个全局的单调递增的代系计数器:generation,这个计数器会赋值给NodeInfo.Generation和Snapshot.generation。
在生成snapshot的时候,遍历链表,比较当前snapshot的代系和每个NodeInfo的代系进行比较,如果NodeInfo.Generation <= Snapshot.generation则停止遍历,因为后面的NodeInfo没有更新,当前的snapshot上对应的信息已经是最新的了,就不需要在执行了。
下面是会引起NodeInfo.Generation更新的操作,并且同时会执行moveNodeInfoToHead()方法,将其移动到链条的开头:
- cache.AddNode()
- cache.UpdateNode()
- cache.RemoveNode()
- cache.AddPod()
- cache.UpdatePod()
- cache.RemovePod()
至于Snapshot中的具体字段的数据结构和使用,暂就不做解析了,这个主要是调度框架和插件使用。
调度操作
在调度pod时,会用到cache的四个方法:IsAssumedPod(),AssumePod(),ForgetPod(),FinishBinding()。
IsAssumedPod()在调度开始的时候会调用来检查pod是否已经assume过了,因为在收到update事件时pod会再次被添加到SchedulingQueue中。比如在binding期间以及在binding之后但是客户端还没收到apiserver的已调度pod的事件。
AssumePod()当然就是assume pod了,AssumePod会将pod添加到对应节点的NodeInfo 、cache.podStates和cache.assumedPods中,这就相当于这个pod把资源占上了。
ForgetPod()会将pod从对应节点的NodeInfo、cache.podStates和cache.assumedPods中删除。ForgetPod()用来在调度过程不成功时进行回退操作,撤销之前AssumePod()占用的资源。
FinishBinding()则是在binding成功之后,从assumedPods移除pod信息。当前代码参考的版本是1.27,pod的移除目前还是通过设置podState.deadline过期时间实现,在cache中有一个协程专门来清理过期的pod。但是当前版本的代码设置的过期时间为0(durationToExpireAssumedPod变量),这个携程实际上不会真正的起作用。durationToExpireAssumedPod变量的值在代码中有过多次修改,但是最终改成了0,并且后面版本可能会移除这个携程。cache.assumedPods中的pod会在已调度pod informer收到Add事件时由cache.AddPod()中进行删除(pkg/scheduler/eventhandlers.go)。关于这个问题的issue可以参考:
- https://github.com/kubernetes/kubernetes/issues/106361
- https://github.com/kubernetes/kubernetes/pull/110925#issuecomment-1174053205
pod informer
pod informer在收到pod状态变化时对调用AddPod(),UpdatePod(),RemovePod()这几个操作,对应在pod添加,更新和删除事件。kube-scheduler中的pod informer有两个,分别是处理未调度的pod和已调度的pod,未调度的pod会调用SchedulingQueue的相关方法,而cache这里的操作是已调度的pod的事件执行的。这几个方法和上面的类似,都会对应节点的NodeInfo、cache.podStates和cache.assumedPods中相关的数据进行添加、更新和删除操作。
node informer
node informer在接收到node状态变化时会调用AddNode(),UpdateNode()和RemoveNode()几个方法。这几个方法分别会对cache.nodes映射、cache.imagesStates、cache.nodeTree和对应的NodeInfo执行添加、更新和删除方法。