k8s中pod如何调度?

k8s中pod如何调度?

当创建k8s pod的时候调度器会决定pod在哪个node上被创建且运行,

调度器给apiserver发出了一个创建pod的api请求,apiserver首先将pod的基本信息保存在etcd,apiserver又会把这些信息给到每个node上的kubelet进程,kubelet一直在监听这些信息,当kubelet发现这个pod的节点信息跟它当前运行的节点一致的时候,就会创建pod进程以及容器当中的docker image进程,创建相应的命名空间,使得进程之间互相隔离,这样pod就在这个节点上运行起来了。

k8s调度器会尽量的去保证所有节点上的资源是相对平衡的,判断节点资源(CPU、内存、存储、端口等)是否适合Pod的资源申请。

查看K8s资源在etcd中的信息

借助kube-etcd-helper这个工具查看etcd中的内容,

写一个操作etcd命令的脚本./etcdheloper.sh,指定etcd的地址,鉴权需要的证书等信息,

查看k8s资源列表,./etcdheloper.sh ls

这是etcd中保存的k8s资源信息,查看指定的pod信息,

跟调度器相关的是这个nodeName,

验证调度器的工作方式

有了etcd helper可以更加详细的看下调度器的工作原理,调度器一直在监听k8s中的pod的创建,通过etcd watch的功能可以去监听一个pod的创建并且看到创建的整个过程。

创建这个pod,使用etcd helper来监听下这个pod在etcd当中变动的过程,

通过这个命令可以看到在etcd中关于这个pod产生了4次变动,每次变动都是一个json,通过JSON Diff工具比较每次json都变动了哪些内容,

第一个json和第二个json比较,多了一个nodeName,

第一次给apiserver发送请求把这个信息保存在etcd当中的时候还没有nodeName,第二次就是更新nodeName,调度器通过算法决定了这个pod要在这个node上创建,

这里声明了pod已经被调度了;

第三次的json相比第二次json的变更内容:

记录了pod中container容器的启动状态和pod的ip。

Pod指定节点运行

这是集群中node的情况,

查看指定node的详情,红色部分决定了node的名称,

这个分别代表节点所在的区域和时区,

每个地域完全独立,但同一个地域的可用区中间是互通的。

地域是指电力和网络互相独立的区域;同一可用区内实例之间的网络延迟更小;

关键点是电力和网络相互独立,这个是在灾备的时候要考虑的。

数据库、k8s的节点、消息队列等常用的资源都是需要做冗余的,如果在一个可用区内做大量的冗余,

看起来比较安全,一旦这个可用区废掉了,所有的冗余信息在短时间内是不可工作的,跨可用区做冗余可用性就会得到极大的增强。

pod在指定的node上运行。

正常工作的节点

这是正常工作的节点,pod通过kubelet这个进程被创建出来。

kubectl向apiserver发送了一个请求,apiserver就把请求信息存储在etcd数据库里,调度器通过事件的监听,通过调度算法来决定pod将会被调度到哪个节点上去,确定是哪个node之后,所以就在etcd的pod信息里面增加了一个nodeName。

kubelet也进行监听,当它发现调度器分配这个pod到某一个节点信息修改的时候,来看这个节点是不是属于它当前运行的node,如果是的话,就会创建这个pod。

k8s是go语言写的,一般用glog打日志,

k8s 基于glog fork出来一个klog,k8s内核是用klog来记录日志的。

glog有个参数:-v,表示日志的详细程度,

从日志中可以看到,在创建pod的时候,先判断pod是否存在,如果不存在的话,则创建。

有2种情况不属于正常工作的节点,

pod不能被调度到节点或者pod根本不可以在节点上运行,比如这个节点的systemd后台进程有问题导致节点不能正常运行,并不代表节点所在的虚拟机崩溃了,但是作为k8s节点是不能正常运行的,这种情况下node被打上一个污点。

NoScheduler表示不能调度到指定节点上;

NoExecute表示新的pod将不可以被调度到指定node上运行,当前在上面运行的pod也将被驱逐。

