上车了!一文尽览Scheduling Framework 应用实践

 

 

Kubernetes 是目前最受欢迎的⾃动化容器管理平台,它提供了灵活的声明式容器编排、自动部署、资源调度等功能。Kube-Scheduler 做为 Kubernetes 的核心组件之一,主要负责整个集群资源的调度功能,根据特定的调度算法和策略,将 Pod 调度到最优的工做节点上面去,从而更加合理、充分地利用集群资源。node

可是随着 Kubernetes 部署的任务类型愈来愈多,原生 Kube-Scheduler 已经不能应对多样的调度需求:好比机器学习、深度学习训练任务对于协同调度功能的需求;高性能计算做业,基因计算工做流对于一些动态资源 GPU、网络、存储卷的动态资源绑定需求等。所以自定义 Kubernetes 调度器的需求愈发迫切,本文讨论了扩展 Kubernetes 调度程序的各类方法,而后使用目前最佳的扩展方式 Scheduling Framework,演示如何扩展 Scheduler。nginx

01 自定义调度器的方式

02 Scheduling Framework 解析

以下图所示,调度框架提供了丰富的扩展点,在这幅图中,Filter 至关于以前 Predicate 预选模块, Score 至关于 Priority 优选模块,每个扩展点模块都提供了接口,咱们能够实现扩展点定义的接口来实现本身的调度逻辑,并将实现的插件注册到扩展点。算法

Scheduling Framework 在执行调度流程时,当运行到扩展点时,会调用咱们注册的插件,经过执行自定义插件的策略,知足调度需求。此外,一个插件能够在多个扩展点注册,用以执行更复杂或有状态的任务。api

Scheduling Framework 每次调度一个 Pod ,都分为调度周期和绑定周期两部分来执行,调度周期为 Pod 选择一个节点,绑定周期则调用 Apiserver,用选好的 Node,更新 Pod 的 spec.nodeName 字段。调度周期和绑定周期统称为 “Scheduling Context” (调度上下文)。调度周期是串行运行的,同一时间只能有一个 Pod 被调度,是线程安全的;而绑定周期由于须要访问 Apiserver 的接口,耗时较长,为了提升调度的效率,须要异步执行,即同一时间,可执行多个 bind 操做,是非线程安全的。安全

若是 Pod 被肯定为不可调度或存在内部错误,那么调度周期或绑定周期将被停止。Pod 将返回队列并等待下一次重试。若是一个绑定周期被终止,它将触发 Reserve 插件中的 UnReserve 方法。服务器

Scheduling Cycle 的扩展点

QueueSort

用于给调度队列排序,默认状况下,全部的 Pod 都会被放到一个队列中,此扩展用于对 Pod 的待调度队列进行排序,以决定先调度哪一个 Pod,QueueSort 扩展本质上只须要实现一个方法  Less(Pod1, Pod2)  用于比较两个 Pod 谁更优先得到调度,同一时间点只能有一个 QueueSort 插件生效。微信

PreFilter

用于对 Pod 的信息进行预处理,或者检查一些集群或 Pod 必须知足的前提条件,好比 Pod 是否包含指定的 annotations 或 labels,若是 PreFilter 返回了 error ,则调度过程终止。网络

Filter

用于排除那些不能运行该 Pod 的节点,对于每个节点,调度器将按顺序执行 Filter 扩展,若是任何一个 Filter 将节点标记为不可选,则余下的 Filter 扩展将不会被执行。若是对默认调度器提供的预选规则不满意,能够在配置中禁用默认调度器的预选算法,在这个扩展点只执行本身自定义的过滤逻辑。Node 节点执行 Filter 策略是并发执行的,因此在同一调度周期中屡次调用过滤器。架构

PostFilter

实现此扩展的插件是在 Filter 阶段以后被调用,仅当没有为 Pod 找到可行的节点时才调用。若是有任何 PostFilter 插件将节点标记为可调度节点,则后面的 PostFilter 插件就不会被调用了。一个典型的 PostFilter 实现是抢占,它试图经过抢占其余 Pod 来使 Pod 可调度。并发

PreScore

在预选后被调用,一般用来在 Score 以前进行一些信息生成或者记录日志和监控信息

Score

实现此扩展的插件为已经过过滤阶段的全部节点进行打分,调度器将针对每个节点调用 Score 扩展。

NormalizeScore

