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方法依次判断是否有冲突,只要有一个存在冲突,即过滤掉该节点。
  • CheckConflict检测冲突的方法,主要是利用map的key是否存在。其中还考虑了监听ip:如果入参监听ip为0.0.0.0,则需要遍历HostPortInfo,检测其中所有的(协议,端口)二元组;否则,若入参监听ip不为0.0.0.0,则只需要判断入参提供的ip和0.0.0.0ip下的(协议,端口)二元组是否已被使用即可。

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的细节关系比较密切:
    1. 根据node的名称获取CSINode对象,该对象的细节参考CSINode Object - Kubernetes CSI Developer Documentation (kubernetes-csi.github.io)
    2. 根据待调度pod的volume、pvc和CSINode信息,封装一个map,其key是driverName/volumeHandle拼成的字符串,该字符串用于唯一标识一个volume;value是该driverName的volumeLimitKey,该值是后面的计数口径。
    3. 获取node的volumeLimit。
    4. 以node上已有的pod为统计目标,封装另一个和2类似的map。
    5. 将2和4统计的两个map分别转成计数:以volumeLimitKey为分组单位。
    6. 对每种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方法中。
    1. 初始化四个值为true的bool变量,默认node不会被过滤掉。同时结合defer,最后会检查这四个bool变量,如果被逻辑置为了false,则会把各自的bool值转化成失败原因,放入reasons slice返回上层。
    2. prefilter扩展点阶段,该插件已经把pvc分成了两类,bound和toBind。此时会分别处理这两类pvc。
    3. 对于bound类pvc,checkBoundClaims方法中会依次遍历这些pvc,拿到其pv,然后检查node是否满足node affinity。
    4. 对于toBind类pvc,会再将其进行细分:claimsToProvision、claimsToFindMatching以及已经被明确分给其他节点的pvc。具体而言,首先遍历所有toBind类的pvc,检查pvc的annotation字段里volume.kubernetes.io/selected-node是否为空。如果为空,说明这个pvc还没找到合适的pv去绑定,则将其分到claimsToFindMatching。如果已经存在该字段,需要确认该字段的值是不是当前的node,如果不是,直接filter掉;如果是,则将其分到claimsToProvision。
    5. 对于claimsToFindMatching类的pvc,会根据storageclass找到所有pv,然后按照规则从这些pv里找出最合适的一个(详细规则参考pkg/controller/volume/persistentvolume/util/util.go文件中的FindMatchingVolume函数)。如果都能找到合适的pv,则会将本组的pvc加入到claimsToProvision组,一起进行最后一步。
    6. 最后,对于所有claimsToProvision类的pvc,会遍历这些pvc,检查其storageClass和相应的provisioner,并检查节点是否满足storageClass的AllowedTopologies字段。如果volumeBinder结构体的capacityCheckEnabled字段是true,即开启了容量检查,还会对provisioner是否有足够的容量满足pvc的容量需求进行检查。

VolumeZone

pkg/scheduler/framework/plugins/volumezone/volume_zone.go

  • 流程简介:
    1. 检查node是否包含指定的几个与可用区相关的label(volumeZoneLabels),如果没有,直接跳过后续逻辑。
    2. 遍历pod所有pvc类型的volume,状态不正常的pvc会被fail掉。
    3. 进一步,获取pvc关联的pv。如果pvName因为需要延迟绑定而为空字符串,则该插件会跳过对其的过滤,除此之外的空pvName,会进一步获取关联的storageClass,并对各种异常情况进行判断。
    4. 各种异常情况排除完毕后,开始遍历pv的label,从中提取出和1中同样的几个label(volumeZoneLabels)。最后,对比pv和node在volumeZoneLabels这些label上的值,只有node包含和pv匹配的value时,才不会被filter掉。

PodTopologySpread

pkg/scheduler/framework/plugins/podtopologyspread/filtering.go

  • 核心逻辑:从prefilter state中拿到所有的打散需求Constraints,遍历,对于每一条打散需求Constraints,进行下面步骤:
    1. 过滤掉拓扑域外的node。
    2. *从prefilter存储的state中拿到TpKeyToCriticalPaths map,用Constraint的拓扑域做key,如果key不存在,则过滤掉
    3. 计算节点是否满足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的反亲和性需求。
文章目录