Kubernetes 开始只提供了 Extender ,通过部署一个 Web 服务实现无侵入式扩展 scheduler插件,但其存在以下几个问题:
-
Extender 扩展点的数量是有限的:在调度期间只有“Filter”和“Prioritize”扩展点。 “Preempt”扩展点在运行默认抢占机制后被调用。“Bind”扩展点用于绑定Pod,且 Bind 扩展点只能绑定一个扩展程序,扩展点会替换掉调度器的 bind 操作。Extender 不能在其他点被调用,例如,不能在运行 predicate 函数之前被调用。
-
性能问题:对扩展程序的每次调用都涉及 JSON 的编解码。调用 webhook(HTTP 请求)也比调用原生函数慢。
-
调度器无法通知 Extender 已中止 Pod 的调度。例如,如果一个 Extender 提供了一个集群资源,而调度器联系了 Extender 并要求它为正在调度的 pod 提供资源的实例,然后调度器在调度 pod 时遇到错误并决定中止调度,那么将很难与扩展程序沟通错误并要求它撤消资源的配置。
-
由于当前的Extender作为一个单独的进程运行,因此不能使用调度器的缓存。要么从 API Server构建自己的缓存,要么只处理他们从调度器接收到的信息。
Scheduling Framework 调度框架是一组新的“插件”API,通过这些 API 允许将许多调度功能使用插件实现,同时保持调度器“核心”简单和可维护。调度器插件必须用 Go 编写,并使用 Kubernetes 调度器代码编译,有一定学习成本。
虽然 Extender 简单,从长远考虑,尽量选择Scheduling Framework。下面是基于官方设计文档对这 2 种扩展方式进行详细整理。
Extender
Scheduler Extender实际上是一个额外的Web服务,通过注册外部 webhook 来扩展默认调度器以调整不同阶段的调度决策。根据自己的需要实现的Filter方法、Prioritize方法、Bind方法。
优点:
-
可以扩展现有调度器的功能,而无需重新编译二进制文件。
-
扩展器可以用任何语言编写。
-
实现后,可用于扩展不同版本的 kube-scheduler。
设计
在调度 pod 时,Extender 允许外部进程过滤节点并确定节点优先级。向 Extender 发出两个单独的 http/https 调用,即“filter”和“prioritize”操作。如果 pod 无法被调度,调度器将尝试从节点抢占较低优先级的 pod,并将它们发送到Extender的“preempt”接口(如果已配置)。Extender可以将节点子集和新被驱逐者返回给调度器。此外,Extender可以选择通过实现“bind”操作将 pod 绑定到 apiserver。
Configuration
要使用 Extender,必须创建调度器配置文件。 配置访问方式是 http 还是 https 以及超时配置等。
// Holds the parameters used to communicate with the extender. If a verb is unspecified/empty,
// it is assumed that the extender chose not to provide that extension.
type ExtenderConfig struct {
// URLPrefix at which the extender is available
URLPrefix string `json:"urlPrefix"`
// Verb for the filter call, empty if not supported. This verb is appended to the URLPrefix when issuing the filter call to extender.
FilterVerb string `json:"filterVerb,omitempty"`
// Verb for the preempt call, empty if not supported. This verb is appended to the URLPrefix when issuing the preempt call to extender.
PreemptVerb string `json:"preemptVerb,omitempty"`
// Verb for the prioritize call, empty if not supported. This verb is appended to the URLPrefix when issuing the prioritize call to extender.
PrioritizeVerb string `json:"prioritizeVerb,omitempty"`
// Verb for the bind call, empty if not supported. This verb is appended to the URLPrefix when issuing the bind call to extender.
// If this method is implemented by the extender, it is the extender's responsibility to bind the pod to apiserver.
BindVerb string `json:"bindVerb,omitempty"`
// The numeric multiplier for the node scores that the prioritize call generates.
// The weight should be a positive integer
Weight int `json:"weight,omitempty"`
// EnableHttps specifies whether https should be used to communicate with the extender
EnableHttps bool `json:"enableHttps,omitempty"`
// TLSConfig specifies the transport layer security config
TLSConfig *ExtenderTLSConfig `json:"tlsConfig,omitempty"`
// HTTPTimeout specifies the timeout duration for a call to the extender. Filter timeout fails the scheduling of the pod. Prioritize
// timeout is ignored, k8s/other extenders priorities are used to select the node.
HTTPTimeout time.Duration `json:"httpTimeout,omitempty"`
// NodeCacheCapable specifies that the extender is capable of caching node information,
// so the scheduler should only send minimal information about the eligible nodes
// assuming that the extender already cached full details of all nodes in the cluster
NodeCacheCapable bool `json:"nodeCacheCapable,omitempty"`
// ManagedResources is a list of extended resources that are managed by
// this extender.
// - A pod will be sent to the extender on the Filter, Prioritize and Bind
// (if the extender is the binder) phases iff the pod requests at least
// one of the extended resources in this list. If empty or unspecified,
// all pods will be sent to this extender.
// - If IgnoredByScheduler is set to true for a resource, kube-scheduler
// will skip checking the resource in filter plugins.
// +optional
ManagedResources []ExtenderManagedResource `json:"managedResources,omitempty"`
// Ignorable specifies if the extender is ignorable, i.e. scheduling should not
// fail when the extender returns an error or is not reachable.
Ignorable bool `json:"ignorable,omitempty"
}
配置文件示例:
apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
extenders:
- urlPrefix: "http://127.0.0.1:12345/api/scheduler"
filterVerb: "filter"
preemptVerb: "preempt"
prioritizeVerb: "prioritize"
bindVerb: "bind"
enableHttps: false
nodeCacheCapable: false
managedResources:
- name: opaqueFooResource
ignoredByScheduler: true
ignorable: false
可以配置多个 Extender,调度器会按顺序调用。
Interface
Extender Web模块可以根据需要实现以下4个接口,每个接口都有对应的request和response。
Filter
Request
// ExtenderArgs represents the arguments needed by the extender to filter/prioritize
// nodes for a pod.
type ExtenderArgs struct {
// Pod being scheduled
Pod api.Pod `json:"pod"`
// List of candidate nodes where the pod can be scheduled
Nodes api.NodeList `json:"nodes"`
// List of candidate node names where the pod can be scheduled; to be
// populated only if Extender.NodeCacheCapable == true
NodeNames *[]string
}
Response
// FailedNodesMap represents the filtered out nodes, with node names and failure messages
type FailedNodesMap map[string]string
// ExtenderFilterResult represents the results of a filter call to an extender
type ExtenderFilterResult struct {
// Filtered set of nodes where the pod can be scheduled; to be populated
// only if Extender.NodeCacheCapable == false
Nodes *v1.NodeList
// Filtered set of nodes where the pod can be scheduled; to be populated
// only if Extender.NodeCacheCapable == true
NodeNames *[]string
// Filtered out nodes where the pod can't be scheduled and the failure messages
FailedNodes FailedNodesMap
// Filtered out nodes where the pod can't be scheduled and preemption would
// not change anything. The value is the failure message same as FailedNodes.
// Nodes specified here takes precedence over FailedNodes.
FailedAndUnresolvableNodes FailedNodesMap
// Error message indicating failure
Error string
}
Prioritize
Request
同 Filter Request
Response
// HostPriority represents the priority of scheduling to a particular host, higher priority is better.
type HostPriority struct {
// Name of the host
Host string
// Score associated with the host
Score int64
}
// HostPriorityList declares a []HostPriority type.
type HostPriorityList []HostPriority
Bind
Request
// ExtenderBindingArgs represents the arguments to an extender for binding a pod to a node.
type ExtenderBindingArgs struct {
// PodName is the name of the pod being bound
PodName string
// PodNamespace is the namespace of the pod being bound
PodNamespace string
// PodUID is the UID of the pod being bound
PodUID types.UID
// Node selected by the scheduler
Node string
}
Response
// ExtenderBindingResult represents the result of binding of a pod to a node from an extender.
type ExtenderBindingResult struct {
// Error message indicating failure
Error string
}
Preempt
Request
// ExtenderPreemptionArgs represents the arguments needed by the extender to preempt pods on nodes.
type ExtenderPreemptionArgs struct {
// Pod being scheduled
Pod *v1.Pod
// Victims map generated by scheduler preemption phase
// Only set NodeNameToMetaVictims if Extender.NodeCacheCapable == true. Otherwise, only set NodeNameToVictims.
NodeNameToVictims map[string]*Victims
NodeNameToMetaVictims map[string]*MetaVictims
}
Response
// ExtenderPreemptionResult represents the result returned by preemption phase of extender.
type ExtenderPreemptionResult struct {
NodeNameToMetaVictims map[string]*MetaVictims
}
Scheduling Framework
调度框架是一组被添加到现有的 Kubernetes 调度器中新的“插件” API。这些 API 允许将许多调度功能实现为插件,并编译到调度器中,同时保持调度“核心”简单和可维护。
随着越来越多功能添加到 Kubernetes 调度器中。这些新功能不断地使代码量更大,逻辑更复杂。从而导致调度器更难维护,错误更难发现和修复,并且那些运行自定义调度器的用户很难赶上和集成新的变化。虽然当前的 Kubernetes 调度器提供了 webhook 来扩展其功能。但是,这些在以下几个方面受到限制:
-
Extender 扩展点的数量是有限的:在调度期间只有“Filter”和“Prioritize”扩展点。 “Preempt”扩展点在运行默认抢占机制后被调用。“Bind”扩展点用于绑定Pod,且 Bind 扩展点只能绑定一个扩展程序,扩展点会替换掉调度器的 bind 操作。Extender 不能在其他点被调用,例如,不能在运行 predicate 函数之前被调用。
-
性能问题:对扩展程序的每次调用都涉及 JSON 的编解码。调用 webhook(HTTP 请求)也比调用原生函数慢。
-
调度器无法通知 Extender 已中止 Pod 的调度。例如,如果一个 Extender 提供了一个集群资源,而调度器联系了 Extender 并要求它为正在调度的 pod 提供资源的实例,然后调度器在调度 pod 时遇到错误并决定中止调度,那么将很难与扩展程序沟通错误并要求它撤消资源的配置。
-
由于当前的 Extender 作为一个单独的进程运行,因此不能使用调度器的缓存。要么从 API Server构建自己的缓存,要么只处理他们从调度器接收到的信息。
上述限制阻碍了构建高性能和更多功能的调度器。理想情况下,希望有一个足够快的扩展机制,以允许将现有功能转换为插件,例如predicate和priority函数。 此类插件将被编译到调度器二进制文件中。此外,自定义调度器的作者可以使用(未修改的)调度器代码和他们自己的插件来编译自定义调度器。
设计目标
-
使调度器更具可扩展性。
-
通过将调度器核心的一些功能移至插件,使调度器核心更简单。
-
在框架中提出扩展点。
-
提出一种机制来接收插件结果并根据收到的结果继续或中止。
-
提出一种机制来处理错误并与插件进行通信。
调度框架在 Kubernetes 调度器中定义了新的扩展点和 Go API,以供“plugins”使用。 插件将调度行为添加到调度器,并在编译时包含进来。调度器的ComponentConfig 将允许启用、禁用和重新排序插件。自定义调度器可以“out-of-tree”编写他们的插件,并编译一个包含他们自己的插件的调度器二进制文件。
调度周期和绑定周期
每次调度一个 pod 的都分为两个阶段:调度周期和绑定周期。 调度周期为 pod 选择一个节点,绑定周期将该调度结果应用于集群。调度周期和绑定周期一起被称为“调度上下文”。 调度周期串行运行,而绑定周期可能并发运行。
如果确定 pod 不可调度或存在内部错误,则可以中止调度周期或绑定周期。 pod 将返回队列并重试。 如果绑定循环被中止,它将触发 Reserve 插件中的 Unreserve 方法。
扩展点
下图展示了 Pod 的调度上下文和调度框架暴露的扩展点。在这张图中,“Filter”相当于 Extender 的“Predicate”,“Scoring”相当于“Priority”。插件注册为在这些扩展点中的一个或多个处调用。 在以下部分中,我们按照调用它们的相同顺序描述每个扩展点。
Queue sort
这些插件用于对调度队列中的 pod 进行排序。其本质上将提供less(pod1, pod2)
功能。一次只能启用一个队列排序插件。
PreFilter
PreFilter 在每个调度周期中调用一次。这些插件用于预处理有关 pod 的信息,或检查集群、pod 必须满足的某些条件。一个前置过滤器插件应该实现一个PreFilter 接口,如果前置过滤器返回一个错误,则调度周期被中止。
Pre-filter 插件可以实现可选的 PreFilterExtensions 接口,该接口定义 AddPod 和 RemovePod 方法以增量修改其预处理信息。框架保证这些函数只会在 PreFilter 之后调用,在克隆的 CycleState 上,并且在调用特定节点上的 Filter 之前可能会多次调用这些函数。
Filter
用于过滤掉无法运行 Pod 的节点。对于每个节点,调度器将按照配置的顺序调用filter插件。如果任何filter插件将该节点标记为不可行,则不会为该节点调用其余插件。节点可能会被并发地评估,并且可以在同一调度周期中多次调用filter。
PostFilter
在Filter阶段之后被调用,仅在没有为 pod 找到可行节点时调用。插件按其配置的顺序调用。如果任何 PostFilter 插件将该节点标记为可调度,则不会调用其余插件。一个典型的 PostFilter 实现是抢占,试图通过抢占其他 Pod 来使 Pod可调度。
PreScore
用于执行PreScore的信息扩展点。将使用过滤阶段的节点列表调用插件 插件可以使用这些数据来更新内部状态或生成logs/metrics。
Scoring
这里的插件有两个阶段:
第一阶段称为“score”,用于对已通过过滤阶段的节点进行排名。调度器将为每个节点调用每个评分插件的 Score函数。
第二阶段是“normalize scoring”,用于在调度器计算节点的最终排名之前修改分数,并且每个评分插件在“normalize scoring”阶段接收同一插件给所有节点的分数。在“score”阶段之后,每个调度周期每个插件都会调用 NormalizeScore 一次。NormalizeScore 是可选的,可以通过实现 ScoreExtensions 接口来提供。
score插件的输出必须是 [MinNodeScore, MaxNodeScore] 范围内的整数。如果不是,则调度周期被中止。这是运行插件的可选 NormalizeScore 函数后的输出。如果未提供 NormalizeScore,则 Score 的输出必须在此范围内。在可选的 NormalizeScore 之后,调度器将根据配置的插件权重组合来自所有插件的节点分数。
例如,假设插件 BlinkingLightScorer 根据节点的闪烁灯数对节点进行排名。
func (*BlinkingLightScorer) Score(state *CycleState, _ *v1.Pod, nodeName string) (int, *Status) {
return getBlinkingLightCount(nodeName)
}
但是,与 MaxNodeScore 相比,闪烁灯的最大计数可能很小。 为了解决这个问题,BlinkingLightScorer 还应该实现 NormalizeScore。
func (*BlinkingLightScorer) NormalizeScore(state *CycleState, _ *v1.Pod, nodeScores NodeScoreList) *Status {
highest := 0
for _, nodeScore := range nodeScores {
highest = max(highest, nodeScore.Score)
}
for i, nodeScore := range nodeScores {
nodeScores[i].Score = nodeScore.Score*MaxNodeScore/highest
}
return nil
}
如果 Score 或 NormalizeScore 返回错误,则调度周期中止。
Reserve
实现 Reserve 扩展的插件有两种方法,即 Reserve 和 Unreserve,它们分别支持两个信息调度阶段,分别称为 Reserve 和 Unreserve。维护运行时状态的插件(又名“stateful plugins”)应该使用这些阶段来通知调度器何时节点上的资源正在为给定 Pod Reserve 和Unreserve。
Reserve 阶段发生在调度器实际将 Pod 绑定到其指定节点之前。它的存在是为了防止调度器等待绑定成功时出现竞争条件。每个 Reserve 插件的 Reserve 方法可能成功或失败; 如果一个 Reserve 方法调用失败,则不会执行后续插件,并且 Reserve 阶段被视为失败。 如果所有插件的 Reserve 方法都成功,则认为 Reserve 阶段成功,并执行剩余的调度周期和绑定周期。
如果 Reserve 阶段或后续阶段失败,则会触发 Unreserve 阶段。发生这种情况时,所有 Reserve 插件的 Unreserve 方法将按照 Reserve 方法调用的相反顺序执行。 此阶段的存在是为了清理与保留 Pod 关联的状态。
注意:Reserve 插件中 Unreserve 方法的实现必须是幂等的,不能失败。
Permit
这些插件用于阻止或延迟 Pod 的绑定。 Permit插件可以做三件事之一。
- approve 批准
一旦所有 Permit 插件都批准了一个 pod,它就会被发送以进行绑定。
- deny 否定
如果任何 Permit 插件拒绝了一个 pod,它会被返回到调度队列。 这将触发 Reserve 插件中的 Unreserve 方法。
- wait等待(超时)
如果许可插件返回“wait”,则 pod 将保持在Permit阶段,直到插件批准它为止。 如果超时,wait 变为deny ,pod 会返回调度队列,在 Reserve 阶段触发 unreserve 方法。
Permit插件作为调度周期的最后一步执行,但是在Permit阶段的等待发生在绑定周期的开始,在 PreBind 插件执行之前。
批准 pod 绑定
虽然任何插件都可以从缓存中接收保留 Pod 的列表并批准它们(请参阅 FrameworkHandle),但我们希望只有许可插件批准绑定处于“waiting”状态的reserved Pod。 一旦 Pod 被批准,它就会被发送到预绑定阶段。
PreBind
用于在绑定 pod 之前执行所需的任何工作。例如,预绑定插件可能会提供一个网络卷并将其挂载到目标节点上,然后才允许 pod 运行。
如果任何 PreBind 插件返回错误,则该 pod 将被拒绝并返回到调度队列。
Bind
用于将 pod 绑定到节点。在所有 PreBind 插件完成之前,不会调用绑定插件。每个绑定插件都按配置的顺序调用。绑定插件可以选择是否处理给定的 Pod。如果绑定插件选择处理 Pod,则跳过剩余的绑定插件。
PostBind
这是一个信息扩展点。PostBind 插件在 Pod 成功绑定后调用。这时绑定周期的结束,可用于清理相关资源。
Plugin API
插件 API 有两个步骤。 1. 注册和配置,2. 使用扩展点接口。扩展点接口有以下形式。
type Plugin interface {
Name() string
}
type QueueSortPlugin interface {
Plugin
Less(*PodInfo, *PodInfo) bool
}
type PreFilterPlugin interface {
Plugin
PreFilter(CycleState, *v1.Pod) *Status
}
// ...
CycleState
大多数插件函数(除了队列排序插件)将使用 CycleState 参数调用。CycleState 表示当前调度上下文。
CycleState 将提供用于访问范围为当前调度上下文的数据的 API。 因为绑定循环可能并发执行,插件可以使用 CycleState 来确保它们处理正确的请求。
CycleState 还提供了一个类似于 context.WithValue 的 API,可用于在不同扩展点的插件之间传递数据。多个插件可以通过这种机制共享状态或通信。该状态仅在单个调度上下文期间保留。注意,插件被假定为受信任的。调度器不会阻止一个插件访问或修改另一个插件的状态。
警告:通过 CycleState 引用的数据在调度上下文结束后无效,并且插件不应持有对该数据的引用超过必要的时间。
FrameworkHandle
CycleState 提供与单个调度上下文相关的 API,而 FrameworkHandle 提供与插件生命周期相关的 API。这就是插件如何获取客户端(kubernetes.Interface)和 SharedInformerFactory,或者从调度器的集群状态缓存中读取数据的方式。该句柄还将提供 API 来列出和批准/拒绝等待的 pod。
警告:FrameworkHandle 提供对 kubernetes API Server和调度器内部缓存的访问。但两者不能保证同步,因此在编写使用来自它们两者的数据的插件时应格外小心。
提供对 API 服务器的插件访问对于实现有用的功能是必要的,尤其是当这些功能使用调度器通常不考虑的对象类型时。提供 SharedInformerFactory 允许插件安全地共享缓存。
Plugin Registration
每个插件都必须定义一个构造函数并将其添加到硬编码注册表中。有关构造函数参数的更多信息,请参阅可选参数。
type PluginFactory = func(runtime.Unknown, FrameworkHandle) (Plugin, error)
type Registry map[string]PluginFactory
func NewRegistry() Registry {
return Registry{
fooplugin.Name: fooplugin.New,
barplugin.Name: barplugin.New,
// New plugins are registered here.
}
}
也可以将插件添加到注册表对象并将其注入调度器。 请参阅自定义调度器插件
Plugin Lifecycle
Initialization
插件初始化有两个步骤。 首先,注册插件。 其次,调度器使用其配置来决定要实例化哪些插件。如果一个插件注册了多个扩展点,它只会被实例化一次。
当一个插件被实例化时,它会被传递 config args 和一个 FrameworkHandle。
Concurrency
插件编写者应该考虑两种类型的并发。在评估多个节点时,一个插件可能会同时被调用多次,并且一个插件可能会从不同的调度上下文中被同时调用。
注意:在一个调度上下文中,每个扩展点都被串行评估。
在调度器的主线程中,一次只处理一个调度周期。直到并包括 permit 在内的任何扩展点都将在下一个调度周期开始之前完成(队列排序扩展点是一个特例。 它不是调度上下文的一部分,并且可以为许多 pod 对同时调用)。在 permit 扩展点之后,异步执行绑定周期。这意味着插件可能会被两个不同的调度上下文同时调用。有状态的插件应该注意处理这些情况。
最后,可以从主线程或绑定线程调用 Reserve 插件中的 Unreserve 方法,具体取决于 pod 被拒绝的方式。
Configuring Plugins
调度器的组件配置将允许启用、禁用或以其他方式配置插件。插件配置分为两部分。
-
每个扩展点的启用插件列表(以及它们应该运行的顺序)。 如果省略这些列表之一,将使用默认列表。
-
每个插件都有一组可选的自定义参数。省略插件的参数等同于使用该插件的默认配置。
插件配置由扩展点组织。每个列表中必须包含注册多个点的插件。
type KubeSchedulerConfiguration struct {
// ... other fields
Plugins Plugins
PluginConfig []PluginConfig
}
type Plugins struct {
QueueSort []Plugin
PreFilter []Plugin
Filter []Plugin
PostFilter []Plugin
PreScore []Plugin
Score []Plugin
Reserve []Plugin
Permit []Plugin
PreBind []Plugin
Bind []Plugin
PostBind []Plugin
}
type Plugin struct {
Name string
Weight int // Only valid for Score plugins
}
type PluginConfig struct {
Name string
Args runtime.Unknown
}
Example:
{
"plugins": {
"preFilter": [
{
"name": "PluginA"
},
{
"name": "PluginB"
},
{
"name": "PluginC"
}
],
"score": [
{
"name": "PluginA",
"weight": 30
},
{
"name": "PluginX",
"weight": 50
},
{
"name": "PluginY",
"weight": 10
}
]
},
"pluginConfig": [
{
"name": "PluginX",
"args": {
"favorite_color": "#326CE5",
"favorite_number": 7,
"thanks_to": "thockin"
}
}
]
}
Enable/Disable
配置则为指定扩展点的插件列表是唯一启用的。如果某个扩展点省略配置,则该扩展点使用默认插件集。
Change Evaluation Order
插件执行顺序由插件在配置中出现的顺序决定。注册多个扩展点的插件可以在每个扩展点有不同的顺序。
Optional Args
插件可以从其配置中接收具有任意结构的参数。因为一个插件可能出现在多个扩展点中,所以配置位于 PluginConfig 的单独列表中。
例如
{
"name": "ServiceAffinity",
"args": {
"LabelName": "app",
"LabelValue": "mysql"
}
}
func NewServiceAffinity(args *runtime.Unknown, h FrameworkHandle) (Plugin, error) {
if args == nil {
return nil, errors.Errorf("cannot find service affinity plugin config")
}
if args.ContentType != "application/json" {
return nil, errors.Errorf("cannot parse content type: %v", args.ContentType)
}
var config struct {
LabelName, LabelValue string
}
if err := json.Unmarshal(args.Raw, &config); err != nil {
return nil, errors.Wrap(err, "could not parse args")
}
//...
}
向后兼容性
当前的 KubeSchedulerConfiguration 类型有 apiVersion:kubescheduler.config.k8s.io/v1alpha1
。 这种新的配置格式是v1alpha2
或 v1beta1
。 较新版本的调度器解析 v1alpha1
时,“policy”部分将用于构造等效的插件配置。
与Cluster Autoscaler的交互
Cluster Autoscaler 必须运行Filter plugins而不是predicates。 这可以通过创建一个Framework实例并调用 RunFilterPlugins 来完成。