在 NormalizeScore 阶段,调度器将会把每一个 Score 扩展对具体某个节点的评分结果和该扩展的权重合并起来,做为最终评分结果,评分结果是一个范围内的整数。若是 Score 或 NormalizeScore 返回错误,则调度周期将停止。

Reserve

此扩展点为 Pod 预留的在要运行节点上的资源,目的是避免调度器在等待 Pod 与节点绑定的过程当中调度新的 Pod 到节点上时,发生实际使用资源超出可用资源的状况。(由于绑定 Pod 到节点上是异步发生的)。这是调度过程的最后一个步骤,Pod 进入 Reserved 状态之后,要么在绑定失败时,触发 Unreserve 扩展,要么在绑定成功时,由 PostBind 扩展结束绑定过程。

Permit

Permit 扩展,发生在 Pod 使用 Reserve 插件预留资源以后, Bind 扩展点 bind 以前,主要有三种策略,批准、拒绝、等待。

1)approve(批准):当全部的 Permit 扩展都批准了 Pod 与节点的绑定,调度器将继续执行绑定过程

2)deny(拒绝):若是任何一个 Permit 扩展 deny 了 Pod 与节点的绑定,Pod 将被放回到待调度队列,此时将触发 Unreserve 扩展

3)wait(等待):若是一个 Permit 扩展返回了 wait,则 Pod 将保持在 Permit 阶段,直到被其余扩展 approve,若是超时事件发生,wait 状态变成 deny,Pod 将被放回到待调度队列,此时将触发 UnReserve 扩展

Binding Cycle 的扩展点

PreBind

扩展用于在 Pod 绑定以前执行某些逻辑。这个插件引入的缘由,是有一些资源,是在不在调度 Pod 时,当即肯定可用的节点的资源,因此调度程序须要确保,这些资源已经成功绑定到选定的节点后,才能将 Pod 调度到此节点。例如,PreBind 扩展能够将一个基于网络的数据卷挂载到节点上,以Pod 可使用。若是任何一个 PreBind 扩展返回错误,Pod 将被放回到待调度队列,此时将触发 Unreserve 扩展。

Bind

Bind 扩展会调用 apiserver 提供的接口,将 Pod 绑定到对应的节点上。

PostBind

PostBind 是一个信息扩展点。成功绑定 Pod 后,将调用 PostBind 插件,可用于清理关联的资源。

UnReserve

是一个通知性质的扩展,若是为 Pod 预留了资源,Pod 又在被绑定过程当中被拒绝绑定,则 Unreserve 扩展将被调用。Unreserve 扩展应该释放已经为 Pod 预留的节点上的计算资源。在一个插件中,Reserve 扩展和 UnReserve 扩展应该成对出现。

03 使用 Scheduling Framework 自定义 Scheduler

自定义插件须要两个步骤:

1)实现插件的接口

2)注册插件并配置插件

3.1 实现插件的接口

这里咱们实现 QueueSort 扩展点,先看看 QueueSort 扩展点定义的接口:

// QueueSortPlugin is an interface that must be implemented by "QueueSort" plugins.// These plugins are used to sort pods in the scheduling queue. Only one queue sort// plugin may be enabled at a time.type QueueSortPlugin interface { Plugin // Less are used to sort pods in the scheduling queue. Less(*QueuedPodInfo, *QueuedPodInfo) bool}

默认的调度器会优先调度优先级较高的 Pod , 其具体实现的方式是使用 QueueSort 这个插件,默认的实现,是对 Pod 的 Priority 值进行排序,但当优先级相同时,再比较 Pod 的 timestamp ,  timestamp 是 Pod 加入 queue 的时间。咱们如今想先根据 Pod 的 Priority 值进行排序,当 Priority 值相同,再根据 Pod 的 QoS 类型进行排序,最后再根据 Pod 的 timestamp 排序。

Qos 类型以下:

1)Guaranteed : resources limits 和 requests 相等

2)Burstable : resources limits 和 requests 不相等

3)BestEffort : 未设置 resources limits 和 requests

具体是,Guaranteed 优先级 高于 Burstable,Burstable 优先级高于 BestEffort。

插件的实现,其实咱们只须要实现 QueueSortPlugin 的 Less 方法:








package qosimport ( v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/kubernetes/pkg/api/v1/pod" v1qos "k8s.io/kubernetes/pkg/apis/core/v1/helper/qos" framework "k8s.io/kubernetes/pkg/scheduler/framework/v1alpha1")// Name is the name of the plugin used in the plugin registry and configurations.const Name = "QOSSort"// Sort is a plugin that implements QoS class based sorting.type Sort struct{}var _ framework.QueueSortPlugin = &Sort{}// Name returns name of the plugin.func (pl *Sort) Name() string { return Name}// Less is the function used by the activeQ heap algorithm to sort pods.// It sorts pods based on their priorities. When the priorities are equal, it uses// the Pod QoS classes to break the tie.func (*Sort) Less(pInfo1, pInfo2 *framework.PodInfo) bool { p1 := pod.GetPodPriority(pInfo1.Pod) p2 := pod.GetPodPriority(pInfo2.Pod) return (p1 > p2) || (p1 == p2 && compQOS(pInfo1.Pod, pInfo2.Pod)) || (p1 == p2 && pInfo1.Timestamp.Before(pInfo2.Timestamp))}func compQOS(p1, p2 *v1.Pod) bool { p1QOS, p2QOS := v1qos.GetPodQOS(p1), v1qos.GetPodQOS(p2) if p1QOS == v1.PodQOSGuaranteed { return true } if p1QOS == v1.PodQOSBurstable { return p2QOS != v1.PodQOSGuaranteed } return p2QOS == v1.PodQOSBestEffort}// New initializes a new plugin and returns it.func New(_ *runtime.Unknown, _ framework.FrameworkHandle) (framework.Plugin, error) { return &Sort{}, nil}

注意:一个 Plugin 能够实现多个扩展点。即在一个 Plugin 中既能够实现 Filter,又能够实现 Score,也能够再实现 PreBind。

3.2 注册和配置插件

1)注册指向默认调度器中注册插件。

2)配置是经过配置来决定哪些插件须要被初始化。

3.2.1 在 Sheduler 中注册已经实现的 Qos 插件


func main() { rand.Seed(time.Now().UnixNano()) command := app.NewSchedulerCommand( app.WithPlugin(qos.Name, qos.New), ) logs.InitLogs() defer logs.FlushLogs() if err := command.Execute(); err != nil { os.Exit(1) }}

3.2.2 经过配置来使用 Qos 插件,并部署自定义调度器

1)配置

经过配置让 Sheduler 知道那些插件须要被初始化,以下面指定了 QueueSort 的插件 Qos,其余的扩展点的插件没有被指定,则都会 Kube-Scheduler 的默认的实现。能够看到,schedulerName 字段表明扩展的调度器名称, plugins 字段中各个扩展点的插件名称,enable 表明该扩展点关于运行你的插件。

apiVersion: v1kind: ConfigMapmetadata: name: scheduler-config3 namespace: kube-systemdata: scheduler-config.yaml: | apiVersion: kubescheduler.config.k8s.io/v1alpha1 kind: KubeSchedulerConfiguration schedulerName: qos-scheduler leaderElection: leaderElect: true lockObjectName: qos-scheduler lockObjectNamespace: kube-system plugins: queueSort: enabled: - name: "QOSSort"

2)接着为调度器建立 RBAC

kind: ClusterRoleapiVersion: rbac.authorization.k8s.io/v1metadata: name: qos-crrules: - apiGroups: - '*' resources: - '*' verbs: - '*' - nonResourceURLs: - '*' verbs: - '*'---apiVersion: v1kind: ServiceAccountmetadata: name: qos-sa namespace: kube-system---kind: ClusterRoleBindingapiVersion: rbac.authorization.k8s.io/v1metadata: name: qos-crb namespace: kube-systemroleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: qos-crsubjects: - kind: ServiceAccount name: qos-sa namespace: kube-system

3)配置调度器的 Deployment 

apiVersion: apps/v1kind: Deploymentmetadata: name: qos-scheduler namespace: kube-system labels: component: qos-schedulerspec: replicas: 1 selector: matchLabels: component: qos-scheduler template: metadata: labels: component: qos-scheduler spec: imagePullSecrets: - name: hybrid-regsecret serviceAccount: qos-sa priorityClassName: system-cluster-critical volumes: - name: scheduler-config3 configMap: name: scheduler-config3 containers: - name: qos-scheduler image: hub.baidubce.com/kun/sxy/qos-scheduler:v1.0.0 imagePullPolicy: Always args: - /qos-sample-scheduler - --config=/scheduler/scheduler-config.yaml - --v=3 resources: requests: cpu: "50m" volumeMounts: - name: scheduler-config3 mountPath: /scheduler

