随着 k8s 的倒退,调度器的实现也在变动,本文将从 1.23 版本源码角度解析 k8s 调度器的外围实现。
调度器总览
整个调度过程由 kubernetes/pkg/scheduler/scheduler.go#L421 的 func (sched *Scheduler) scheduleOne(ctx context.Context)
实现。这个函数有两百多行,能够分为四个局部:
获取待调度 Pod 对象:通过 sched.NextPod()从优先级队列中获取一个优先级最高的待调度 Pod 资源对象,该过程是阻塞模式的,当优先级队列中不存在任何 Pod 资源对象时,sched.config.NextPod 函数处于期待状态。
调度阶段:通过 sched.Algorithm.Schedule(schedulingCycleCtx, sched.Extenders, fwk, state, pod)
调度函数执行预选调度算法和优选调度算法,为 Pod 资源对象抉择一个适合的节点。
抢占阶段:当高优先级的 Pod 资源对象没有找到适合的节点时,调度器会通过 sched.preempt
函数尝试抢占低优先级的 Pod 资源对象的节点。
绑定阶段:当调度器为 Pod 资源对象抉择了一个适合的节点时,通过 sched.bind
函数将适合的节点与 Pod 资源对象绑定在一起。
调度过程
进入过滤阶段前的节点数量计算
在初始化调度器的时候,kube-scheduler 会对节点数量进行优化。如下图:
门路:
其中红框是调度器的一个性能优化,通过 PercentageOfNodesToScore
机制,在集群节点数量很多的时候,只加载指定百分比的节点,这样在大集群中,能够显著优化调度性能;这个百分比数值能够调整,默认为 50,即加载一半的节点;具体的节点数量由一个不简单的计算过程得出:
其中,minFeasibleNodesToFind
为预设的参加预选的最小可用节点数,当初的值为 100。见上图 172 行,当集群节点数量小于该值或 percentageOfNodesToScore
百分比大于等于 100 时候,间接返回所有节点。当大于 100 个节点的时候,应用了一个公式,adaptivePercentage = basePercentageOfNodesToScore - numAllNodes/125
,翻译一下的话就是自适应百分比数 = 默认百分比数 - 所有节点数 /125,见 178 行,默认百分比为 50,假如有 1000 个节点,那么自适应百分比数 =50-1000/125=42;180 和 181 行则是指定了一个百分比上限 minFeasibleNodesPercentageToFind,当初的值为 5。即后面算进去的百分比如果小于 5,则取上限 5。依照这个机制,那么参加过滤的节点数 =100042%=420 个。当这个节点数小于 minFeasibleNodesToFind 的时候,则返回 minFeasibleNodesToFind。因而,1000 个节点的集群最终参加预选的是 420 个;同理能够计算,5000 个节点的集群,参加预选的是 5000(50-5000/125)%=500 个。能够看到,只管节点数量从 1000 减少到了 5000,但参加预选的只从 420 减少到了 500。
过滤阶段
通过 PercentageOfNodesToScore 失去参加预选调度的节点数量之后,scheduler 会通过 podInfo := sched.NextPod()
从调度队列中获取 pod 信息;而后进入 Schedule,这是一个定义了 schedule 的接口,k8s 实现了一个 genericScheduler,如果要自定义本人的调度器,实现该接口,而后在 deployment 中指定用该调度器就行。
type ScheduleAlgorithm interface {Schedule(context.Context, []framework.Extender, framework.Framework, *framework.CycleState, *v1.Pod) (scheduleResult ScheduleResult, err error)
}
进入 genericScheduler 后,首先就进入预选阶段 findNodesThatFitPod,或者称为过滤阶段,此阶段会取得过滤之后可用的所有节点,供下一阶段应用,即 feasibleNodes。
findNodesThatFitPod 供蕴含以下四局部:
fwk.RunPreFilterPlugins:运行过滤前的解决插件。RunPreFilterPlugins 负责运行一组框架已配置的 PreFilter 插件。如果任何插件返回除 Success 之外的任何内容,它将设置返回的 *Status::code 为 non-success。则调度周期停止。
g.evaluateNominatedNode:将某个节点独自执行过滤。如果 Pod 指定了某个 Node 上运行,这个节点很可能是惟一适宜 Pod 的候选节点,那么会在过滤所有节点之前,查看该 Node,具体条件为:len(pod.Status.NominatedNodeName) > 0 && feature.DefaultFeatureGate.Enabled(features.PreferNominatedNode)
,这个机制也叫“提名节点”。
g.findNodesThatPassFilters:将所有节点进行预选过滤。这个函数会创立一个可用 node 的节点feasibleNodes := make([]*v1.Node, numNodesToFind)
,而后通过 checkNode 遍历 node,查看 node 是否合乎运行 Pod 的条件,即运行所有的预选调度算法(如下所示),如果合乎则退出 feasibelNodes 列表。
for _, pl := range f.filterPlugins {pluginStatus := f.runFilterPlugin(ctx, pl, state, pod, nodeInfo)
if !pluginStatus.IsSuccess() {if !pluginStatus.IsUnschedulable() {
// Filter plugins are not supposed to return any status other than
// Success or Unschedulable.
errStatus := framework.AsStatus(fmt.Errorf("running %q filter plugin: %w", pl.Name(), pluginStatus.AsError())).WithFailedPlugin(pl.Name())
return map[string]*framework.Status{pl.Name(): errStatus}
}
pluginStatus.SetFailedPlugin(pl.Name())
statuses[pl.Name()] = pluginStatus
if !f.runAllFilters {
// Exit early if we don't need to run all filters.
return statuses
}
}
}
findNodesThatPassExtenders:将上一步通过预选的 Node 再通过扩大过滤器过滤一遍。这个其实是 k8s 留给用户的自定义过滤器。它遍历所有的 extender 来确定是否关怀对应的资源,如果关怀就会调用 Filter 接口来进行近程调用 feasibleList, failedMap, failedAndUnresolvableMap, err := extender.Filter(pod, feasibleNodes),并将筛选后果传递给下一个 extender,逐渐放大筛选汇合。近程调用是一个 http 的实现,如下图:
至此,预选阶段完结。整个预选过程逻辑上很天然,预处理 -> 过滤 -> 用户自定义过滤 -> 完结。
在预处理阶段 (PreFilterPlugin),官网次要定义了:
InterPodAffinity: 实现 Pod 之间的亲和性和反亲和性,InterPodAffinity 实现了 PreFilterExtensions,因为抢占调度的 Pod 可能与以后的 Pod 具备亲和性或者反亲和性;
NodePorts: 查看 Pod 申请的端口在 Node 是否可用,NodePorts 未实现 PreFilterExtensions;
NodeResourcesFit: 查看 Node 是否领有 Pod 申请的所有资源,NodeResourcesFit 未实现 PreFilterEtensions;
PodTopologySpread: 实现 Pod 拓扑散布;
ServiceAffinity: 查看属于某个服务(Service) 的 Pod 与配置的标签所定义的 Node 汇合是否适配,这个插件还反对将属于某个服务的 Pod 扩散到各个 Node,ServiceAffinity 实现了 PreFilterExtensions 接口;
VolumeBinding: 查看 Node 是否有申请的卷,是否能够绑定申请的卷,VolumeBinding 未实现 PreFilterExtensions 接口;
过滤插件在晚期版本叫做预选算法,但在较新的版本曾经删除了 /pkg/scheduler/algorithem 这个包,因为用过滤更贴切一点。在这个目录下能够找到所有的插件实现:
基本上通过名字就晓得是做什么的, 不赘述,如
InterPodAffinity: 实现 Pod 之间的亲和性和反亲和性;
NodeAffinity: 实现了 Node 选择器和节点亲和性
NodeLabel: 依据配置的标签过滤 Node;
NodeName: 查看 Pod 指定的 Node 名称与以后 Node 是否匹配;
NodePorts: 查看 Pod 申请的端口在 Node 是否可用;
…
优选阶段
预选的后果是 true 或 false,意味着一个节点要么满足 Pod 的运行要求,要么不满足;失去泛滥满足的节点后,最终决定 Pod 调度到哪个节点。
在调度器中,优选的过程由 prioritizeNodes 负责,它会返回一个带分数的节点列表,定义如下:
// NodeScore is a struct with node name and score.
type NodeScore struct {
Name string
Score int64
}
最终由 selectHost 返回一个 node 名字,作为最终的 ScheduleResult. 上面进行具体分析。
prioritizeNodes 分为三局部,运行打分前解决插件,运行所有的打分插件,将所有分数相加:
优选阶段最次要的就是运行各种打分插件,kube-scheduler 会调用 ScorePlugin 对通过 FilterPlugin 的 Node 评分,所有 ScorePlugin 的评分都有一个明确的整数范畴,比方[0, 100],这个过程称之为标准化评分。在标准化评分之后,kube-scheduler 将依据配置的插件权重合并所有插件的 Node 评分得出 Node 的最终评分。依据 Node 的最终评分对 Node 进行排序,得分最高者就是最合适 Pod 的 Node。
type ScorePlugin interface {
Plugin
// 计算节点的评分,此时须要留神的是参数 Node 名字,而不是 Node 对象。// 如果实现了 PreScorePlugin 就从 CycleState 获取状态,如果没实现,调度框架在创立插件的时候传入了句柄,能够获取指定的 Node。// 返回值的评分是一个 64 位整数,是一个由插件自定义实现取值范畴的分数。Score(ctx context.Context, state *CycleState, p *v1.Pod, nodeName string) (int64, *Status)
// 返回 ScoreExtensions 接口,此类设计与 PreFilterPlugin 类似
ScoreExtensions() ScoreExtensions}
// ScorePlugin 的扩大接口
type ScoreExtensions interface {// ScorePlugin().Score()返回的分数没有任何束缚,然而多个 ScorePlugin 之间须要标准化分数范畴,否则无奈合并分数。// 比方 ScorePluginA 的分数范畴是[0, 10],ScorePluginB 的分数范畴是[0, 100],那么 ScorePluginA 的分数再高对于 ScorePluginB 的影响也是十分无限的。NormalizeScore(ctx context.Context, state *CycleState, p *v1.Pod, scores NodeScoreList) *Status
}
实现该接口的插件有:
ImageLocality: 抉择曾经存在 Pod 运行所需容器镜像的 Node,这样能够省去下载镜像的过程,对于镜像十分大的容器是一个十分有价值的个性,因为启动工夫能够节约几秒甚至是几十秒;
InterPodAffinity: 实现 Pod 之间的亲和性和反亲和性;
NodeAffinity: 实现了 Node 选择器和节点亲和性
NodeLabel: 依据配置的标签过滤 Node;
NodePreferAvoidPods: 基于 Node 的注解 scheduler.alpha.kubernetes.io/preferAvoidPods 打分;
NodeResourcesBalancedAllocation: 调度 Pod 时,抉择资源分配更为平均的 Node;
NodeResourcesLeastAllocation: 调度 Pod 时,抉择资源分配较少的 Node;
NodeResourcesMostAllocation: 调度 Pod 时,抉择资源分配较多的 Node;
RequestedToCapacityRatio: 依据已分配资源的配置函数抉择偏爱 Node;
PodTopologySpread: 实现 Pod 拓扑散布;
SelectorSpread: 对于属于 Services、ReplicaSets 和 StatefulSets 的 Pod,偏好跨多节点部署;
ServiceAffinity: 查看属于某个服务(Service) 的 Pod 与配置的标签所定义的 Node 汇合是否适配,这个插件还反对将属于某个服务的 Pod 扩散到各个 Node;
TaintToleration: 实现了污点和容忍度;
打分之后通过 selectHost 抉择最终 pod 将被调度的节点:
func (g *genericScheduler) selectHost(nodeScoreList framework.NodeScoreList) (string, error) {if len(nodeScoreList) == 0 {return "", fmt.Errorf("empty priorityList")
}
maxScore := nodeScoreList[0].Score
selected := nodeScoreList[0].Name
cntOfMaxScore := 1
for _, ns := range nodeScoreList[1:] {
if ns.Score > maxScore {
maxScore = ns.Score
selected = ns.Name
cntOfMaxScore = 1
} else if ns.Score == maxScore {
cntOfMaxScore++
if rand.Intn(cntOfMaxScore) == 0 {
// Replace the candidate with probability of 1/cntOfMaxScore
selected = ns.Name
}
}
}
return selected, nil
}
至此,优选阶段完结。
总结
总结,k8s 定义了调度的接口,并实现了 genericScheduler(也是 k8s 中惟一的官网调度器)以及泛滥的插件,这层形象其实为开发人员自定义调度器提供了很大的便当。往小的说,各类插件以及扩大插件也提供了丰盛的细粒度管制。当然,最简略的还是去依据理论须要调整优选的打分逻辑,使得 Pod 的调度满足生产须要。