当前位置: 首页 > news >正文

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就完全不能处理。

http://www.dtcms.com/a/319558.html

相关文章:

  • USB3.0 枚举流程
  • 前端页面直接生成PDF下载文件
  • Python实现点云随机一致性(RANSAC)配准——粗配准
  • 软件测试中,pytest 运行完成后,如何自动发送邮件?
  • vscode 打开设置
  • OpenCV 入门实战:从环境配置到图像 / 视频处理
  • Java 八大经典排序算法全解析
  • Redis持久化存储
  • 2025 年华数杯赛题浅析-助攻快速选题
  • Centos6停止服务后yum改用阿里云
  • Syzkaller实战教程10: MoonShine复现Trace2syz功能演示
  • 手动开发一个TCP服务器调试工具(三):使用 QWidget 构建 TCP 服务控制界面
  • 强化学习详解:从理论到前沿的全面解析
  • 【Redis面试精讲 Day 15】Redis分布式锁实现与挑战
  • C++ 类和对象(2)
  • Kubernetes学习
  • 安卓开发:网络状态监听封装的奥秘
  • 根据浏览器语言判断wordpress访问不同语言的站点
  • 计算机视觉前言-----OpenCV库介绍与计算机视觉入门准备
  • Python 偏函数(functools.partial)详解
  • MySQL ORDER BY 语句详细说明
  • SVG组件
  • 96-基于Flask的酷狗音乐数据可视化分析系统
  • 微信小程序常见功能实现
  • OpenCV 入门教程:开启计算机视觉之旅
  • uwsgi 启动 django 服务
  • Next.js 15 重磅发布:React 19 集成 + 性能革命,开发者必看新特性指南
  • CentOS 7 安装 Anaconda
  • 秋招笔记-8.7
  • Redis的三种特殊类型