4)使用 kubectl apply 部署后,能够看到 qos-scheduler 已经启动了

$ kubectl get pods -n kube-systemNAME READY STATUS RESTARTS AGEqos-scheduler-79c767954f-225mr 1/1 Running 0 44m

5) 使用自定义调度器调度 Pod

在 Pod 中的 spec.schedulerName 上指定 qos-scheduler,自定义调度器将会为 Pod 执行调度逻辑。

apiVersion: v1kind: Podmetadata: name: test labels: app: testspec: schedulerName: qos-scheduler containers: - image: nginx name: nginx ports: - containerPort: 80

建立后,能够看到 Pod 已经被正常调度,并启动成功。

$ kubectl get podsNAME READY STATUS RESTARTS AGEtest 1/1 Running 0 15s

04 使用调度框架的其余示例

4.1 Coscheduling(协同调度)

有时候,咱们须要使用协同调度,相似于 Kube-batch 的功能(也称为“ Gang 调度”)。Gang 调度容许同时安排必定数量的 Pod 。若是 Gang 的全部成员不能同时调度,他们都不该该调度。Scheduling Framework 中的 Gang 调度可使用 “Permit” 插件完成。

1)主调度线程逐个处理 Pod 并为它们预留节点,每一个 Pod 都会调用准入阶段的 Gang 调度插件。

2) 当它发现 Pod 属于一个 Gang 时,它会检查 Gang 的属性。若是没有足够的成员按期或处于“等待”状态,该插件返回“等待”。

3) 当数字达到指望值时,处于等待状态的全部 Pod 均被批准并发送进行绑定。

4.2 Dynamic Resource Binding(动态资源绑定)

在调度 Pod 时,有一些动态的资源好比 volumn ,还不属于备选节点的资源,调度程序须要确保此类集群级资源绑定到选定的节点,而后才能将pod调度到有此类资源的节点的节点。可使用 Scheduling Framework 的 PreBind 扩展点实现插件,完成这种动态资源的绑定功能。

05总结

本文首先分析了几种扩展调度器方式的优缺点,以后介绍了 Scheduling Framework 的实现原理,以及如何基于 Scheduling Framework 去实现一个自定义插件。Scheduling Framework 做为目前扩展 Kubernetes 调度器的最佳方式,采用插件和插件扩展点机制,能够轻松对调度器进行扩展、定制。将来,会用更多的调度需求使用 Scheduling Framework 进行扩展。

06落地

Kubernetes 技术已然成为了容器编排领域的事实标准。百度从 2010 年开始探索容器和集群管理技术,2016年自主研发的 Matrix 集群管理系统已经管理了数十万台机器和服务。

随着 Kubernetes 技术的成熟,咱们看到了开源技术强大的生命力,从 2018 年开始尝试向 Kubernetes 架构演化。试点成功以后,启动了大规模 Kubernetes 架构融合项目。一方面保留百度在 Matrix 上积累的核心技术能力,另外一方面让存量业务能够更低成本的迁移到 Kubernetes 之上。

百度于 2020 年得到 InfoQ 十大云原生创新技术方案,对百度云原生来讲仅仅是个开始。目前大规模 Kubernetes 融合架构的业务正在百度云原生各产品技术架构中稳定运行并持续增加,百度云原生团队也将会在继续服务好客户的同时,利用Kubernetes技术实践经验不断优化产品,更好地助力各行各业的客户实现基于云原生架构的数字化转型。

百度智能云云原平生台,为客户建设容器化和无服务器化的基础设施,提供企业级的微服务治理能力,同时集成源自百度自身多年实践的DevOps工具链。保障开发者享受到高效、灵活、弹性的开发与运维体验,助力企业更高效率低风险地构建云原生应用,普遍应用于金融、互联网、制造等各行各业的云原生转型阶段。

重磅!云原生计算交流群成立

扫码添加小助手便可申请加入,必定要备注:名字-公司/学校-地区,根据格式备注,才能经过且邀请进群。

了解更多微服务、云原生技术的相关信息,请关注咱们的微信公众号【云原生计算】