参数名称可以任意起,污点一旦被创建,对节点就生效了,ubuntu这个pod状态一直pending,就表示调度不过去 ,原因就是因为这个node被打上了污点。

解除污点,pod就被调度到这个node节点上启动了。

给node打上NoExecute污点,

这个节点上面的这个pod直接就停掉了,

去掉NoExecute污点,新的pod就可以在这个节点上运行了。

给node打污点的情况实际用的比较少,除非排错,比如pod还能在node上跑,不希望新的pod被调度过来,先打一个污点,再在上面排查问题。

如果要重启node或修改配置,一般通过拉警戒线的方式,

跟打污点的效果是一样的,

去除警戒线。

打污点或拉警戒线的使用场景:

场景1,比如阿里云systemd进程因版本的问题需要升级,会用这个命令,

场景2,节点有特殊的工作用途,比如master节点,一般至少用2个node做master节点,阿里云可以去托管master节点,比如当前的集群中只有worker节点没有master节点是因为被阿里云托管了,对于这种情况也需要给master node打上污点,不将pod调度到master node上去。

打污点key有两种形式,一种是以字符串label的方式,

另外一种比如env=prod,

表示节点是测试环境还是生产环境。

除非pod有env=prod并且可以容忍NoExecute这样的标签,才能被调度在这个pod上,

node亲和性

node1 16核64G内存,node2 16核64G内存,node3 32核 64G内存,让pod向性能比较好的node上运行即pod亲和node3,

pod亲和于什么样的node去运行,在调度的时候affinity是必须的,但在实际运行的时候又用不到,只是调度的时候用到。

在调度的时候80%的概率到一个node,20%的概率到另外一个node。

pod的亲和性

details pod运行在哪个node上,ubuntu pod也运行在details pod所在的node上,

实际运用的场景比如前后端的pod运行在同一个node上。启动pod的时候去查有没有满足app:details这个条件的pod,如果有的话,就在运行在这个pod所在的node上。

pod的反亲和性

每个node有不同的hostname,如果发现这个node上已经运行了跟我一样label的pod,那我就不在这个node上运行了,再找一个新的node即同样的一个pod不在同一个node上运行这样的效果,

65这个node上已经运行了ubuntu了,

再启动一个ubuntu,就不会在65这个node上运行了,而是在124这个node上运行,

再运行ubuntu3和ubuntu4,为什么ubuntu4一直pending是因为每个node上都有ubuntu了,4没有node可以运行了。

pod亲和度使用场景比较多,node亲和度几乎用不到,因为同一个集群,尽量使用同样的ecs虚拟机,尽量不要有差异化。

就算要区分环境,比如这2台配置比较小的机器做测试环境,(生产环境的机器要比测试环境多的多,这里只是做假设),更倾向于配成2个不同的vpc(私有云)

,每个vpc有自己独立的网段,2个vpc相对安全些,让2个网段互通可以使用阿里云的cen,

这样比较好,而不是做一个大的集群(里面什么样的node都有),再通过打污点、打标签,个人感觉这样会比较累。

k8s 调度做的事情很简单,就是为创建的 pod 找到合适的 node,找到后直接发送一个 v1.Binding 资源对象给 apiserver 。但整个过程是很复杂的。

k8s 调度模块本来就比较复杂的了,不仅逻辑复杂,还涉及到很多概念。

(本文源码引用的版本是 v1.23.14)

重要概念

先简单介绍一下重要的概念

调度框架

即 大名鼎鼎的 scheduler framework,是一个 interface。即类似于 cni, csi 一样定义接口,我们可以自己实现方便扩展。 仔细去看 type Framework interface 源码,其实就是一些列的 Plugins 调用方法。

源码里也只有一个实现实例 frameworkImpl

cache 本地化

即把 node 信息和 pod 信息缓存到本地了!为啥要这样,可以先不用知道。

记住node 信息 和 pod 信息 可以在本地获取,而且不用担心和 apiserver 数据不一致的问题!

