kube-scheduler调度器调度框架源码学习篇2
Filter扩展点
概述
filter扩展点会将某个pod无法被调度到的node过滤掉,最终仅保留可供该pod调度的node。
该扩展点的套路为
- 关键输入:待调度的某个pod对象,可供其选择的某个node及围绕该node相关的细节信息
- 输出:如果该node可以被该pod调度,则返回nil;否则中途通过
framework.NewStatus
返回一个具体的不可调度的状态,将该节点剔除。 - 核心操作:不像prefilter扩展点会在return前加工一些数据并存储,filter扩展点正如其名称一样,是一个比较纯粹的过滤器:或者返回nil,node被保留;或者返回一个错误的status,node被剔除。
NodeUnschedulable
pkg/scheduler/framework/plugins/nodeunschedulable/node_unschedulable.go
- 核心逻辑:
- 检查传入的每个node,如果该node的状态未知,则直接返回不可调度的状态
- 另外对于某个明确的状态为Unschedulable的node,除了能够容忍该污点的pod,也会返回不可调度状态
NodeName
pkg/scheduler/framework/plugins/nodename/node_name.go
- 核心逻辑:如果podSpec里指定了nodeName,则对于所有nodeName不匹配的node,都会被该插件过滤掉
TaintToleration
pkg/scheduler/framework/plugins/tainttoleration/taint_toleration.go
- 核心逻辑:对该插件在FIlter扩展点的每次调用,会对比pod的污点容忍和node的污点,如果出现冲突,即node上有该pod无法容忍的污点,则会返回不可调度状态,过滤掉该节点。
- 只有effect为NoSchedule和NoExecute类型的污点可能会在该阶段被filter掉,拥有effect为PreferNoSchedule的污点的node在本阶段不受影响
NodeAffinity
pkg/scheduler/framework/plugins/nodeaffinity/node_affinity.go
-
核心逻辑:从prefilter扩展点中取出该插件保存的state,其中包含了该pod配置的的NodeSelector和Affinity,并和传入的node进行匹配测试,必须同时满足二者匹配的node才能被保留,匹配失败则该node被剔除。
-
NodeSelector(这里指pod的)匹配涉及到的核心代码(不知道为啥代码里变量叫labelSelector)
if s.labelSelector != nil { if !s.labelSelector.Matches(labels.Set(node.Labels)) { return false, nil } }
s.labelSelector由GetRequiredNodeAffinity函数生成,其定义为labels.Selector的接口,具体实现是一个叫做internalSelector的结构体。进一步查看其Matches方法,实现的思路为遍历internalSelector slice里的每个Requirement,根据其key、operator和value,判断node的label是否和Requirement相符,只有全部的Requirement都相符,该node才不会被filter掉。
-
Affinity(同样是指pod的)匹配和上面NodeSelector匹配整体流程类似,和NodeSelector不同的是当有多条affinity时只要匹配到一条符合的,node就不会被过滤
-
该插件支持在配置中额外添加nodeSelector
NodePorts
pkg/scheduler/framework/plugins/nodeports/node_ports.go
- 核心逻辑:
- 传入的framework.NodeInfo实例有一个HostPortInfo结构体,该结构体存储了node上所有已经被占用的端口(实际存储的是监听ip、协议、端口组成的三元组map)。同时该结构体有一个实例方法
CheckConflict(ip, protocol string, port int32) bool {}
,即通过入参的三元组,判断是否构成冲突。 - 因此该插件的该扩展点会从取出preFilter扩展点存储的pod对节点端口的需求,并遍历,用HostPortInfo的CheckConflict方法依次判断是否有冲突,只要有一个存在冲突,即过滤掉该节点。
- 传入的framework.NodeInfo实例有一个HostPortInfo结构体,该结构体存储了node上所有已经被占用的端口(实际存储的是监听ip、协议、端口组成的三元组map)。同时该结构体有一个实例方法
- CheckConflict检测冲突的方法,主要是利用map的key是否存在。其中还考虑了监听ip:如果入参监听ip为
0.0.0.0
,则需要遍历HostPortInfo,检测其中所有的(协议,端口)二元组;否则,若入参监听ip不为0.0.0.0
,则只需要判断入参提供的ip和0.0.0.0
ip下的(协议,端口)二元组是否已被使用即可。
NodeResourcesFit
pkg/scheduler/framework/plugins/noderesources/fit.go
- 核心逻辑:检查传入的node是否有足够的各项硬件资源以满足pod的资源需求,这里的资源除了包含cpu、内存、gpu等常见硬件资源,还包含为了可扩展性专门设置的叫做ScalarResources的一大类资源,其本质是一个map,可以支持各类其他非常用硬件资源。pod的资源最小需求量由该插件在preFilter扩展点计算完毕,Filter扩展点只需要取出来即可。
fitsRequest
函数会对比pod需求与node供给,最终返回一个slice,其中包含了所有不足的资源。如果node没有不足的资源,则该node不会被filter掉。 - 该插件可额外配置ignoredResources和ignoredResourceGroups,使得在对比资源需求和供给时忽略某种资源,但是仅支持忽略ScalarResources里的资源类型
- fitsRequest会对以下几类资源分别做对比:cpu、内存、节点内置存储、pod数量和ScalarResources这个map里的所有资源(ignoredResources除外)
VolumeRestrictions
pkg/scheduler/framework/plugins/volumerestrictions/volume_restrictions.go
- 核心逻辑:遍历待调度pod podSpec里的Volumes,如果有以下四种卷类型:GCE PD、AWS EBS、Ceph RBD、ISCSI,则需要检测节点上已有pod的同种volume是否和待调度pod的volume需求有冲突。检测冲突的逻辑因卷类型而异。
NodeVolumeLimits
pkg/scheduler/framework/plugins/nodevolumelimits/csi.go
- 核心逻辑:与CSI配合,统计节点上某种类型volume数量,以及pod的volume需求数量,计算是否超出node上配置的数量上限
- 大致流程,此处与CSI的细节关系比较密切:
- 根据node的名称获取CSINode对象,该对象的细节参考CSINode Object - Kubernetes CSI Developer Documentation (kubernetes-csi.github.io)。
- 根据待调度pod的volume、pvc和CSINode信息,封装一个map,其key是driverName/volumeHandle拼成的字符串,该字符串用于唯一标识一个volume;value是该driverName的volumeLimitKey,该值是后面的计数口径。
- 获取node的volumeLimit。
- 以node上已有的pod为统计目标,封装另一个和2类似的map。
- 将2和4统计的两个map分别转成计数:以volumeLimitKey为分组单位。
- 对每种volumeLimitKey,计算该pod新增的volume是否会导致超过节点限制,有超过的,则直接filter掉该节点。
VolumeBinding
pkg/scheduler/framework/plugins/volumebinding/volume_binding.go
- 核心逻辑:获取pod的pvc,如果该pvc已经和pv绑定,则检查该pv的node亲和性是否与传入的node匹配;如果该pvc尚未与pv绑定,则尝试寻找满足pvc条件的pv,再继续检测pv的node亲和性;如果存储容量追踪被启用,还会对容量进行检测。
- 主要逻辑的入口位于
pkg/controller/volume/scheduling/scheduler_binder.go
中volumeBinder结构体的FindPodVolumes方法中。- 初始化四个值为true的bool变量,默认node不会被过滤掉。同时结合defer,最后会检查这四个bool变量,如果被逻辑置为了false,则会把各自的bool值转化成失败原因,放入reasons slice返回上层。
- prefilter扩展点阶段,该插件已经把pvc分成了两类,bound和toBind。此时会分别处理这两类pvc。
- 对于bound类pvc,
checkBoundClaims
方法中会依次遍历这些pvc,拿到其pv,然后检查node是否满足node affinity。 - 对于toBind类pvc,会再将其进行细分:claimsToProvision、claimsToFindMatching以及已经被明确分给其他节点的pvc。具体而言,首先遍历所有toBind类的pvc,检查pvc的annotation字段里
volume.kubernetes.io/selected-node
是否为空。如果为空,说明这个pvc还没找到合适的pv去绑定,则将其分到claimsToFindMatching。如果已经存在该字段,需要确认该字段的值是不是当前的node,如果不是,直接filter掉;如果是,则将其分到claimsToProvision。 - 对于claimsToFindMatching类的pvc,会根据storageclass找到所有pv,然后按照规则从这些pv里找出最合适的一个(详细规则参考
pkg/controller/volume/persistentvolume/util/util.go
文件中的FindMatchingVolume
函数)。如果都能找到合适的pv,则会将本组的pvc加入到claimsToProvision组,一起进行最后一步。 - 最后,对于所有claimsToProvision类的pvc,会遍历这些pvc,检查其storageClass和相应的provisioner,并检查节点是否满足storageClass的AllowedTopologies字段。如果volumeBinder结构体的capacityCheckEnabled字段是true,即开启了容量检查,还会对provisioner是否有足够的容量满足pvc的容量需求进行检查。
VolumeZone
pkg/scheduler/framework/plugins/volumezone/volume_zone.go
- 流程简介:
- 检查node是否包含指定的几个与可用区相关的label(
volumeZoneLabels
),如果没有,直接跳过后续逻辑。 - 遍历pod所有pvc类型的volume,状态不正常的pvc会被fail掉。
- 进一步,获取pvc关联的pv。如果pvName因为需要延迟绑定而为空字符串,则该插件会跳过对其的过滤,除此之外的空pvName,会进一步获取关联的storageClass,并对各种异常情况进行判断。
- 各种异常情况排除完毕后,开始遍历pv的label,从中提取出和1中同样的几个label(
volumeZoneLabels
)。最后,对比pv和node在volumeZoneLabels
这些label上的值,只有node包含和pv匹配的value时,才不会被filter掉。
- 检查node是否包含指定的几个与可用区相关的label(
PodTopologySpread
pkg/scheduler/framework/plugins/podtopologyspread/filtering.go
- 核心逻辑:从prefilter state中拿到所有的打散需求Constraints,遍历,对于每一条打散需求Constraints,进行下面步骤:
- 过滤掉拓扑域外的node。
- *从prefilter存储的state中拿到TpKeyToCriticalPaths map,用Constraint的拓扑域做key,如果key不存在,则过滤掉。
- 计算节点是否满足maxSkew要求:'existing matching num' + 'if self-match (1 or 0)' - 'global min matching num' <= 'maxSkew'。
InterPodAffinity
pkg/scheduler/framework/plugins/interpodaffinity/filtering.go
- 核心逻辑:在prefilter扩展点整理统计了三个和pod亲和性有关的数据结构,在filter扩展点阶段,将分别使用这三个数据结构,来对node进行过滤,三种亲和性要求的过滤实现方式大体是相似的:首先过滤掉拓扑域不符的节点,在拓扑域内的节点中,如果对于待调度pod的每一条RequiredAffinityTerms/RequiredAntiAffinityTerms或每一条existingAntiAffinityCounts,都能在state存储的map中找到满足条件的value(此处逻辑还需要梳理),则节点不会被过滤掉。
- 是否满足pod间亲和性:对于拓扑域内彼此亲和pod群的第一个pod,由于尚没有pod与之亲和,所以会允许其调度,只要拓扑域满足即可。
- 是否满足pod间反亲和性:指待调度pod的反亲和性需求。
- 是否满足节点上已有pod的反亲和性:如果该待调度的pod调度到某节点,是否会违背该节点已有pod的反亲和性需求。
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。