Assume

乐观假设,也可以理解为模拟行为。 这是一种提高性能的设计,即调度需要的 node 和 pod 信息直接从 cache 中取,而没有从 apiserver 获取。

整个调度并没有真正发生,是通过本地 cache 的信息模拟地调度(pod.spec.NodeName = nodeName 就算调度成功)。

这么做的的目的是为了减少跟 apiserver 的交互(毕竟是一次消耗性能的I/O)。通过 Assume 这钟设计也就明白了 cache 本地化的必要性

调度队列

即 type SchedulingQueue interface 也是一个 抽象 interface ,实例也只有一个 PriorityQueue 。 其中的 activeQ、podBackoffQ、unschedulableQ 跟调度有关 。

新建的pod,以及待调度的 pod 都在 activeQ 中。

podBackoffQ即延迟调度队列,队列会有一个默认的延时时间。如果调度性能下降调度慢的话, pod 就会进入这里。这里再补充一个 backoff 的机制:

backoff机制是并发编程中常见的一种机制,即如果任务反复执行依旧失败,则会按次增长等待调度时间,降低重试效率,从而避免反复失败浪费调度资源

调度失败的 pod 都会进入 unschedulableQ 队列中。

这里我们知道在调度的时候,pod 都是从 activeQ 队列里面取的。

至于 podBackoffQ 和 unschedulableQ 中的 pod 如何到 activeQ 中这算是细节,后面会出调度细节的详细文章,这里可以不用关心,也不会影响对调度流程的理解。

Extension points

这些 points 才是扩展调度框架的关键。看下图

那条贯穿绿色和黄色模块的箭头,就是一个 pod 成功调度"要走的路"。

绿色模块叫调度周期。 主要有2个阶段,一个阶段叫 filter 用于过滤出符合要求的可调度 node 集合;另一个叫 score,选出最高分的 node 作为最终调度到的地方。

(可能你见到叫 Predicates 和 Priorities 两个阶段。 Predicates 是 filter 以前的叫法,Priorities 是 score 以前的叫法)

黄色模块叫绑定周期。

里面绿色、红色、黄色的箭标就叫 Extension points, 可以比作pod 调度路上一个一个的"检查点"!有些地方把Extension points就直接叫 plugins,也没问题因为每一个 "检查点" 都是由一个或多个 plugins 组合而成,这些 plugins 你可以想象成不同的"检查官"。 这点会在下文"调度流程"这一节中了解到。

KubeSchedulerProfile

KubeSchedulerProfile 是比较重要的概念,主要就是用于扩展自定义的调度框架,以及配合 KubeSchedulerConfiguration 配置涉及到的 plugins 。

但这里不了解这个东西不影响明白整个调度流程。

还是那句话,这篇文章重点是梳理调度流程。

调度流程

整个调度的源码入库是在 :

// cmd/kube-scheduler/app/server.go

func runCommand(cmd *cobra.Command, opts *options.Options, registryOptions ...Option) error {

...

...

cc, sched, err := Setup(ctx, opts, registryOptions...) // 初始化 scheduler

if err != nil {

return err

}

复制代码
 return Run(ctx, cc, sched) // 开始调度

}

开始调度前思考一个问题,如何拿到新创建的pod?

如果看过之前的 深入k8s -- Controller源码分析 ,肯定会有感觉,k8s 所有资源的监听都是通过 informer。

这里也是一样的。在 scheduler.New() 中的 addAllEventHandlers() ,就可以到相关代码。里面不仅有 pod,还有 node 等其他事件。

好了,调度流程正文开始了!

sched.SchedulingQueue.Run() 将其他队列的pod 加到 activeQ 中。

真正执行调度逻辑的地方 sched.scheduleOne, 咱们就直接看关键源码吧,一些不太重要的就省略了,比如 metric 相关的:

func (sched *Scheduler) scheduleOne(ctx context.Context) {

// 从 activeQ 中 pop 出 一个 pod

// 到底是pop 最新 pod,还是最老pod呢?就留给读者们了。

podInfo := sched.NextPod()

...

// 根据 pod.Spec.SchedulerName 拿到对应的调度框架实例(这里是 default-scheduler)

fwk, err := sched.frameworkForPod(pod)

...

...

// filter 和 score 后 返回最优节点

scheduleResult, err := sched.Algorithm.Schedule(schedulingCycleCtx, sched.Extenders, fwk, state, pod)

if err != nil {

var nominatingInfo *framework.NominatingInfo

reason := v1.PodReasonUnschedulable

if fitError, ok := err.(*framework.FitError); ok { // 调度失败

if !fwk.HasPostFilterPlugins() { // 默认调度框架是会有 postFilterPlugins的

klog.V(3).InfoS("No PostFilter plugins are registered, so no preemption will be performed")

} else {

// 这里面会执行抢占逻辑,什么是抢占呢?

// 相当于给 pod "开后门", 即如果 pod 的优先值比较高,删除一个比他优先级低的 pod(victim)。

// 然后返回 pod 抢占到的节点

result, status := fwk.RunPostFilterPlugins(ctx, state, pod, fitError.Diagnosis.NodeToStatusMap)

...

...

// 构建提名信息

if result != nil {

nominatingInfo = result.NominatingInfo

}

...

...

// 这里面就是执行调度失败的逻辑, 会把 抢占者pod 加入到 unschedulableQ 队列中, 就可以直接返回等待下一次调度

sched.recordSchedulingFailure(fwk, podInfo, err, reason, nominatingInfo)

return

}

}

}

...

.

// 这就是我们说的 乐观假设,通过本地化数据模拟调度成功。最终结果就是 pod.Spec.NodeName = SuggestedHost

// 经过 filter 和 score 就开始模拟调度了

err = sched.assume(assumedPod, scheduleResult.SuggestedHost)

if err != nil {

...

// 这里就没有抢占,重新调度。

// 看到 "clearNominatedNode"没,跟上面不一样,这里是直接删除提名信息

sched.recordSchedulingFailure(fwk, assumedPodInfo, err, SchedulerError, clearNominatedNode)

return

}

复制代码
// 即有时候会为 pod 预留一些资源比如 pvc(在 PreFilter 中也只会调用VolumeBinding,这时候就预留了pvc与 pv 的绑定逻辑, 放在上下文 CycleState 中的) 
// default-scheduler 底层这处只有一个 VolumeBinding 插件。所以这里也是操作本地缓存--将 pvc 与 pv 的绑定。
if sts := fwk.RunReservePluginsReserve(schedulingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost); !sts.IsSuccess() {
   ...
   ...
   sched.recordSchedulingFailure(fwk, assumedPodInfo, sts.AsError(), SchedulerError, clearNominatedNode)
   return
}

// 这里会遇到"检查点" Permit。
// 有点尴尬的是 default-scheduler 里面没有实现 Permit插件。 所以可以不用管这块逻辑 所以注释掉
// runPermitStatus := fwk.RunPermitPlugins(schedulingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)

// 最后就是通过异步协程的方式,向 apiserver 发送 pod 与 node 的绑定请求

go func() {

// 遇到绑定周期的 PreBind。

// 你看源码会发现跟调度周期中"检查点" Reserve 配置的插件是一样的,都是调用 VolumeBinding 插件实例。

// 就是做的 pvc 与 pv 的绑定

preBindStatus := fwk.RunPreBindPlugins(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)

...

...

// 遇到 Bind "检查点"。

// 其实底层非常简单,就是给 apiserver 发送 v1.Binding 资源类型来完成真正的绑定。

sched.bind(bindingCycleCtx, fwk, assumedPod, scheduleResult.SuggestedHost, state)

...

...

// 最后会"检查点" PostBind。

// 但default-scheduler 里面也没有实现 PostBind插件。 所以也可以不用管这块逻辑

// fwk.RunPostBindPlugins(bindingCycleCtx, state, assumedPod, scheduleResult.SuggestedHost)

}()

}

到这,一个调度周期的流程就讲完了, 总结一下流程如下:

拿到 pod

执行 filter 和 score 两大类 extend points ("检查点")

这里如果执行失败,会发生抢占

assume 模拟调度

执行 Reserve extend point (VolumeBinding plugins), 即在缓存中预绑定 pvc 与 pv。到这里绑定周期走完。

最后就是绑定周期

PreBind extend point (VolumeBinding plugins):向 apiserver 发送 pvc 与 pv 的绑定

Bind extend point 向 apiserver 发送 pod 与 node 的绑定

"检查点"

所谓"检查点"就是 extend points,上文也有介绍。

虽然 k8s 官方给出的是 Plugins 的概念,即调度策略可扩展的地方。

但个人更愿意把他们比作调度路上的"检查点",也确实像:每个"检查点"会有"多个不同的检察官"刁难,只要有一个地方返回 error, 整个调度就失败。

你可以在固定的点位(上文绿红黄箭标的地方),放置多个对应的"检察官"(plugins)。

我们来看看 "default-scheduler",默认放置了哪些:

// pkg/scheduler/apis/config/v1beta2/default_plugins.go

func getDefaultPlugins() *v1beta2.Plugins {

plugins := &v1beta2.Plugins{

QueueSort: v1beta2.PluginSet{

Enabled: []v1beta2.Plugin{

{Name: names.PrioritySort},

},

},

PreFilter: v1beta2.PluginSet{

Enabled: []v1beta2.Plugin{

{Name: names.NodeResourcesFit},

{Name: names.NodePorts},

{Name: names.VolumeRestrictions},

{Name: names.PodTopologySpread},

{Name: names.InterPodAffinity},

{Name: names.VolumeBinding},

{Name: names.NodeAffinity},

},

},

Filter: v1beta2.PluginSet{

Enabled: []v1beta2.Plugin{

{Name: names.NodeUnschedulable},

{Name: names.NodeName},

{Name: names.TaintToleration},

{Name: names.NodeAffinity},

{Name: names.NodePorts},

{Name: names.NodeResourcesFit},

{Name: names.VolumeRestrictions},

{Name: names.EBSLimits},

{Name: names.GCEPDLimits},

{Name: names.NodeVolumeLimits},

{Name: names.AzureDiskLimits},

{Name: names.VolumeBinding},

{Name: names.VolumeZone},

{Name: names.PodTopologySpread},

{Name: names.InterPodAffinity},

},

},

PostFilter: v1beta2.PluginSet{

Enabled: []v1beta2.Plugin{

{Name: names.DefaultPreemption},

},

},

PreScore: v1beta2.PluginSet{

Enabled: []v1beta2.Plugin{

{Name: names.InterPodAffinity},

{Name: names.PodTopologySpread},

{Name: names.TaintToleration},

{Name: names.NodeAffinity},

},

},

Score: v1beta2.PluginSet{

Enabled: []v1beta2.Plugin{

{Name: names.NodeResourcesBalancedAllocation, Weight: pointer.Int32Ptr(1)},

{Name: names.ImageLocality, Weight: pointer.Int32Ptr(1)},

{Name: names.InterPodAffinity, Weight: pointer.Int32Ptr(1)},

{Name: names.NodeResourcesFit, Weight: pointer.Int32Ptr(1)},

{Name: names.NodeAffinity, Weight: pointer.Int32Ptr(1)},

// Weight is doubled because:

// - This is a score coming from user preference.

// - It makes its signal comparable to NodeResourcesFit.LeastAllocated.

{Name: names.PodTopologySpread, Weight: pointer.Int32Ptr(2)},

{Name: names.TaintToleration, Weight: pointer.Int32Ptr(1)},

},

},

Reserve: v1beta2.PluginSet{

Enabled: []v1beta2.Plugin{

{Name: names.VolumeBinding},

},

},

PreBind: v1beta2.PluginSet{

Enabled: []v1beta2.Plugin{

{Name: names.VolumeBinding},

},

},

Bind: v1beta2.PluginSet{

Enabled: []v1beta2.Plugin{

{Name: names.DefaultBinder},

},

},

}

applyFeatureGates(plugins)

复制代码
return plugins

}

看到这,大家应该就很清晰每个扩展点(检查点)里具体的有哪些插件(检察官)。 而且也看到了, 这版里面调度周期并没有 "Normalize Score" 、 "Permit"; 绑定周期没有 "WaitOnPermit"、"PostBind" 这些 "检查点",也就是说假如我们自己实现调度框架,可以跳过一些检查点。

"开后门"

所谓"开后门",就是如果 pod 有设置优先级,调度器会根据算法选出一个 node,将优先级比待调度pod 低的 pod(victim)都 "优雅"删除,然后设置抢占 pod pod.status.NominatedNodeName = 选出的 node 。

概念的详情可以看官网的说明

这里我们来看看源码逻辑,发生抢占还是在 scheduleOne 中,调度流程一节中有提到:

func (sched *Scheduler) scheduleOne(ctx context.Context) {

...

...

// 执行完filter 和 score 逻辑

scheduleResult, err := sched.Algorithm.Schedule(schedulingCycleCtx, sched.Extenders, fwk, state, pod)

if err != nil {

// 提名信息

var nominatingInfo *framework.NominatingInfo

reason := v1.PodReasonUnschedulable

if fitError, ok := err.(*framework.FitError); ok {

...

result, status := fwk.RunPostFilterPlugins(ctx, state, pod, fitError.Diagnosis.NodeToStatusMap)

if result != nil {

nominatingInfo = result.NominatingInfo

}

}

...

...

// 调度失败方法

sched.recordSchedulingFailure(fwk, podInfo, err, reason, nominatingInfo)

return

}

...

}

可以看到在执行 filter 和 score 的所有"检查点"后, 如果没有找到合适的 node,就要去执行抢占,即调用 PostFilterPlugins,也就是在 "Extension points" 一节中出现的红色箭标。

我们通过跳转来到抢占的源码逻辑,这里我们还是只关注流程:

func (ev *Evaluator) Preempt(ctx context.Context, pod *v1.Pod, m framework.NodeToStatusMap) (*framework.PostFilterResult, *framework.Status) {

...

...

// 1. 验证是否需要发生抢占,如果有 pod 已经处于删除中,就可以不用发生抢占

if !ev.PodEligibleToPreemptOthers(pod, m[pod.Status.NominatedNodeName]) {

return nil, framework.NewStatus(framework.Unschedulable)

}

复制代码
// 2. 找到所有可以抢占的 node 以及他上面的可以被抢占的pod(我们统一将这类 pod 称为 victim)
candidates, nodeToStatusMap, err := ev.findCandidates(ctx, pod, m)
if err != nil && len(candidates) == 0 {
   return nil, framework.AsStatus(err)
}

// 3. 通过 extenders 在过滤一次
candidates, status := ev.callExtenders(pod, candidates)

// 4. 找到最合适的
bestCandidate := ev.SelectCandidate(candidates)
if bestCandidate == nil || len(bestCandidate.Name()) == 0 {
    return nil, framework.NewStatus(framework.Unschedulable)
}

// 5. 删除 victim, 再清除节点上比抢占 pod 优先级低的pod的提名(pod.Status.NominatedNodeName = "")
if status := ev.prepareCandidate(bestCandidate, pod, ev.PluginName); !status.IsSuccess() {
    return nil, status
}

}

其实整体逻辑是比较清晰简单了。

然后我们再回到 scheduleOne 中 的 sched.recordSchedulingFailure(fwk, podInfo, err, reason, nominatingInfo) 方法中:

func (sched *Scheduler) recordSchedulingFailure(fwk framework.Framework, podInfo *framework.QueuedPodInfo, err error, reason string, nominatingInfo *framework.NominatingInfo) {

// 通过这里将被抢占的 pod 添加到 unschedulableQ 中。

// (不要被他 Error 迷惑了, 以为是个错误日志 -_-!!!)

sched.Error(podInfo, err)

复制代码
if sched.SchedulingQueue != nil {
    // 增加提名信息
    sched.SchedulingQueue.AddNominatedPod(podInfo.PodInfo, nominatingInfo)
} 
...
...
// 更新 pod status 中的提名: pod.Status.NominatedNodeName = nominatingInfo.NominatedNodeName
if err := updatePod(sched.client, pod, &v1.PodCondition{
      Type:    v1.PodScheduled,
  Status:  v1.ConditionFalse,
  Reason:  reason,
  Message: err.Error(),
}, nominatingInfo); err != nil {
      klog.ErrorS(err, "Error updating pod", "pod", klog.KObj(pod))
}

}

我们前面说过 unschedulableQ 中的 pod 会被转移到 activeQ 中重新调度。

所以到这里抢占 pod 的调度就结束了。

总结

一个 pod 的调度流程总结起来还是比较简单:

就是走完"调度周期" 和 "绑定周期"的Extends Points,整个调度周期是在本地模拟的。同时如果调度失败,可能会发生抢占其他pod的可能。这里总结成3点:

调度前通过本地缓存的 node 和 pod 信息,如果有 pvc 还会涉及到 pvc 和 pv 的缓存信息,来模拟调度

完整的调度分为 "调度周期" 以及 "绑定周期",每个周期中都有各自的 Extends Points(检查点)。我们可以硬编码的方式,添加自己编写的 Extends Points Plugins(我们前面说的检察官),来扩展调度功能。

在某节点抢占优先级低的 pod

k8s里面Pod是最小的原子调度单位。所有跟调度和资源管理相关的属性都是属于Pod对象的字段。其中比较重要的是Pod的CPU和内存配置。

可压缩资源:CPU,它不足的时候,Pod只会"饥饿",不会退出。

不可压缩资源:内存,它不足的时候,就会OOM。

因为Pod可以由多个Container组成,CPU和内存资源的限制要配置在每个Container的定义上。特别的,CPU的配额还可以是分数,比如0.5个CPU,分配一半的算力。

k8s里面Pod的CPU和内存资源,分为limits和requests两种情况,在调度的时候,kube-scheduler只会按照requests的值进行计算;而在真正设置Cgroups限制时,kubelet会按照limits来设置。

k8s对CPU和内存资源限额的设计,参考了Borg论文中对"动态资源边界"的定义,调度系统不是必须严格遵循容器化作业在提交时设置的资源边界,因为实际场景中,大多数作业用到的资源其实远少于它所请求的资源限额。

k8s里面的QoS模型:不同的limits和requests设置方式会将Pod划分到不同的QoS级别。

Guaranteed:同时设置了limits和requests;或者仅设置了limits

Burstable:至少一个Container设置了requests

BestEffort:既没有设置limits,也没有设置requests

当宿主机资源紧张的时候,kubelet对Pod进行Eviction(资源回收),会具体挑选哪些Pod进行删除,顺序是:1. BestEffort; 2. Burstable; 3. Guaranteed。

对于同QoS类别的Pod,k8s还会根据Pod的优先级来进一步地排序和选择。

默认调度器

默认调度器的主要职责是为新创建出来的Pod寻找一个最合适的节点:

从集群所有的节点中根据调度算法选出所有可以运行该Pod的节点;

从第一步的结果中,再根据调度算法挑选一个最符合条件的节点作为最终结果。

首先,会有一组叫作Predicate的调度算法来检查每个节点;然后,再调用一组叫作Priority的调度算法,来给上一步得到的结果里的每个节点打分,得分最高的那个节点就是最终的调度结果。

调度器对一个Pod调度成功,实际上就是将它的spec.nodeName字段填上调度结果的节点名字。

默认情况下,k8s的调度队列是一个PriorityQueue(优先级队列),并且当集群的某些信息发生变化时,调度器还会对调度队列里的内容进行一些特殊操作,主要出于调度优先级和抢占的考虑

k8s的默认调度器还有负责更新调度器缓存,调度部分优化性能的一个根本原则,就是尽可能地将集群信息缓存化,以便从根本上提高 Predicate 和 Priority 调度算法的执行效率。

k8s中默认调度器的可扩展机制叫作Scheduler Framework他的主要目的是在调度器生命周期的各个关键点上,向用户暴露可以进行扩展和实现的接口,从而赋予用户自定义调度器的能力。

比如,

可以提供一个自己的调度队列的实现,从而控制每个Pod开始被调度(出队)的时机;

可以提供自己的过滤算法实现,根据自己的需求选择机器。

可插拔式逻辑都是标准的Go语言插件机制,需要在编译的时候选择把哪些插件编译进去。

默认调度器策略解析

预选策略

预选在调度过程中的作用可以理解为Filter,即它按照调度策略从当前集群的所有节点中"过滤"出一系列符合条件的节点。这些节点都是可以运行待调度Pod的宿主机。

在k8s中,默认的调度策略有如下几种:

最基础的过滤规则:GeneralPredicates,比如计算宿主机的CPU和内存资源是否够用。

和Volume相关的过滤规则:MaxPDVolumeCountPredicate、VolumeZonePredicate、VolumeBindingPredicate。这一组过滤规则和容器PV相关。

和宿主机相关的过滤规则:PodToleratesNodeTaints、NodeMemoryPressurePredicate。这一组过滤规则主要考察待调度Pod是否满足节点本身的某些条件。

Pod相关的过滤规则:PodAffinityPredicate,pod和pod之间的亲密和反亲密的关系。

默认调度器的优先级和抢占机制

优先级和抢占机制解决的事Pod调度失败时该怎么办的问题。正常来说,当一个Pod调度失败之后,它会被暂时"搁置",直到Pod被更新或者集群状态发生变化,调度器才会对这个Pod进行重新调度。

优先级

当我们希望一个高优先级的Pod调度失败后,该Pod不会被搁置,而是挤走某个节点上一些低优先级的Pod,以此保证这个高优先级Pod调度成功。

抢占

当一个高优先级的Pod调度失败时,调度器的抢占能力就会被触发。此时,调度器就会试图从当前集群里寻找一个节点:当该节点上的一个或者多个低优先级Pod被删除后,待调度的高优先级Pod可以被调度到该节点上。

GPU管理和Device Plugin机制

目前只支持基本的需求(GPU申请的数量),对异构的GPU集群支持的不好。比如我的Pod想在计算能力最强的那个GPU上运行,Device Plugin就完全不能处理。

相关推荐
周小码2 小时前
Go开发的自行托管代理加速服务:支持Docker与GitHub加速
docker·golang·github
川石课堂软件测试2 小时前
Oracle 数据库使用事务确保数据的安全
数据库·python·功能测试·docker·oracle·单元测试·prometheus
奋斗的老史3 小时前
25年Docker镜像无法下载的四种对策
docker·容器·eureka
chillxiaohan3 小时前
Docker学习记录
学习·docker·容器
柯南二号3 小时前
【后端】Docker 常用命令详解
服务器·nginx·docker·容器
新鲜萝卜皮4 小时前
容器内运行的进程,在宿主机的top命令中可以显示吗?
容器
我今天指定是不行了6 小时前
Docker安装与部分应用安装
docker
容器魔方6 小时前
Karmada v1.15 版本发布!多模板工作负载资源感知能力增强
云原生·容器·云计算
streaker3036 小时前
Docker + Jenkins + Nginx 实现前端自动化构建与静态资源发布(含一键初始化脚本)
docker·jenkins
THMAIL7 小时前
机器学习从入门到精通 - Python环境搭建与Jupyter魔法:机器学习起航必备
linux·人工智能·python·算法·机器学习·docker·逻辑回归