kubernetes调度器

scheduling framework extensions

目录

文章目录

实验环境

实验环境:
1、win10,vmwrokstation虚机;
2、k8s集群:3台centos7.6 1810虚机,1个master节点,2个node节点
   k8s version:v1.22.2
   containerd://1.5.5

实验软件

链接:https://pan.baidu.com/s/1P3Z_ujk22dYDXzM37WI5FA?pwd=v01w
提取码:v01w

2022.2.18-39.亲合性调度-实验代码.zip

链接:https://pan.baidu.com/s/1Nbkwz0rvDris8F6qFegJtw?pwd=4dx5
提取码:4dx5
2022.2.20-42.服务质量-实验代码

链接:https://pan.baidu.com/s/1CAC9j5yU-sg-aDfLPYVflg?pwd=b3jp
提取码:b3jp
2022.2.19-40.拓扑分布约束-实验代码

链接:https://pan.baidu.com/s/19x43qNOI2oogXTcgEDCwgQ?pwd=0min
提取码:0min
2022.2.19-41.Descheduler-s实验代码

本节实践

  1. 实践:nodeSelector测试(测试成功)-2022.5.16
  2. 实践:节点亲和性测试(测试成功)-2022.5.16
  3. 实践:pod亲和性(测试成功)-2022.5.16
  4. 实践:pod反亲和性(测试成功)-2022.5.16
  5. 实践:污点与容忍(测试成功)-2022.5.16
  6. 实践:nodeName测试(测试成功)-2022.5.16
  7. 实践:resources.requests测试(测试成功)-2022.5.17
  8. 实践:resources.limits测试(测试成功)-2022.5.17
  9. 实践:Qos解析(测试成功)-2022.5.17
  10. 实践:用拓扑分布约束来扩展daemonset功能(测试成功)-2022.5.17
  11. 实践:descheduler安装(测试成功)-2022.5.17

前置知识


存储:(容器:可写层 & 挂载外部存储)
扩展资源:gpu等;

调度器

kube-scheduler 是 kubernetes 的核心组件之一,主要负责整个集群资源的调度功能,根据特定的调度算法和策略,将 Pod 调度到最优的工作节点上面去,从而更加合理、更加充分的利用集群的资源,这也是我们选择使用 kubernetes 一个非常重要的理由。如果一门新的技术不能帮助企业节约成本、提供效率,我相信是很难推进的。

🍂 什么是调度?

调度,可以说是Kubernetes最核心的功能之一了。

Pod是Kubernetes中最小的调度单元,而Pod又是运行在Node之上的。所谓调度,简单来说就是为一个新创建出来的 Pod,寻找一个最适合它运行的Node。

🍂 调度概览

🍂 调度器分类


1.单体调度器:对于大规模批量调度诉求场景,不能胜任!(基于pod的事件调度)!
2.两层调度器:应用平台–hadoop,spark;资源调度器(负责底层计算资源的管理),应用调度器;resource offers,存在的问题:1.资源争抢如何解决?2.分配资源不合理如何处理 解决办法:悲观锁 先锁定资源,再进行资源的腾挪处理。–>效率不高
3.状态共享调度器:基于版本控制/事务控制的基于乐观锁的调度! full state,本地缓存,回写,冲突判断,重试。

1、调度流程

1.默认调度器

kube-scheduler

kube-scheduler负责分配调度Pod 到集群内的节点上,它监听kube-apiserver,查询还未分配Node的Pod,然后根据调度策略为这些Pod分配节点(更新Pod 的NodeName字段)。(生产者-消费者模型,plugin)

默认情况下,kube-scheduler 提供的默认调度器能够满足我们绝大多数的要求,我们前面和大家接触的示例也基本上用的默认的策略,都可以保证我们的 Pod 可以被分配到资源充足的节点上运行。但是在实际的线上项目中,可能我们自己会比 kubernetes 更加了解我们自己的应用,比如我们希望一个 Pod 只能运行在特定的几个节点上,或者这几个节点只能用来运行特定类型的应用,这就需要我们的调度器能够可控

例如:随便导出一个pod的yaml文件,可以看到其默认调度器就是`default-scheduler`.
$ kubectl get po apisix-etcd-0 -napisix -oyaml
……
schedulerName: default-scheduler
……

#也就是k8s集群默认使用的:
$ kubectl get po -A
……
kube-system            kube-scheduler-master1                             1/1     Running    3 (27d ago)   108d

kube-scheduler 的主要作用就是根据特定的调度算法和调度策略将 Pod 调度到合适的 Node 节点上去,是一个独立的二进制程序启动之后会一直监听 API Server,获取到 PodSpec.NodeName 为空的 Pod,对每个 Pod 都会创建一个 binding。

kube-scheduler structrue

这个过程在我们看来好像比较简单,但在实际的生产环境中,需要考虑的问题就有很多了:

  • 如何保证全部的节点调度的公平性?要知道并不是所有节点资源配置一定都是一样的
  • 如何保证每个节点都能被分配资源?
  • 集群资源如何能够被高效利用?
  • 集群资源如何才能被最大化使用?
  • 如何保证 Pod 调度的性能和效率?(假设说有1w个节点,我是否可以在其中1k个节点上进行筛选呢,这样就可以大幅度提高调度效率了😀)
  • 用户是否可以根据自己的实际需求定制自己的调度策略?

🍂 调度主要分为以下几个部分:

  • 首先是预选过程,过滤掉不满足条件的节点,这个过程称为 Predicates(过滤)
  • 然后是优选过程,对通过的节点按照优先级排序,称之为 Priorities(打分)
  • 最后从中选择优先级最高的节点,如果中间任何一步骤有错误,就直接返回错误

Predicates 阶段首先遍历全部节点,过滤掉不满足条件的节点,属于强制性规则,这一阶段输出的所有满足要求的节点将被记录并作为第二阶段的输入,如果所有的节点都不满足条件,那么 Pod 将会一直处于 Pending 状态,直到有节点满足条件,在这期间调度器会不断的重试。

所以我们在部署应用的时候,如果发现有 Pod 一直处于 Pending 状态,那么就是没有满足调度条件的节点,这个时候可以去检查下节点资源是否可用。

Priorities 阶段即再次对节点进行筛选,如果有多个节点都满足条件的话,那么系统会按照节点的优先级(priorites)大小对节点进行排序,最后选择优先级最高的节点来部署 Pod 应用。

01、如果你的pod处于pending状态,那么一定就是调度器出现了问题,那么原因会很多,有可能是你的node资源不足,有可能是你的节点已经被占用了……(因此需要使用kubectl describle pod xxx来查看原因)

02、所谓的bing操作就是如下:
$ kubectl get po apisix-etcd-0 -napisix -oyaml
……
nodeName: node2 #将pod的配置清单的nodeName字段补充完成。
……

🍂 下面是调度过程的简单示意图:

kube-scheduler filter

更详细的流程是这样的:

  • 首先,客户端通过 API Server 的 REST API 或者 kubectl 工具创建 Pod 资源

  • API Server 收到用户请求后,存储相关数据到 etcd 数据库中

  • 调度器监听 API Server 查看到还未被调度(bind)的 Pod 列表,循环遍历地为每个 Pod 尝试分配节点,这个分配过程就是我们上面提到的两个阶段:

    • 预选阶段(Predicates),过滤节点,调度器用一组规则过滤掉不符合要求的 Node 节点,比如 Pod 设置了资源的 request,那么可用资源比 Pod 需要的资源少的主机显然就会被过滤掉。
    • 优选阶段(Priorities),为节点的优先级打分,将上一阶段过滤出来的 Node 列表进行打分,调度器会考虑一些整体的优化策略,比如把 Deployment 控制的多个 Pod 副本尽量分布到不同的主机上,使用最低负载的主机等等策略。
  • 经过上面的阶段过滤后选择打分最高的 Node 节点和 Pod 进行 binding 操作,然后将结果存储到 etcd 中, 最后被选择出来的 Node 节点对应的 kubelet 去执行创建 Pod 的相关操作(当然也是 watch APIServer 发现的)。

🍂 Predicates plugin工作原理


链式过滤器

🍂 调度插件


LeastAllocated:空闲资源多的分高 --使的node上的负载比较合理一点!
MostAllocated:空闲资源少的分高 – 可以退回Node资源!

🍀 最简单的Scheduler实现


k8s 0.2版本的一个架构图,基本调度方式就是随机,轮询会稍微复杂一些;

随机

2.扩展调度器(extender)

包括很多时候,默认的调度器已经不能满足业务需求,需要对它做自定义的扩展和实现,具体该怎么做,背后的原理又是什么样的,业界有没有优秀案例可供参考,这些都是非常令人头疼的问题。(extender本身就是一个拉低性能的因素。)

🍂 考虑到实际环境中的各种复杂情况,kubernetes 的调度器采用插件化的形式实现,可以方便用户进行定制或者二次开发,我们可以自定义一个调度器并以插件形式和 kubernetes 进行集成。

开发人员注意即可:

kubernetes 调度器的源码位于 kubernetes/pkg/scheduler 中,其中 Scheduler 创建和运行的核心程序,对应的代码在 pkg/scheduler/scheduler.go,如果要查看 kube-scheduler 的入口程序,对应的代码在 cmd/kube-scheduler/scheduler.go

https://github1s.com/kubernetes/kubernetes/tree/v1.22.5

image-20220216165230058

image-20220216165341841

3.调度框架

基于Scheduler Framework实现扩展

未来主流的方法。

🍂 目前调度器已经全部通过插件的方式实现了调度框架,默认开启的调度插件如以下代码所示:

// pkg/scheduler/algorithmprovider/registry.go

func getDefaultConfig() *schedulerapi.Plugins {
    return &schedulerapi.Plugins{
        QueueSort: &schedulerapi.PluginSet{
            Enabled: []schedulerapi.Plugin{
                {Name: queuesort.Name},
            },
        },
        PreFilter: &schedulerapi.PluginSet{
            Enabled: []schedulerapi.Plugin{
                {Name: noderesources.FitName},
                {Name: nodeports.Name},
                {Name: podtopologyspread.Name},
                {Name: interpodaffinity.Name},
                {Name: volumebinding.Name},
            },
        },
        Filter: &schedulerapi.PluginSet{
            Enabled: []schedulerapi.Plugin{
                {Name: nodeunschedulable.Name},
                {Name: noderesources.FitName},
                {Name: nodename.Name},
                {Name: nodeports.Name},
                {Name: nodeaffinity.Name},
                {Name: volumerestrictions.Name},
                {Name: tainttoleration.Name},
                {Name: nodevolumelimits.EBSName},
                {Name: nodevolumelimits.GCEPDName},
                {Name: nodevolumelimits.CSIName},
                {Name: nodevolumelimits.AzureDiskName},
                {Name: volumebinding.Name},
                {Name: volumezone.Name},
                {Name: podtopologyspread.Name},
                {Name: interpodaffinity.Name},
            },
        },
        PostFilter: &schedulerapi.PluginSet{
            Enabled: []schedulerapi.Plugin{
                {Name: defaultpreemption.Name},
            },
        },
        PreScore: &schedulerapi.PluginSet{ #打分
            Enabled: []schedulerapi.Plugin{
                {Name: interpodaffinity.Name},
                {Name: podtopologyspread.Name},
                {Name: tainttoleration.Name},
            },
        },
        Score: &schedulerapi.PluginSet{
            Enabled: []schedulerapi.Plugin{
                {Name: noderesources.BalancedAllocationName, Weight: 1},
                {Name: imagelocality.Name, Weight: 1},
                {Name: interpodaffinity.Name, Weight: 1},
                {Name: noderesources.LeastAllocatedName, Weight: 1},
                {Name: nodeaffinity.Name, Weight: 1},
                {Name: nodepreferavoidpods.Name, Weight: 10000},
                // Weight is doubled because:
                // - This is a score coming from user preference.
                // - It makes its signal comparable to NodeResourcesLeastAllocated.
                {Name: podtopologyspread.Name, Weight: 2},
                {Name: tainttoleration.Name, Weight: 1},
            },
        },
        Reserve: &schedulerapi.PluginSet{
            Enabled: []schedulerapi.Plugin{
                {Name: volumebinding.Name},
            },
        },
        PreBind: &schedulerapi.PluginSet{
            Enabled: []schedulerapi.Plugin{
                {Name: volumebinding.Name},
            },
        },
        Bind: &schedulerapi.PluginSet{
            Enabled: []schedulerapi.Plugin{
                {Name: defaultbinder.Name},
            },
        },
    }
}

从上面我们可以看出调度器的一系列算法由各种插件在调度的不同阶段来完成,下面我们就先来了解下调度框架。

调度框架定义了一组扩展点,用户可以实现扩展点定义的接口来定义自己的调度逻辑(我们称之为扩展),并将扩展注册到扩展点上,调度框架在执行调度工作流时,遇到对应的扩展点时,将调用用户注册的扩展。调度框架在预留扩展点时,都是有特定的目的,有些扩展点上的扩展可以改变调度程序的决策方法,有些扩展点上的扩展只是发送一个通知。

我们知道每当调度一个 Pod 时,都会按照两个过程来执行:调度过程和绑定过程

调度过程为 Pod 选择一个合适的节点,绑定过程则将调度过程的决策应用到集群中(也就是在被选定的节点上运行 Pod),将调度过程和绑定过程合在一起,称之为调度上下文(scheduling context)。需要注意的是**调度过程是同步运行的(同一时间点只为一个 Pod 进行调度)**,绑定过程可异步运行(同一时间点可并发为多个 Pod 执行绑定)。

调度过程和绑定过程遇到如下情况时会中途退出:

  • 调度程序认为当前没有该 Pod 的可选节点
  • 内部错误

这个时候,该 Pod 将被放回到 待调度队列,并等待下次重试。

1.扩展点(Extension Points)

下图展示了调度框架中的调度上下文及其中的扩展点,一个扩展可以注册多个扩展点,以便可以执行更复杂的有状态的任务。

scheduling framework extensions

调度阶段:
1.predicate(预选):
2.priority/score(优选):
绑定阶段:

🍂 详细过程:

  1. QueueSort 扩展用于对 Pod 的待调度队列进行排序,以决定先调度哪个 Pod,QueueSort 扩展本质上只需要实现一个方法 Less(Pod1, Pod2) 用于比较两个 Pod 谁更优先获得调度即可,同一时间点只能有一个 QueueSort 插件生效。
  2. Pre-filter 扩展用于对 Pod 的信息进行预处理,或者检查一些集群或 Pod 必须满足的前提条件,如果 pre-filter 返回了 error,则调度过程终止。
  3. Filter 扩展用于排除那些不能运行该 Pod 的节点,对于每一个节点,调度器将按顺序执行 filter 扩展;如果任何一个 filter 将节点标记为不可选,则余下的 filter 扩展将不会被执行。调度器可以同时对多个节点执行 filter 扩展。
  4. Post-filter 是一个通知类型的扩展点,调用该扩展的参数是 filter 阶段结束后被筛选为可选节点的节点列表,可以在扩展中使用这些信息更新内部状态,或者产生日志或 metrics 信息。
  5. Scoring 扩展用于为所有可选节点进行打分,调度器将针对每一个节点调用 Soring 扩展,评分结果是一个范围内的整数。在 normalize scoring 阶段,调度器将会把每个 scoring 扩展对具体某个节点的评分结果和该扩展的权重合并起来,作为最终评分结果。
  6. Normalize scoring 扩展在调度器对节点进行最终排序之前修改每个节点的评分结果,注册到该扩展点的扩展在被调用时,将获得同一个插件中的 scoring 扩展的评分结果作为参数,调度框架每执行一次调度,都将调用所有插件中的一个 normalize scoring 扩展一次。
  7. Reserve 是一个通知性质的扩展点,有状态的插件可以使用该扩展点来获得节点上为 Pod 预留的资源,该事件发生在调度器将 Pod 绑定到节点之前,目的是避免调度器在等待 Pod 与节点绑定的过程中调度新的 Pod 到节点上时,发生实际使用资源超出可用资源的情况(因为绑定 Pod 到节点上是异步发生的)。这是调度过程的最后一个步骤,Pod 进入 reserved 状态以后,要么在绑定失败时触发 Unreserve 扩展,要么在绑定成功时,由 Post-bind 扩展结束绑定过程。
  8. Permit 扩展用于阻止或者延迟 Pod 与节点的绑定。Permit 扩展可以做下面三件事中的一项:
    • approve(批准):当所有的 permit 扩展都 approve 了 Pod 与节点的绑定,调度器将继续执行绑定过程
    • deny(拒绝):如果任何一个 permit 扩展 deny 了 Pod 与节点的绑定,Pod 将被放回到待调度队列,此时将触发 Unreserve 扩展。
    • wait(等待):如果一个 permit 扩展返回了 wait,则 Pod 将保持在 permit 阶段,直到被其他扩展 approve,如果超时事件发生,wait 状态变成 deny,Pod 将被放回到待调度队列,此时将触发 Unreserve 扩展
  9. Pre-bind 扩展用于在 Pod 绑定之前执行某些逻辑。例如,pre-bind 扩展可以将一个基于网络的数据卷挂载到节点上,以便 Pod 可以使用。如果任何一个 pre-bind 扩展返回错误,Pod 将被放回到待调度队列,此时将触发 Unreserve 扩展。
  10. Bind 扩展用于将 Pod 绑定到节点上:
    • 只有所有的 pre-bind 扩展都成功执行了,bind 扩展才会执行
    • 调度框架按照 bind 扩展注册的顺序逐个调用 bind 扩展
    • 具体某个 bind 扩展可以选择处理或者不处理该 Pod
    • 如果某个 bind 扩展处理了该 Pod 与节点的绑定,余下的 bind 扩展将被忽略
  11. Post-bind 是一个通知性质的扩展:
    • Post-bind 扩展在 Pod 成功绑定到节点上之后被动调用
    • Post-bind 扩展是绑定过程的最后一个步骤,可以用来执行资源清理的动作
  12. Unreserve 是一个通知性质的扩展,如果为 Pod 预留了资源,Pod 又在被绑定过程中被拒绝绑定,则 unreserve 扩展将被调用。Unreserve 扩展应该释放已经为 Pod 预留的节点上的计算资源。在一个插件中,reserve 扩展和 unreserve 扩展应该成对出现

🍂 如果我们要实现自己的插件,必须向调度框架注册插件并完成配置,另外还必须实现扩展点接口,对应的扩展点接口我们可以在源码 pkg/scheduler/framework/v1alpha1/interface.go 文件中找到,如下所示:

// Plugin is the parent type for all the scheduling framework plugins.
type Plugin interface {
    Name() string
}

type QueueSortPlugin interface {
    Plugin
    Less(*PodInfo, *PodInfo) bool
}

// PreFilterPlugin is an interface that must be implemented by "prefilter" plugins.
// These plugins are called at the beginning of the scheduling cycle.
type PreFilterPlugin interface {
    Plugin
    PreFilter(pc *PluginContext, p *v1.Pod) *Status
}

// FilterPlugin is an interface for Filter plugins. These plugins are called at the
// filter extension point for filtering out hosts that cannot run a pod.
// This concept used to be called 'predicate' in the original scheduler.
// These plugins should return "Success", "Unschedulable" or "Error" in Status.code.
// However, the scheduler accepts other valid codes as well.
// Anything other than "Success" will lead to exclusion of the given host from
// running the pod.
type FilterPlugin interface {
    Plugin
    Filter(pc *PluginContext, pod *v1.Pod, nodeName string) *Status
}

// PostFilterPlugin is an interface for Post-filter plugin. Post-filter is an
// informational extension point. Plugins will be called with a list of nodes
// that passed the filtering phase. A plugin may use this data to update internal
// state or to generate logs/metrics.
type PostFilterPlugin interface {
    Plugin
    PostFilter(pc *PluginContext, pod *v1.Pod, nodes []*v1.Node, filteredNodesStatuses NodeToStatusMap) *Status
}

// ScorePlugin is an interface that must be implemented by "score" plugins to rank
// nodes that passed the filtering phase.
type ScorePlugin interface {
    Plugin
    Score(pc *PluginContext, p *v1.Pod, nodeName string) (int, *Status)
}

// ScoreWithNormalizePlugin is an interface that must be implemented by "score"
// plugins that also need to normalize the node scoring results produced by the same
// plugin's "Score" method.
type ScoreWithNormalizePlugin interface {
    ScorePlugin
    NormalizeScore(pc *PluginContext, p *v1.Pod, scores NodeScoreList) *Status
}

// ReservePlugin is an interface for Reserve plugins. These plugins are called
// at the reservation point. These are meant to update the state of the plugin.
// This concept used to be called 'assume' in the original scheduler.
// These plugins should return only Success or Error in Status.code. However,
// the scheduler accepts other valid codes as well. Anything other than Success
// will lead to rejection of the pod.
type ReservePlugin interface {
    Plugin
    Reserve(pc *PluginContext, p *v1.Pod, nodeName string) *Status
}

// PreBindPlugin is an interface that must be implemented by "prebind" plugins.
// These plugins are called before a pod being scheduled.
type PreBindPlugin interface {
    Plugin
    PreBind(pc *PluginContext, p *v1.Pod, nodeName string) *Status
}

// PostBindPlugin is an interface that must be implemented by "postbind" plugins.
// These plugins are called after a pod is successfully bound to a node.
type PostBindPlugin interface {
    Plugin
    PostBind(pc *PluginContext, p *v1.Pod, nodeName string)
}

// UnreservePlugin is an interface for Unreserve plugins. This is an informational
// extension point. If a pod was reserved and then rejected in a later phase, then
// un-reserve plugins will be notified. Un-reserve plugins should clean up state
// associated with the reserved Pod.
type UnreservePlugin interface {
    Plugin
    Unreserve(pc *PluginContext, p *v1.Pod, nodeName string)
}

// PermitPlugin is an interface that must be implemented by "permit" plugins.
// These plugins are called before a pod is bound to a node.
type PermitPlugin interface {
    Plugin
    Permit(pc *PluginContext, p *v1.Pod, nodeName string) (*Status, time.Duration)
}

// BindPlugin is an interface that must be implemented by "bind" plugins. Bind
// plugins are used to bind a pod to a Node.
type BindPlugin interface {
    Plugin
    Bind(pc *PluginContext, p *v1.Pod, nodeName string) *Status
}

🍂 对于调度框架插件的启用或者禁用,我们可以使用安装集群时的 KubeSchedulerConfiguration 资源对象来进行配置。下面的例子中的配置启用了一个实现了 reservepreBind 扩展点的插件,并且禁用了另外一个插件,同时为插件 foo 提供了一些配置信息:

apiVersion: kubescheduler.config.k8s.io/v1alpha1
kind: KubeSchedulerConfiguration

...

plugins:
  reserve:
    enabled:
    - name: foo
    - name: bar
    disabled:
    - name: baz
  preBind:
    enabled:
    - name: foo
    disabled:
    - name: baz

pluginConfig:
- name: foo
  args: >
    foo插件可以解析的任意内容

🍂 扩展的调用顺序如下:

  • 如果某个扩展点没有配置对应的扩展,调度框架将使用默认插件中的扩展
  • 如果为某个扩展点配置且激活了扩展,则调度框架将先调用默认插件的扩展,再调用配置中的扩展
  • 默认插件的扩展始终被最先调用,然后按照 KubeSchedulerConfiguration 中扩展的激活 enabled 顺序逐个调用扩展点的扩展
  • 可以先禁用默认插件的扩展,然后在 enabled 列表中的某个位置激活默认插件的扩展,这种做法可以改变默认插件的扩展被调用时的顺序

假设默认插件 foo 实现了 reserve 扩展点,此时我们要添加一个插件 bar,想要在 foo 之前被调用,则应该先禁用 foo 再按照 bar foo 的顺序激活。示例配置如下所示:

apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration

...

plugins:
  reserve:
    enabled:
    - name: bar
    - name: foo
    disabled:
    - name: foo

在源码目录 pkg/scheduler/framework/plugins/examples 中有几个示范插件,我们可以参照其实现方式。

2.示例(代码部分)

因为涉及到代码部分,本次这里不做演示,看下就好。

其实要实现一个调度框架的插件,并不难,我们只要实现对应的扩展点,然后将插件注册到调度器中即可,下面是默认调度器在初始化的时候注册的插件:

// pkg/scheduler/algorithmprovider/registry.go
func NewRegistry() Registry {
    return Registry{
        // FactoryMap:
        // New plugins are registered here.
        // example:
        // {
        //  stateful_plugin.Name: stateful.NewStatefulMultipointExample,
        //  fooplugin.Name: fooplugin.New,
        // }
    }
}

但是可以看到默认并没有注册一些插件,所以要想让调度器能够识别我们的插件代码,就需要自己来实现一个调度器了,当然这个调度器我们完全没必要完全自己实现,直接调用默认的调度器,然后在上面的 NewRegistry() 函数中将我们的插件注册进去即可。在 kube-scheduler 的源码文件 kubernetes/cmd/kube-scheduler/app/server.go 中有一个 NewSchedulerCommand 入口函数,其中的参数是一个类型为 Option 的列表,而这个 Option 恰好就是一个插件配置的定义:

// Option configures a framework.Registry.
type Option func(framework.Registry) error

// NewSchedulerCommand creates a *cobra.Command object with default parameters and registryOptions
func NewSchedulerCommand(registryOptions ...Option) *cobra.Command {
  ......
}

所以我们完全就可以直接调用这个函数来作为我们的函数入口,并且传入我们自己实现的插件作为参数即可,而且该文件下面还有一个名为 WithPlugin 的函数可以来创建一个 Option 实例:

// WithPlugin creates an Option based on plugin name and factory.
func WithPlugin(name string, factory framework.PluginFactory) Option {
    return func(registry framework.Registry) error {
        return registry.Register(name, factory)
    }
}

所以最终我们的入口函数如下所示:

func main() {
    rand.Seed(time.Now().UTC().UnixNano())

    command := app.NewSchedulerCommand(
        app.WithPlugin(sample.Name, sample.New),
    )

    logs.InitLogs()
    defer logs.FlushLogs()

    if err := command.Execute(); err != nil {
        _, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
        os.Exit(1)
    }

}

其中 app.WithPlugin(sample.Name, sample.New) 就是我们接下来要实现的插件,从 WithPlugin 函数的参数也可以看出我们这里的 sample.New 必须是一个 framework.PluginFactory 类型的值,而 PluginFactory 的定义就是一个函数:

type PluginFactory = func(configuration *runtime.Unknown, f FrameworkHandle) (Plugin, error)

所以 sample.New 实际上就是上面的这个函数,在这个函数中我们可以获取到插件中的一些数据然后进行逻辑处理即可,插件实现如下所示,我们这里只是简单获取下数据打印日志,如果你有实际需求的可以根据获取的数据就行处理即可,我们这里只是实现了 PreFilterFilterPreBind 三个扩展点,其他的可以用同样的方式来扩展即可:

// 插件名称
const Name = "sample-plugin"

type Args struct {
    FavoriteColor  string `json:"favorite_color,omitempty"`
    FavoriteNumber int    `json:"favorite_number,omitempty"`
    ThanksTo       string `json:"thanks_to,omitempty"`
}

type Sample struct {
    args   *Args
    handle framework.FrameworkHandle
}

func (s *Sample) Name() string {
    return Name
}

func (s *Sample) PreFilter(pc *framework.PluginContext, pod *v1.Pod) *framework.Status {
    klog.V(3).Infof("prefilter pod: %v", pod.Name)
    return framework.NewStatus(framework.Success, "")
}

func (s *Sample) Filter(pc *framework.PluginContext, pod *v1.Pod, nodeName string) *framework.Status {
    klog.V(3).Infof("filter pod: %v, node: %v", pod.Name, nodeName)
    return framework.NewStatus(framework.Success, "")
}

func (s *Sample) PreBind(pc *framework.PluginContext, pod *v1.Pod, nodeName string) *framework.Status {
    if nodeInfo, ok := s.handle.NodeInfoSnapshot().NodeInfoMap[nodeName]; !ok {
        return framework.NewStatus(framework.Error, fmt.Sprintf("prebind get node info error: %+v", nodeName))
    } else {
        klog.V(3).Infof("prebind node info: %+v", nodeInfo.Node())
        return framework.NewStatus(framework.Success, "")
    }
}

//type PluginFactory = func(configuration *runtime.Unknown, f FrameworkHandle) (Plugin, error)
func New(configuration *runtime.Unknown, f framework.FrameworkHandle) (framework.Plugin, error) {
    args := &Args{}
    if err := framework.DecodeInto(configuration, args); err != nil {
        return nil, err
    }
    klog.V(3).Infof("get plugin config args: %+v", args)
    return &Sample{
        args: args,
        handle: f,
    }, nil
}

完整代码可以前往仓库 https://github.com/cnych/sample-scheduler-framework 获取。

实现完成后,编译打包成镜像即可,然后我们就可以当成普通的应用用一个 Deployment 控制器来部署即可,由于我们需要去获取集群中的一些资源对象,所以当然需要申请 RBAC 权限,然后同样通过 --config 参数来配置我们的调度器,同样还是使用一个 KubeSchedulerConfiguration 资源对象配置,可以通过 plugins 来启用或者禁用我们实现的插件,也可以通过 pluginConfig 来传递一些参数值给插件:

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: sample-scheduler-clusterrole
rules:
  - apiGroups:
      - ""
    resources:
      - endpoints
      - events
    verbs:
      - create
      - get
      - update
  - apiGroups:
      - ""
    resources:
      - nodes
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - ""
    resources:
      - pods
    verbs:
      - delete
      - get
      - list
      - watch
      - update
  - apiGroups:
      - ""
    resources:
      - bindings
      - pods/binding
    verbs:
      - create
  - apiGroups:
      - ""
    resources:
      - pods/status
    verbs:
      - patch
      - update
  - apiGroups:
      - ""
    resources:
      - replicationcontrollers
      - services
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - apps
      - extensions
    resources:
      - replicasets
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - apps
    resources:
      - statefulsets
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - policy
    resources:
      - poddisruptionbudgets
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - ""
    resources:
      - persistentvolumeclaims
      - persistentvolumes
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - ""
    resources:
      - configmaps
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - "storage.k8s.io"
    resources:
      - storageclasses
      - csinodes
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - "coordination.k8s.io"
    resources:
      - leases
    verbs:
      - create
      - get
      - list
      - update
  - apiGroups:
      - "events.k8s.io"
    resources:
      - events
    verbs:
      - create
      - patch
      - update
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: sample-scheduler-sa
  namespace: kube-system
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: sample-scheduler-clusterrolebinding
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: sample-scheduler-clusterrole
subjects:
- kind: ServiceAccount
  name: sample-scheduler-sa
  namespace: kube-system
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: scheduler-config
  namespace: kube-system
data:
  scheduler-config.yaml: |
    apiVersion: kubescheduler.config.k8s.io/v1beta1
    kind: KubeSchedulerConfiguration
    leaderElection:
      leaderElect: true
      leaseDuration: 15s
      renewDeadline: 10s
      resourceLock: endpointsleases
      resourceName: sample-scheduler
      resourceNamespace: kube-system
      retryPeriod: 2s
    profiles:
      - schedulerName: sample-scheduler
        plugins:
          preFilter:
            enabled:
              - name: "sample-plugin"
          filter:
            enabled:
              - name: "sample-plugin"
        pluginConfig:
          - name: sample-plugin
            args:  # runtime.Object
              favorColor: "#326CE5"
              favorNumber: 7
              thanksTo: "Kubernetes"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-scheduler
  namespace: kube-system
  labels:
    component: sample-scheduler
spec:
  selector:
    matchLabels:
      component: sample-scheduler
  template:
    metadata:
      labels:
        component: sample-scheduler
    spec:
      serviceAccountName: sample-scheduler-sa
      priorityClassName: system-cluster-critical
      volumes:
        - name: scheduler-config
          configMap:
            name: scheduler-config
      containers:
        - name: scheduler
          image: cnych/sample-scheduler:v0.2.4
          imagePullPolicy: IfNotPresent
          command:
            - sample-scheduler
            - --config=/etc/kubernetes/scheduler-config.yaml
            - --v=3
          volumeMounts:
            - name: scheduler-config
              mountPath: /etc/kubernetes
#          livenessProbe:
#            httpGet:
#              path: /healthz
#              port: 10251
#            initialDelaySeconds: 15
#          readinessProbe:
#            httpGet:
#              path: /healthz
#              port: 10251

直接部署上面的资源对象即可,这样我们就部署了一个名为 sample-scheduler 的调度器了,接下来我们可以部署一个应用来使用这个调度器进行调度:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-scheduler
spec:
  selector:
    matchLabels:
      app: test-scheduler
  template:
    metadata:
      labels:
        app: test-scheduler
    spec:
      schedulerName: sample-scheduler  # 指定使用的调度器,不指定使用默认的default-scheduler
      containers:
        - image: nginx:1.7.9
          imagePullPolicy: IfNotPresent
          name: nginx
          ports:
            - containerPort: 80

这里需要注意的是我们现在手动指定了一个 schedulerName 的字段,将其设置成上面我们自定义的调度器名称 sample-scheduler

我们直接创建这个资源对象,创建完成后查看我们自定义调度器的日志信息:

➜ kubectl get pods -n kube-system -l component=sample-scheduler
NAME                               READY   STATUS    RESTARTS   AGE
sample-scheduler-896658cd7-k7vcl   1/1     Running   0          57s
➜ kubectl logs -f sample-scheduler-896658cd7-k7vcl -n kube-system
I0114 09:14:18.878613       1 eventhandlers.go:173] add event for unscheduled pod default/test-scheduler-6486fd49fc-zjhcx
I0114 09:14:18.878670       1 scheduler.go:464] Attempting to schedule pod: default/test-scheduler-6486fd49fc-zjhcx
I0114 09:14:18.878706       1 sample.go:77] "Start PreFilter Pod" pod="test-scheduler-6486fd49fc-zjhcx"
I0114 09:14:18.878802       1 sample.go:93] "Start Filter Pod" pod="test-scheduler-6486fd49fc-zjhcx" node="node2" preFilterState=&{Resource:{MilliCPU:0 Memory:0 EphemeralStorage:0 AllowedPodNumber:0 ScalarResources:map[]}}
I0114 09:14:18.878835       1 sample.go:93] "Start Filter Pod" pod="test-scheduler-6486fd49fc-zjhcx" node="node1" preFilterState=&{Resource:{MilliCPU:0 Memory:0 EphemeralStorage:0 AllowedPodNumber:0 ScalarResources:map[]}}
I0114 09:14:18.879043       1 default_binder.go:51] Attempting to bind default/test-scheduler-6486fd49fc-zjhcx to node1
I0114 09:14:18.886360       1 scheduler.go:609] "Successfully bound pod to node" pod="default/test-scheduler-6486fd49fc-zjhcx" node="node1" evaluatedNodes=3 feasibleNodes=2
I0114 09:14:18.887426       1 eventhandlers.go:205] delete event for unscheduled pod default/test-scheduler-6486fd49fc-zjhcx
I0114 09:14:18.887475       1 eventhandlers.go:225] add event for scheduled pod default/test-scheduler-6486fd49fc-zjhcx

可以看到当我们创建完 Pod 后,在我们自定义的调度器中就出现了对应的日志,并且在我们定义的扩展点上面都出现了对应的日志,证明我们的示例成功了,也可以通过查看 Pod 的 schedulerName 来验证:

➜ kubectl get pods
NAME                              READY   STATUS    RESTARTS       AGE
test-scheduler-6486fd49fc-zjhcx   1/1     Running   0              35s
➜ kubectl get pod test-scheduler-6486fd49fc-zjhcx -o yaml
......
restartPolicy: Always
schedulerName: sample-scheduler
securityContext: {}
serviceAccount: default
......

从 Kubernetes v1.17 版本开始,Scheduler Framework 内置的预选和优选函数已经全部插件化,所以要扩展调度器我们应该掌握并理解调度框架这种方式。

2、调度器调优

作为 kubernetes 集群的默认调度器,kube-scheduler 主要负责将 Pod 调度到集群的 Node 上。在一个集群中,满足一个 Pod 调度请求的所有节点称之为 可调度 Node,调度器先在集群中找到一个 Pod 的可调度 Node,然后根据一系列函数对这些可调度 Node 进行打分,之后选出其中得分最高的 Node 来运行 Pod,最后,调度器将这个调度决定告知 kube-apiserver,这个过程叫做绑定

在 Kubernetes 1.12 版本之前,kube-scheduler 会检查集群中所有节点的可调度性,并且给可调度节点打分。Kubernetes 1.12 版本添加了一个新的功能,允许调度器在找到一定数量的可调度节点之后就停止继续寻找可调度节点。该功能能提高调度器在大规模集群下的调度性能,这个数值是集群规模的百分比,这个百分比通过 percentageOfNodesToScore 参数来进行配置,其值的范围在 1 到 100 之间,最大值就是 100%,如果设置为 0 就代表没有提供这个参数配置。

Kubernetes 1.14 版本又加入了一个特性,在该参数没有被用户配置的情况下,调度器会根据集群的规模自动设置一个集群比例,然后通过这个比例筛选一定数量的可调度节点进入打分阶段。该特性使用线性公式计算出集群比例,比如100个节点的集群下会取 50%,在 5000节点的集群下取 10%,这个自动设置的参数的最低值是 5%,换句话说,调度器至少会对集群中 5% 的节点进行打分,除非用户将该参数设置的低于 5。

注意

当集群中的可调度节点少于 50 个时,调度器仍然会去检查所有节点,因为可调度节点太少,不足以停止调度器最初的过滤选择。如果我们想要关掉这个范围参数,可以将 percentageOfNodesToScore 值设置成 100。

percentageOfNodesToScore 的值必须在 1 到 100 之间,而且其默认值是通过集群的规模计算得来的,另外 50 个 Node 的数值是硬编码在程序里面的,设置这个值的作用在于:当集群的规模是数百个节点并且 percentageOfNodesToScore 参数设置的过低的时候,调度器筛选到的可调度节点数目基本不会受到该参数影响。当集群规模较小时,这个设置对调度器性能提升并不明显,但是在超过 1000 个 Node 的集群中,将调优参数设置为一个较低的值可以很明显的提升调度器性能。

不过值得注意的是,该参数设置后可能会导致只有集群中少数节点被选为可调度节点,很多 Node 都没有进入到打分阶段,这样就会造成一种后果,一个本来可以在打分阶段得分很高的 Node 甚至都不能进入打分阶段。由于这个原因,所以这个参数不应该被设置成一个很低的值,通常的做法是不会将这个参数的值设置的低于 10,很低的参数值一般在调度器的吞吐量很高且对 Node 的打分不重要的情况下才使用。换句话说,只有当你更倾向于在可调度节点中任意选择一个 Node 来运行这个 Pod 时,才使用很低的参数设置。

如果你的集群规模只有数百个节点或者更少,实际上并不推荐你将这个参数设置得比默认值更低,因为这种情况下不太会有效的提高调度器性能。

3、创建一个Pod的工作流程

一般情况下我们部署的 Pod 是通过集群的自动调度策略来选择节点的,默认情况下调度器考虑的是资源足够,并且负载尽量平均。但是有的时候我们需要能够更加细粒度的去控制 Pod 的调度,比如我们希望一些机器学习的应用只跑在有 GPU 的节点上但是有的时候我们的服务之间交流比较频繁,又希望能够将这服务的 Pod 都调度到同一个的节点上。这就需要使用一些调度方式来控制 Pod 的调度了,主要有两个概念:亲和性和反亲和性,亲和性又分成节点亲和性(nodeAffinity)和 Pod 亲和性(podAffinity)

1.架构图

Kubernetes基于list-watch机制的控制器架构,实现组件间交互的解耦。
其他组件监控自己负责的资源,当这些资源发生变化时,kube-apiserver会通知这些组件,这个过程类似于发布与订阅

image-20220309130416811

2.剖析过程
  • 我们通过命令行创建一个pod:

image-20210614222733041

  • 当我们在执行命令kubectl run pod4 --image=nginx后,各组件之间的调用流程是如何的呢?
1、kubectl向apiserver发送一个创建pod的请求
2、apiserver接收到并向etcd写入存储,写入成功返回一个提示

#这个过程类似于老板和顾客之间的关系:
	api-server:开店老板
	etcd:仓库
	其他组件:顾客

image-20210615063729371

  • 注意:如果在执行如下kubectl run pod4 --image=nginx命令时,卡着了,说明什么问题呢?

–>说明:
etcd数据库写入有问题/达到性能瓶颈,或者,api-server和etcd 2者总有一个有问题:

image-20210615063918928

  • 继续:scheduler向apiserver查询未分配的pod资源,通过自身调度算法选择一个合适的node进行绑定(给这个pod资源打一个标记,标记分配到node1)注意:它这个调度算法还是比较复杂、均匀一点的,它会考虑到你的机器的硬件配置,pod属性等等一些综合的属性;

image-20210615065028529

  • 问题:如果scheduler组件有问题,那么此时pod会出现什么状态?

答:pod会出现通过kubectl get pod根本看不到你刚创建的pod信息的,更别说它的状态了,因为它根本没分配。

因此,如果你创建的pod信息根本看不到,那么会是哪个组件可能有问题?–>scheduler组件可能出在问题。

  • 如果是pending状态:pod是已经绑定到某个节点了

  • 继续流程讲解:

4、kubelet向apiserver查询分配到自己节点的pod,调用docker api(/var/run/docker.sock)创建容器
5、kubelet获取docker创建容器的状态,并汇报给apiserver,apiserver更新状态到etcd存储
6、kubectl get pods就能查看pod状态

备注:
kubelet的功能主要是管理容器:

这个是默认调用的docker api接口
[root@k8s-master ~]#ll /var/run/docker.sock
srw-rw---- 1 root docker 0 Jun 14 11:46 /var/run/docker.sock

image-20210615070717425

image-20210615070800673

  • 问题:controller-manager为什么没用到?

controller-manager是用于管理控制器,例如deployment(rs)、service,因为创建的是一个pod,不受它管理。

如果controller-manager要放在这里,一般是放在etcd后面的

image-20210615071819565

  • kube-proxy为什么没用到?

proxy是用于管理pod网络,例如service,因为没创建service。

kube-proxy的主要功能就是维护好service,service是k8s的抽象资源;

  • 扩展

比如,这个容器创建的时候创建失败了,不是一个running状态,也不是一个pending状态。可能就是docker在启动容器时,用你那个镜像启动容器失败了。所以这是你需要用docker去run一个镜像看能不能起来。

3.Pod中影响调度的主要属性

image-20210615073053200

⚠️ 注意

resources: {} 资源调度依据这个,挺重要的;
很多大厂都会去二开"schedulerName: default-scheduler"这个调度器的,会去加一些调度策略,进而完成他们的需求;

🍂 调度原因失败分析

kubectl get pod <NAME> -o wide

查看调度失败原因:kubectl describe pod <NAME>
• 节点CPU/内存不足
• 有污点,没容忍 (tolerations)
• 没有匹配到节点标签 (n)
4.调度器需要充分考虑诸多的因素


资源高效利用:装箱率要高!
afinity:微服务,分步式系统,网络调用,本机调用,排除了网络调用,额外的传输时间,物理网卡带宽限制!
anti-affinity:某个业务的不同副本,不能让其跑在一台机器上,一个机架上,一个地域里,使其分布在不同的故障域。
locality:数据本地化,是一个很重要的概念,哪里有数据,我的作业就去哪里,这样可以减少数据拷贝的开销。k8s里的拉取镜像。

🍂

听起来很简单,但这个过程中会涉及Predicate、Priority等各种调度算法,还有优先级(Priority )、**抢占(Preemption)**等各种机制。在实际的调度设计中,有非常多需要考虑的问题,比如:

1. 公平:如何保证每个节点都能被分配资源

2. 资源高效利用:怎样压榨集群的资源能力,让资源被最大化使用

3. 效率:调度的性能要好,能够尽快地对大批量的 pod 完成调度工作

4. 灵活:允许用户根据自己的需求控制调度的逻辑

5.Kubernetes中的资源分配

1.limits:在Cgroups里使用;cpu.cfs_quota/cpu.cfs_period(10w)=1
2.requests:cpu这个requests其实在Cgroup里也起作用。当你多个应用发生资源抢占时,他们抢占的cpu时间比较是多少呢?是通过cpu.share去调节的。k8s是如何实现的呢?这里如果设置的是一个cpu,request是1的话,那么cpu.share是1024。 如果你设置的是100m,相当于是0.1个cpu,那么cpu.share就是0.1*1024=102. 也就是cpu.requests也是最终会体现到Cgroups里面去的

6.Init container的资源需求

1、nodeSelector

nodeSelector:用于将Pod调度到匹配Label的Node上,如果没有匹配的标签会调度失败

作用:

  • 约束Pod到特定的节点运行
  • 完全匹配节点标签

应用场景:

  • 专用节点:根据业务线将Node分组管理
  • 配备特殊硬件:部分Node配有SSD硬盘、GPU
💘 实践:nodeSelector测试(测试成功)-2022.5.16

在了解亲和性之前,我们先来了解一个非常常用的调度方式:nodeSelector。我们知道 label 标签是 kubernetes 中一个非常重要的概念,用户可以非常灵活的利用 label 来管理集群中的资源,比如最常见的 Service 对象通过 label 去匹配 Pod 资源,而 Pod 的调度也可以根据节点的 label 来进行调度

  • 我们可以通过下面的命令查看我们的 node 的 label:
[root@master1 ~]#kubectl get node --show-labels 
NAME      STATUS   ROLES                  AGE    VERSION   LABELS
master1   Ready    control-plane,master   109d   v1.22.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=master1,kubernetes.io/os=linux,node-role.kubernetes.io/control-plane=,node-role.kubernetes.io/master=,node.kubernetes.io/exclude-from-external-load-balancers=
node1     Ready    <none>                 109d   v1.22.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=node1,kubernetes.io/os=linux
node2     Ready    <none>                 109d   v1.22.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=node2,kubernetes.io/os=linux
  • 现在我们先给节点 node2 增加一个com=youdianzhishi的标签,命令如下:
[root@master1 ~]#kubectl label nodes node2 com=youdianzhishi
node/node2 labeled

我们可以通过上面的 --show-labels 参数可以查看上述标签是否生效。

[root@master1 ~]#kubectl get node node2 --show-labels 
NAME    STATUS   ROLES    AGE    VERSION   LABELS
node2   Ready    <none>   109d   v1.22.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,com=youdianzhishi,kubernetes.io/arch=amd64,kubernetes.io/hostname=node2,kubernetes.io/os=linux
  • Pod里配置nodeSelector字段:

当节点被打上了相关标签后,在调度的时候就可以使用这些标签了,只需要在 Pod 的 spec 字段中添加 nodeSelector 字段,里面是我们需要被调度的节点的 label 标签,比如,下面的 Pod 我们要强制调度到 node2 这个节点上去,我们就可以使用 nodeSelector 来表示了:

$ vim 01-node-selector-demo.yaml

# 01-node-selector-demo.yaml
apiVersion: v1
kind: Pod
metadata:
  labels:
    app: busybox-pod
  name: test-busybox
spec:
  containers:
  - command:
    - sleep
    - "3600"
    image: busybox
    imagePullPolicy: Always
    name: test-busybox
  nodeSelector: #注意:nodeSelector是和containers同级的;注意,这个放的顺序一定要放在containers后面。不然会报错的!
    com: youdianzhishi
  • 部署后,我们就可以通过 describe 命令查看调度结果:
hg@LAPTOP-G8TUFE0T:/mnt/c/Users/hg/Desktop/yaml$ kubectl apply -f 01-node-selector-demo.yaml 
pod/test-busybox created

hg@LAPTOP-G8TUFE0T:/mnt/c/Users/hg/Desktop/yaml$ kubectl get po  -owide
NAME           READY   STATUS    RESTARTS   AGE   IP             NODE    NOMINATED NODE   READINESS GATES
test-busybox   1/1     Running   0          78s   10.244.2.210   node2   <none>           <none>


[root@master1 ~]#kubectl describe po test-busybox  
Name:         test-busybox
Namespace:    default
Priority:     0
Node:         node2/172.29.9.53
Start Time:   Thu, 17 Feb 2022 19:45:11 +0800
Labels:       app=busybox-pod
Annotations:  <none>
Status:       Running
IP:           10.244.2.210
IPs:
  IP:  10.244.2.210
Containers:
  test-busybox:
    Container ID:  containerd://1b4d323942e6d305a4ea25f655eaced77f8cb8e4229eaf1972dc9dfb1246a0c0
    Image:         busybox
    Image ID:      docker.io/library/busybox@sha256:5acba83a746c7608ed544dc1533b87c737a0b0fb730301639a0179f9344b1678
    Port:          <none>
    Host Port:     <none>
    Command:
      sleep
      3600
    State:          Running
      Started:      Thu, 17 Feb 2022 19:45:31 +0800
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-p5z6t (ro)
Conditions:
  Type              Status
  Initialized       True 
  Ready             True 
  ContainersReady   True 
  PodScheduled      True 
Volumes:
  kube-api-access-p5z6t:
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  3607
    ConfigMapName:           kube-root-ca.crt
    ConfigMapOptional:       <nil>
    DownwardAPI:             true
QoS Class:                   BestEffort
Node-Selectors:              com=youdianzhishi
Tolerations:                 node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
                             node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
  Type    Reason     Age    From               Message
  ----    ------     ----   ----               -------
  Normal  Scheduled  2m45s  default-scheduler  Successfully assigned default/test-busybox to node2
  Normal  Pulling    2m43s  kubelet            Pulling image "busybox"
  Normal  Pulled     2m26s  kubelet            Successfully pulled image "busybox" in 17.583571931s
  Normal  Created    2m26s  kubelet            Created container test-busybox
  Normal  Started    2m25s  kubelet            Started container test-busybox
[root@master1 ~]#

我们可以看到 Events 下面的信息,我们的 Pod 通过默认的 default-scheduler 调度器被绑定到了 node2 节点。不过需要注意的是nodeSelector 属于强制性的,如果我们的目标节点没有可用的资源,我们的 Pod 就会一直处于 Pending 状态。

通过上面的例子我们可以感受到 nodeSelector 的方式比较直观,但是还不够灵活,控制粒度偏大。接下来我们再和大家了解下更加灵活的方式:节点亲和性(nodeAffinity)。

测试结束。😘

🍂 问题:

因pod中nodeSelector里的标签未出现在all node节点,但后续给node打好符合要求的标签,原来处于pending状态的pod会自动迁移过去的吗?–>会的

2、亲和性和反亲和性调度

前面我们了解了 kubernetes 调度器的调度流程,我们知道默认的调度器在使用的时候,经过了 predicatespriorities 两个阶段,但是在实际的生产环境中,往往我们需要根据自己的一些实际需求来控制 Pod 的调度,这就需要用到 nodeAffinity(节点亲和性)podAffinity(pod 亲和性) 以及 podAntiAffinity(pod 反亲和性)

🍂 亲和性调度可以分成软策略硬策略两种方式:

  • 软策略就是如果现在没有满足调度要求的节点的话,Pod 就会忽略这条规则,继续完成调度过程,说白了就是满足条件最好了,没有的话也无所谓
  • 硬策略就比较强硬了,如果没有满足条件的节点的话,就不断重试直到满足条件为止,简单说就是你必须满足我的要求,不然就不干了

对于亲和性和反亲和性都有这两种规则可以设置: preferredDuringSchedulingIgnoredDuringExecutionrequiredDuringSchedulingIgnoredDuringExecution,前面的就是软策略,后面的就是硬策略。

1.节点亲和性

节点亲和性(nodeAffinity)主要是用来控制 Pod 要部署在哪些节点上,以及不能部署在哪些节点上的,它可以进行一些简单的逻辑组合了,不只是简单的相等匹配(比如前面的nodeSelector就是标签的=)。

nodeAffinity:节点亲和类似于nodeSelector,可以根据节点上的标签来约束Pod可以调度到哪些节点。

🍂 相比nodeSelector:

  • 匹配有更多的逻辑组合,不只是字符串的完全相等,支持的操作符有:In、NotIn、Exists、DoesNotExist、Gt、Lt
  • 调度分为软策略和硬策略,而不是硬性要求
    • 硬(required):必须满足
    • 软(preferred):尝试满足,但不保证

🍂 这里的匹配逻辑是 label 标签的值在某个列表中,现在 Kubernetes 提供的操作符有下面的几种:

  • In:label 的值在某个列表中 (这里的操作符,我们一般只用到in就足够了;)
  • NotIn:label 的值不在某个列表中
  • Gt:label 的值大于某个值
  • Lt:label 的值小于某个值
  • Exists:某个 label 存在
  • DoesNotExist:某个 label 不存在

⚠️ 注意:

但是需要注意的是如果 nodeSelectorTerms 下面有多个选项的话,满足任何一个条件就可以了;如果 matchExpressions有多个选项的话,则必须同时满足这些条件才能正常调度 Pod。

🍂

比nodeSelector更高级的一个!
matchExpressions比selector更加灵活。

💘 实践:节点亲和性测试(测试成功)-2022.5.16
  • 比如现在我们用一个 Deployment 来管理8个 Pod 副本,现在我们来控制下这些 Pod 的调度,如下例子:
# 02-node-affinity-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: node-affinity
  labels:
    app: node-affinity
spec:
  replicas: 8
  selector:
    matchLabels:
      app: node-affinity
  template:
    metadata:
      labels:
        app: node-affinity
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80
          name: nginxweb
      affinity: #定义亲和性
        nodeAffinity: #节点亲和性 
          requiredDuringSchedulingIgnoredDuringExecution:  # 硬策略
            nodeSelectorTerms:
            - matchExpressions:
              - key: kubernetes.io/hostname
                operator: NotIn
                values:
                - master1 #相当于只能调度到node1和node2节点。默认就不会调度到master1节点
          preferredDuringSchedulingIgnoredDuringExecution:  # 软策略(尽可能调度到node2节点)
          - weight: 1
            preference:
              matchExpressions:
              - key: com
                operator: In
                values:
                - youdianzhishi

上面这个 Pod 首先是要求不能运行在 master1 这个节点上,如果有个节点满足 com=youdianzhishi 的话就优先调度到这个节点上。

由于上面 node02 节点我们打上了 com=youdianzhishi 这样的 label 标签,所以按要求会优先调度到这个节点来的。

  • 现在我们来创建这个 Pod,然后查看具体的调度情况是否满足我们的要求。
➜ kubectl apply -f node-affinty-demo.yaml
deployment.apps/node-affinity created
➜ kubectl get pods -l app=node-affinity -o wide #老师这个有部分pod被调度到node1节点
NAME                            READY   STATUS    RESTARTS   AGE     IP             NODE         NOMINATED NODE   READINESS GATES
node-affinity-cdd9d54d9-bgbbh   1/1     Running   0          2m28s   10.244.2.247   node2   <none>           <none>
node-affinity-cdd9d54d9-dlbck   1/1     Running   0          2m28s   10.244.4.16    node1   <none>           <none>
node-affinity-cdd9d54d9-g2jr6   1/1     Running   0          2m28s   10.244.4.17    node1   <none>           <none>
node-affinity-cdd9d54d9-gzr58   1/1     Running   0          2m28s   10.244.1.118   node1   <none>           <none>
node-affinity-cdd9d54d9-hcv7r   1/1     Running   0          2m28s   10.244.2.246   node2   <none>           <none>
node-affinity-cdd9d54d9-kvxw4   1/1     Running   0          2m28s   10.244.2.245   node2   <none>           <none>
node-affinity-cdd9d54d9-p4mmk   1/1     Running   0          2m28s   10.244.2.244   node2   <none>           <none>
node-affinity-cdd9d54d9-t5mff   1/1     Running   0          2m28s   10.244.1.117   node2   <none>           <none>

从结果可以看出有5个 Pod 被部署到了 node2 节点上,但是可以看到并没有一个 Pod 被部署到 master1 这个节点上,因为我们的硬策略就是不允许部署到该节点上,而 node2 是软策略,所以会尽量满足

测试结束。😘

2.pod 亲和性和pod 反亲和性

Pod 亲和性(podAffinity)主要解决 Pod 可以和哪些 Pod 部署在同一个拓扑域中的问题(其中拓扑域用主机标签实现,可以是单个主机,也可以是多个主机组成的 cluster、zone 等等),而 Pod 反亲和性主要是解决 Pod 不能和哪些 Pod 部署在同一个拓扑域中的问题,它们都是处理的 Pod 与 Pod 之间的关系。比如一个 Pod 在一个节点上了,那么我这个也得在这个节点,或者你这个 Pod 在节点上了,那么我就不想和你待在同一个节点上。

👉🏼 这个是很重要的,线上业务基本要配置这种podAntiAffinity。

🍂 拓扑域

这里要重点理解下什么是拓扑域?–>你可以把它看成为一个分组。

image-20220218131033136

💘 实验:pod亲和性(测试成功)-2022.5.16
  • 由于我们这里只有一个集群,并没有区域或者机房的概念,所以我们这里直接使用主机名来作为拓扑域,把 Pod 创建在同一个主机上面。
[root@master1 ~]# kubectl get node --show-labels 
NAME      STATUS   ROLES                  AGE    VERSION   LABELS
master1   Ready    control-plane,master   110d   v1.22.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=master1,kubernetes.io/os=linux,node-role.kubernetes.io/control-plane=,node-role.kubernetes.io/master=,node.kubernetes.io/exclude-from-external-load-balancers=
node1     Ready    <none>                 110d   v1.22.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=node1,kubernetes.io/os=linux
node2     Ready    <none>                 110d   v1.22.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,com=youdianzhishi,kubernetes.io/arch=amd64,kubernetes.io/hostname=node2,kubernetes.io/os=linux
  • 同样,还是针对上面的资源对象,我们来测试下 Pod 的亲和性:
# 03-pod-affinity-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pod-affinity
  labels:
    app: pod-affinity
spec:
  replicas: 3
  selector:
    matchLabels:
      app: pod-affinity
  template:
    metadata:
      labels:
        app: pod-affinity
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80
          name: nginxweb
      affinity:
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:  # 硬策略
          - labelSelector: #去选择具有app in ["busybox-pod"]的pod所在的hostname这个域。
              matchExpressions:
              - key: app
                operator: In
                values:
                - busybox-pod
            topologyKey: kubernetes.io/hostname

上面这个例子中的 Pod 需要调度到某个指定的节点上,并且该节点上运行了一个带有 app=busybox-pod 标签的 Pod。我们可以查看有标签 app=busybox-pod 的 pod 列表:

[root@master1 ~]#kubectl get pods -l app=busybox-pod -o wide
NAME           READY   STATUS    RESTARTS      AGE   IP             NODE    NOMINATED NODE   READINESS GATES
test-busybox   1/1     Running   5 (23m ago)   18h   10.244.2.210   node2   <none>           <none>
  • 我们看到这个 Pod 运行在了 node2 的节点上面,所以按照上面的亲和性来说,上面我们部署的3个 Pod 副本也应该运行在 node2 节点上:
$ kubectl apply -f 03-pod-affinity-demo.yaml 
deployment.apps/pod-affinity created
$ kubectl get pods -o wide -l app=pod-affinity
NAME                           READY   STATUS    RESTARTS   AGE   IP             NODE    NOMINATED NODE   READINESS GATES
pod-affinity-785f687c5-52t54   1/1     Running   0          60s   10.244.2.228   node2   <none>           <none>
pod-affinity-785f687c5-g594p   1/1     Running   0          60s   10.244.2.229   node2   <none>           <none>
pod-affinity-785f687c5-s6j7h   1/1     Running   0          60s   10.244.2.227   node2   <none>           <none>
  • 如果我们把上面的 test-busybox 和 pod-affinity 这个 Deployment 都删除,然后重新创建 pod-affinity 这个资源,看看能不能正常调度呢:
$ kubectl delete -f 01-node-selector-demo.yaml
pod "test-busybox" deleted
$ kubectl delete -f 03-pod-affinity-demo.yaml
deployment.apps "pod-affinity" deleted

$ kubectl apply -f 03-pod-affinity-demo.yaml 
deployment.apps/pod-affinity created
$ kubectl get po
NAME                           READY   STATUS    RESTARTS   AGE
pod-affinity-785f687c5-2256q   0/1     Pending   0          80s
pod-affinity-785f687c5-7gpz5   0/1     Pending   0          80s
pod-affinity-785f687c5-97gpj   0/1     Pending   0          80s

我们可以看到都处于 Pending 状态了,这是因为现在没有一个节点上面拥有 app=busybox-pod 这个标签的 Pod,而上面我们的调度使用的是硬策略,所以就没办法进行调度了,大家可以去尝试下重新将 test-busybox 这个 Pod 调度到其他节点上,观察下上面的3个副本会不会也被调度到对应的节点上去。(这里可以自己测试下,可以利用node1的kubernetes.io/hostname: node1标签用nodeSelector来实现)

  • 我们这个地方使用的是 kubernetes.io/hostname 这个拓扑域,意思就是我们当前调度的 Pod 要和目标的 Pod 处于同一个主机上面,因为要处于同一个拓扑域下面。为了说明这个问题,我们把拓扑域改成 beta.kubernetes.io/os,同样的我们当前调度的 Pod 要和目标的 Pod 处于同一个拓扑域中,目标的 Pod 是拥有 beta.kubernetes.io/os=linux 的标签,而我们这里所有节点都有这样的标签,这也就意味着我们所有节点都在同一个拓扑域中,所以我们这里的 Pod 可以被调度到任何一个节点,重新运行上面的 app=busybox-pod 的 Pod,然后再更新下我们这里的资源对象:
$ kubectl get po -owide
NAME                            READY   STATUS    RESTARTS   AGE    IP             NODE    NOMINATED NODE   READINESS GATES
pod-affinity-6bf5bb4fc4-j6ctw   1/1     Running   0          22s    10.244.2.230   node2   <none>           <none>
pod-affinity-6bf5bb4fc4-xb7tr   1/1     Running   0          22s    10.244.2.231   node2   <none>           <none>
pod-affinity-6bf5bb4fc4-xl6pn   1/1     Running   0          22s    10.244.1.97    node1   <none>           <none>

image-20220218145745621

可以看到现在是分别运行在2个节点下面的,因为他们都属于 beta.kubernetes.io/os 这个拓扑域(而busybox-pod也刚好在这个域下,因此符合硬策略要求)。

⚠️ 这里需要注意下:通过上面这个实验可以看到,这2个node节点都属于beta.kubernetes.io/os这个拓扑域,但只有node1上有app=pod-affitity这个标签的pod,从结果可以看到也是可以调度到node2上的。

实验结束。😘

🍂 pod 反亲和性(podAntiAffinity)

Pod 反亲和性(podAntiAffinity)则是反着来的,比如一个节点上运行了某个 Pod,那么我们的模板 Pod 则不希望被调度到这个节点上面去了。

💘 实验:pod反亲和性(测试成功)-2022.5.16
  • 我们把上面的 podAffinity 直接改成 podAntiAffinity
# 04-pod-antiaffinity-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: pod-antiaffinity
  labels:
    app: pod-antiaffinity
spec:
  replicas: 3
  selector:
    matchLabels:
      app: pod-antiaffinity
  template:
    metadata:
      labels:
        app: pod-antiaffinity
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80
          name: nginxweb
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:  # 硬策略
          - labelSelector: #3个pod副本不会调度到具有app=busybox-pod所在的hostanme这个域(节点)上面
              matchExpressions:
              - key: app
                operator: In
                values:
                - busybox-pod
            topologyKey: kubernetes.io/hostname #注意:pod反亲和性是直接不往这个域里直接调度pod的!!!

这里的意思就是如果一个节点上面有一个 app=busybox-pod 这样的 Pod 的话,那么我们的 Pod 就别调度到这个节点上面来,上面我们把app=busybox-pod 这个 Pod 固定到了 node2 这个节点上面的,所以正常来说我们这里的 Pod 不会出现在该节点上:

$ kubectl apply -f 04-pod-antiaffinity-demo.yaml 
deployment.apps/pod-antiaffinity created
$ kubectl get po -owide
NAME                                READY   STATUS    RESTARTS   AGE   IP             NODE    NOMINATED NODE   READINESS GATES
pod-antiaffinity-57c57dd9f7-jspkt   1/1     Running   0          25s   10.244.1.100   node1   <none>           <none>
pod-antiaffinity-57c57dd9f7-mm78w   1/1     Running   0          25s   10.244.1.99    node1   <none>           <none>
pod-antiaffinity-57c57dd9f7-x9mft   1/1     Running   0          25s   10.244.1.98    node1   <none>           <none>

我们可以看到没有被调度到 node2 节点上,因为我们这里使用的是 Pod 反亲和性。

  • 大家可以思考下,如果这里我们将拓扑域更改成 beta.kubernetes.io/os 会怎么样呢?可以自己去测试下看看。
$ kubectl apply -f 04-pod-antiaffinity-demo.yaml 
deployment.apps/pod-antiaffinity created
$ kubectl get po
NAME                               READY   STATUS    RESTARTS   AGE
pod-antiaffinity-c5fb4db4d-jj5j7   0/1     Pending   0          4s
pod-antiaffinity-c5fb4db4d-xxnhg   0/1     Pending   0          4s
pod-antiaffinity-c5fb4db4d-zkbgp   0/1     Pending   0          4s

实验结束。😘

3、污点与容忍

Taint(污点)与Tolerations(污点容忍)

Taints:避免Pod调度到特定Node上

Tolerations:允许Pod调度到持有Taints的Node上

应用场景:

  • 专用节点:根据业务线将Node分组管理,希望在默认情况下不调度该节点,只有配置了污点容忍才允许分配
  • 配备特殊硬件:部分Node配有SSD硬盘、GPU,希望在默认情况下不调度该节点,只有配置了污点容忍才允许分配
  • 基于Taint的驱逐

对于 nodeAffinity 无论是硬策略还是软策略方式,都是调度 Pod 到预期节点上。而污点(Taints)恰好与之相反,如果一个节点标记为 Taints ,除非 Pod 也被标识为可以容忍污点节点,否则该 Taints 节点不会被调度 Pod。

比如用户希望把 Master 节点保留给 Kubernetes 系统组件使用,或者把一组具有特殊资源预留给某些 Pod,则污点就很有用了,Pod 不会再被调度到 taint 标记过的节点。我们使用 kubeadm 搭建的集群默认就给 master 节点添加了一个污点标记,所以我们看到我们平时的 Pod 都没有被调度到 master 上去。

⚠️ 污点:其实是一个label标签,只不过它是一个特殊的label标签。

[root@master1 ~]#kubectl get node master1 --show-labels 
NAME      STATUS   ROLES                  AGE    VERSION   LABELS
master1   Ready    control-plane,master   110d   v1.22.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=master1,kubernetes.io/os=linux,node-role.kubernetes.io/control-plane=,node-role.kubernetes.io/master=(注意,这个是一个空标签),node.kubernetes.io/exclude-from-external-load-balancers=

[root@master1 ~]#kubectl describe node master1
Name:               master1
Roles:              master
Labels:             beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/os=linux
                    kubernetes.io/arch=amd64
                    kubernetes.io/hostname=master1
                    kubernetes.io/os=linux
                    node-role.kubernetes.io/master=
......
Taints:             node-role.kubernetes.io/master:NoSchedule
Unschedulable:      false
......

我们可以使用上面的命令查看 master 节点的信息,其中有一条关于 Taints 的信息:node-role.kubernetes.io/master:NoSchedule,就表示master 节点打了一个污点的标记,其中影响的参数是 NoSchedule,表示 Pod 不会被调度到标记为 taints 的节点。除了 NoSchedule 外,还有另外两个选项:

  • PreferNoSchedule:NoSchedule 的软策略版本,表示尽量不调度到污点节点上去
  • NoExecute:该选项意味着一旦 Taint 生效(被打上taint),如该节点内正在运行的 Pod 没有对应容忍(Tolerate)设置,则会直接被逐出。 哈哈😂这个命令也是够狠。。。kubectl taint node k8s-node1 disktype=ssd:NoExecute

image-20220516220902219

🍂 污点 taint 标记节点的命令如下:

➜ kubectl taint nodes node2 test=node2:NoSchedule
node "node2" tainted

上面的命名将 node2 节点标记为了污点,影响策略是 NoSchedule只会影响新的 Pod 调度,如果仍然希望某个 Pod 调度到 taint 节点上,则必须在 Spec 中做出 Toleration 定义,才能调度到该节点。

🍂 最后如果我们要取消节点的污点标记,可以使用下面的命令:

➜ kubectl taint nodes node2 test-
node "node2" untainted
💘 实践:污点与容忍(测试成功)-2022.5.16
  • 比如现在我们想要将一个 Pod 调度到 master 节点:
# 05-taint-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: taint
  labels:
    app: taint
spec:
  replicas: 3
  selector:
    matchLabels:
      app: taint
  template:
    metadata:
      labels:
        app: taint
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - name: http
          containerPort: 80
      tolerations:
      - key: "node-role.kubernetes.io/master"
        operator: "Exists"
        effect: "NoSchedule"

由于 master 节点被标记为了污点,所以我们这里要想 Pod 能够调度到改节点去,就需要增加容忍的声明:

tolerations:
- key: "node-role.kubernetes.io/master"
  operator: "Exists"
  effect: "NoSchedule"
  • 然后创建上面的资源,查看结果:
➜ kubectl apply -f taint-demo.yaml
deployment.apps "taint" created

➜ kubectl get pods -o wide
NAME                                      READY     STATUS             RESTARTS   AGE       IP             NODE
......
taint-845d8bb4fb-57mhm                    1/1       Running            0          1m        10.244.4.247   node2
taint-845d8bb4fb-bbvmp                    1/1       Running            0          1m        10.244.0.33    master1
taint-845d8bb4fb-zb78x                    1/1       Running            0          1m        10.244.4.246   node2
......

我们可以看到有一个 Pod 副本被调度到了 master 节点,这就是容忍的使用方法。

那么问题来了:我这个pod可以调度到没打这个污点的node上去吗?还是说会优先调度到这个打了污点的节点上去呢?。。。

–>经测试:是可以调度上去的。pod配置了容忍之后,原来打了污点的node就可以和其他节点一样去负载pod了,具体调度策略是看调度器的。

  • 最后如果我们要取消节点的污点标记,可以使用下面的命令:
➜ kubectl taint nodes node2 test-
node "node2" untainted

实验结束。😘

🍂

Taints和Tolerations用于保证Pod 不被调度到不合适的Node上,其中Taint应用于Node上,而Toleration则应用于Pod上。

🍂 nodeSelector/nodeAffinity与Taint/Tolerations区别

nodeSelector/nodeAffinity--一种主观意识,我要分配在哪个node节点上去;
而Taint/Tolerations:是一种排斥行为,在node上排斥pod分配到自己身上;

nodeSelector/nodeAffinity--管理员强迫node
taint/toleration--node主动的一脸嫌弃

🍂

k8s的自愈能力/故障转移能力:–>污点和容忍!

🍂 tolerations 属性的写法

对于 tolerations 属性的写法,其中的 key、value、effect 与 Node 的 Taint 设置需保持一致, 还有以下几点说明:

  • 如果 operator 的值是 Exists,则 value 属性可省略
  • 如果 operator 的值是 Equal,则表示其 key 与 value 之间的关系是 equal(等于)
  • 如果不指定 operator 属性,则默认值为 Equal

另外,还有两个特殊值:

  • 空的 key 如果再配合 Exists 就能匹配所有的 key 与 value,也就是是能容忍所有节点的所有 Taints !!!😋

  • 空的 effect 匹配所有的 effect

例子:

master节点上,为什么除了静态pod之外,上面还有calico/kube-proxy组件可以运行。

为什么呢?原因在这里:污点容忍是可以直接放宽条件写的,例如calico,不分什么key什么value,符合带有污点是NoSchedule都可以允许分配。

image-20220310074709618

image-20220310075310952

image-20220310075518987

image-20220310075606692

4、nodeName

nodeName:指定节点名称,用于将Pod调度到指定的Node上,不经过调度器 (nodeName指哪打哪😂,nodeName一般在测试中用,但工作中根本没怎么用到)

💘 实践:nodeName测试(测试成功)-2022.5.16
  • 创建pod的yaml并修改
[root@k8s-master ~]#kubectl run pod6 --image=nginx --dry-run=client -o yaml > pod6.yaml
[root@k8s-master ~]#vim pod6.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod-example
  labels:
    app: nginx
spec:
  nodeName: k8s-node1
  containers:
  - name: nginx
    image: nginx:1.15 
  • 注意:原来k8s-node1节点是已经打好污点了的。
[root@k8s-master ~]#kubectl describe nodes |grep Taint
Taints:             node-role.kubernetes.io/master:NoSchedule
Taints:             ssd=ok:NoSchedule
Taints:             <none>
[root@k8s-master ~]#kubectl describe nodes k8s-node1 |grep Taint
Taints:             ssd=ok:NoSchedule
[root@k8s-master ~]#
  • apply并查看效果
[root@k8s-master ~]#kubectl apply -f pod6.yaml
pod/pod6 created
[root@k8s-master ~]#kubectl get pod -o wide

image-20210618062414511

  • 结论

如果在pod里指定了nodeName字段,那么即使该pod没有配置污点容忍,也会被强制开通的指定的节点上去的。

测试结束。😘

5、优先级调度

与前面所讲的**调度优选策略中的优先级(Priorities)**不同,前面所讲的优先级指的是节点优先级,而我们这里所说的优先级指的是 Pod 的优先级,高优先级的 Pod 会优先被调度,或者在资源不足的情况牺牲低优先级的 Pod,以便于重要的 Pod 能够得到资源部署。

🍂 要定义 Pod 优先级,就需要先定义 PriorityClass 对象,该对象没有 Namespace 的限制:

apiVersion: v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000000
globalDefault: false
description: "This priority class should be used for XYZ service pods only."

其中:

  • value 为 32 位整数的优先级,该值越大,优先级越高
  • globalDefault 用于未配置 PriorityClassName 的 Pod,整个集群中应该只有一个 PriorityClass 将其设置为 true

🍂 然后通过在 Pod 的 spec.priorityClassName 中指定已定义的 PriorityClass 名称即可:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  containers:
  - name: nginx
    image: nginx
    imagePullPolicy: IfNotPresent
  priorityClassName: high-priority

另外一个值得注意的是当节点没有足够的资源供调度器调度 Pod,导致 Pod 处于 pending 时,抢占(preemption)逻辑就会被触发,抢占会尝试从一个节点删除低优先级的 Pod,从而释放资源使高优先级的 Pod 得到节点资源进行部署。

6、QOS

1.服务质量

QoSQuality of Service 的缩写,即服务质量,为了实现资源被有效调度和分配的同时提高资源利用率,Kubernetes 针对不同服务质量的预期,通过 QoS 来对 pod 进行服务质量管理。对于一个 pod 来说,服务质量体现在两个具体的指标:CPU 和内存。当节点上内存资源紧张时,Kubernetes 会根据预先设置的不同 QoS 类别进行相应处理。

2.资源限制

如果未做过节点 nodeSelector、亲和性(node affinity)或 pod 亲和、反亲和性等高级调度策略设置,我们没有办法指定服务部署到指定节点上,这样就可能会造成 CPU 或内存等密集型的 pod 同时分配到相同节点上,造成资源竞争。另一方面,如果未对资源进行限制,一些关键的服务可能会因为资源竞争因 OOM 等原因被 kill 掉,或者被限制 CPU 使用。

我们知道对于每一个资源,container 可以指定具体的资源需求(requests)和限制(limits),requests 申请范围是0到节点的最大配置,而 limits 申请范围是 requests 到无限,即 0 <= requests <= Node Allocatable, requests <= limits <= Infinity

对于 CPU,如果 pod 中服务使用的 CPU 超过设置的 limits,pod 不会被 kill 掉但会被限制,因为 CPU 是可压缩资源,如果没有设置 limits,pod 可以使用全部空闲的 CPU 资源。

对于内存,当一个 pod 使用内存超过了设置的 limits,pod 中容器的进程会被 kernel 因 OOM kill 掉,当 container 因为 OOM 被 kill 掉时,系统倾向于在其原所在的机器上重启该 container 或本机或其他重新创建一个 pod。(包括我们的磁盘也是一个不可压缩资源)

🍂 关键是你作为运维,你要大概清楚每个服务消耗资源的情况

我们知道,tomcat一般是比较耗费资源的,起码需要1c以上;(java应用特别消耗cpu资源、内存资源)

如果你的pod里的requests配置为0.5c,而limits就配置为了0.8c,那么有可能你的容器都启动不起来;

不像nginx,你给它个100m,200m,它都能起来

🍂 主机cpu资源利用率满时,机器出现的现象–>卡顿

问题:默认情况下,宿主机和容器都没做资源限制的话,容器理论上是可以无限制获取到主机的计算资源(如cpu和内存)。如果某个容器应用特别耗cpu,当它把主机cpu耗尽时(资源利用率超高,达到98,99%以上),问主机会出现什么现象?

答:会出现卡顿现象,执行一条命令,老半天才出来,像老年痴呆一样;

是因为cpu是按时间片来分配的,cpu的分配原则:为了尽可能地公平地给大家一个分配,但cpu没有特别硬性的限制

🍂 总结

1、宿主机只有2c,这里的limits可以设置为100c,但是毫无意义,只是可以这样设置。limits是最大可用资源,一般是requests的20%左右,不太超出太多,否则limits限制就没多少意义了;
2、limits不能小于requests;
3、reqeusts只是一个预留性质,不是pod配置写多少,宿主机就会占用多少资源;
4、k8s根据request来统计每个节点预分配资源,来判断下一个pod能不能分配我这个节点;
5、所以,我们工作中,一定要配置requests和limits,来解决资源争抢问题;
6、limits不配置的话,默认和requests配置一样;

3.QoS 分类

**Kubelet 提供 QoS 服务质量管理,支持系统级别的 OOM 控制。**在 Kubernetes 中,QoS 主要分为 GuaranteedBurstableBest-Effort 三类,优先级从高到低。

QoS 分类并不是通过一个配置项来直接配置的,而是通过配置 CPU/内存的 limits 与 requests 值的大小来确认服务质量等级的,我们通过使用 kubectl get pod xxx -o yaml 可以看到 pod 的配置输出中有 qosClass 一项,该配置的作用是为了给资源调度提供策略支持,调度算法根据不同的服务质量等级可以确定将 pod 调度到哪些节点上。

1、Guaranteed(有保证的)

系统用完了全部内存,且没有其他类型的容器可以被 kill 时,该类型的 pods 会被 kill 掉,也就是说最后才会被考虑 kill 掉,属于该级别的 pod 有以下两种情况:

  • pod 中的所有容器都且仅设置了 CPU 和内存的 limits
  • pod 中的所有容器都设置了 CPU 和内存的 requests 和 limits ,且单个容器内的 requests==limits(requests不等于0)

1️⃣ pod 中的所有容器都且仅设置了 limits:

containers:
  name: foo
    resources:
      limits:
        cpu: 10m
        memory: 1Gi
  name: bar
    resources:
      limits:
        cpu: 100m
        memory: 100Mi

因为如果一个容器只指明 limit 而未设定 requests,则 requests 的值等于 limit 值,所以上面 pod 的 QoS 级别属于 Guaranteed。

⚠️ 另外需要注意若容器指定了 requests 而未指定 limits,则 limits 的值等于节点资源的最大值;若容器指定了 limits 而未指定 requests,则 requests 的值等于 limits。

2️⃣ 另外一个就是 pod 中的所有容器都明确设置了 requests 和 limits,且单个容器内的 requests==limits

containers:
  name: foo
    resources:
      limits:
        cpu: 10m
        memory: 1Gi
      requests:
        cpu: 10m
        memory: 1Gi
  name: bar
    resources:
      limits:
        cpu: 100m
        memory: 100Mi
      requests:
        cpu: 100m
        memory: 100Mi

容器 foo 和 bar 内 resources 的 requests 和 limits 均相等,该 pod 的 QoS 级别属于 Guaranteed。

🍂 CPU单位换算

容器资源限制:
• resources.limits.cpu
• resources.limits.memory

容器使用的最小资源需求,作为容器调度时资源分配的依据:
• resources.requests.cpu
• resources.requests.memory

CPU单位:可以写m也可以写浮点数,例如0.5=500m,1=1000m
CPU单位:m 毫核,1核=1000m=1,2核=2000m=2,0.5核=500m=0.5,0.2核=200m=0.2
apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  containers:
    - name: web
      image: nginx
      resources:
        requests:
          memory: "64Mi" #特别注意:内存的单位是Mi,cpu的单位是m
          cpu: "250m"
        limits:
          memory: "128Mi" 
          cpu: "500m"
#K8s会根据Request的值去查找有足够资源的Node来调度此Pod
2、Burstable(不稳定的)

系统用完了全部内存,且没有 Best-Effort 类型的容器可以被 kill 时,该类型的 pods 会被 kill 掉。pod 中只要有一个容器的 requests 和 limits 的设置不相同,该 pod 的 QoS 即为 Burstable。

1️⃣ 比如容器 foo 指定了 resource,而容器 bar 未指定:

containers:
  name: foo
    resources:
      limits:
        cpu: 10m
        memory: 1Gi
      requests:
        cpu: 10m
        memory: 1Gi
  name: bar

2️⃣ 或者容器 foo 设置了内存 limits,而容器 bar 设置了 CPU limits:

containers:
  name: foo
    resources:
      limits:
        memory: 1Gi
  name: bar
    resources:
      limits:
        cpu: 100m

上面两种情况定义的 pod 都属于 Burstable 类别的 QoS。

3、Best-Effort(尽最大努力)

系统用完了全部内存时,该类型 pods 会最先被 kill 掉。如果 pod 中所有容器的 resources 均未设置 requests 与 limits,那么该 pod 的 QoS 即为 Best-Effort。

1️⃣ 比如容器 foo 和容器 bar 均未设置 requests 和 limits:

containers:
  name: foo
    resources:
  name: bar
    resources:
资源限制对Pod调度的影响
💘 实践:resources.requests测试(测试成功)-2022.5.17
  • 创建yaml并修改
[root@k8s-master ~]#kubectl run pod6 --image=nginx --dry-run=client -o yaml > resources.yaml
root@k8s-master ~]#vim resources.yaml
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: pod6
  name: pod6
spec:
  containers:
  - image: nginx
    name: pod6
    resources:
      #最小资源
      cpu: 500m
      memory: 512Mi
  • 部署并查看
[root@k8s-master ~]#kubectl apply  -f resources.yaml
[root@k8s-master ~]#kubectl get pod

image-20210616063348641

  • 拷贝刚才创建的那个yaml文件并修改配置
[root@k8s-master ~]#cp resources.yaml resources2.yaml
[root@k8s-master ~]#vim resources2.yaml
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: resource-2
  name: resource-2
spec:
  containers:
  - image: nginx
    name: resource-2
    resources:
      #最小资源
      requests:
        cpu: 1500m
        memory: 512m #这里的单位写错了,应该是Mi才对的。。。
  • 部署并查看,resource-2也起来了:

image-20210616063948332

  • 再次拷贝刚才创建的那个yaml文件并修改配置
[root@k8s-master ~]#cp resources2.yaml resources3.yaml
[root@k8s-master ~]#vim resources3.yaml
[root@k8s-master ~]#kubectl apply -f resources3.yaml
pod/resource-3 created
[root@k8s-master ~]#

image-20210616064233338

部署并查看效果:

此时发现最后创建的那个pod 一直是pending状态:

image-20210616064551464

  • 为什么呢?我们查看一下它的描述:

image-20210616064727484

是因为node节点的cpu不足,导致pod调度失败的

sufficient adj.充足的,足够的 insufficient adj.不充足的,不足够的

  • 自己机器计算配置大小:

image-20210616072559721

查看宿主机的资源使用率情况:

[root@k8s-master ~]#kubectl describe node  k8s-node1

可分配的:

image-20210616072242236

image-20210616072411357

image-20210616073215481

  • 此时,node1可用cpu容量为:8%m node2可用总量为:63%m

而此时resource3.yaml里的pod需求为1900m,很明显以上2个node均无法满足其需求,因此和这个pod将无法被分配,一直处于pending状态

  • 总结

K8s会根据Request的值去查找有足够资源的Node来调度此Pod,这边看的就是这里的值。

默认你不配置这个requests的值,这里resuest值就是0,即这个pod可以完全使用宿主机所有的cpu 资源,各个pod可能会出现争抢cpu现象

实验结束。😘

💘 实践:resources.limits测试(测试成功)-2022.5.17
  • 创建yaml并修改
[root@k8s-master ~]#cp resources3.yaml resources4.yaml
[root@k8s-master ~]#vim resources4.yaml
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: resource-4
  name: resource-4
spec:
  containers:
  - image: nginx
    name: resource-4
    resources:
      #最小资源
      requests:
        cpu: 500m
        memory: 512Mi
      #最大资源,一般是requests 20%左右
      limits:
        cpu: 600m
        memory: 612Mi

最大资源,一般是requests 20%左右

  • apply下并查看
[root@k8s-master ~]#kubectl apply -f resources4.yaml #这个是不会用影响调度的
pod/resource-4 created
[root@k8s-master ~]#kubectl get pod
NAME                  READY   STATUS    RESTARTS   AGE
pod4                  1/1     Running   0          33h
pod6                  1/1     Running   0          81m
resource-2            1/1     Running   0          75m
resource-3            0/1     Pending   0          70m
resource-4            1/1     Running   0          6s #已成功启动pod
web-96d5df5c8-wwc96   1/1     Running   0          41h
[root@k8s-master ~]#
  • pod 的最大使用上线是可以查看的
[root@k8s-master ~]#kubectl describe pod resource-4

image-20210616075703778

  • 我们再次测试下:把limits对应值改大,会不会影响pod开通?=>不会。
[root@k8s-master ~]#cp resources4.yaml resources5.yaml
[root@k8s-master ~]#vim resources5.yaml
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: resource-5
  name: resource-5
spec:
  containers:
  - image: nginx
    name: resource-5
    resources:
      #最小资源
      requests:
        cpu: 500m
        memory: 512Mi
      #最大资源,一般是requests 20%左右
      limits:
        cpu: 1900m #把这个改成1900m,看会不会影响它开通
        memory: 612Mi
[root@k8s-master ~]#kubectl apply -f resources5.yaml
pod/resource-5 created
[root@k8s-master ~]#kubectl get pod
NAME                  READY   STATUS    RESTARTS   AGE
pod4                  1/1     Running   0          33h
pod6                  1/1     Running   0          88m
resource-2            1/1     Running   0          81m
resource-3            0/1     Pending   0          77m
resource-4            1/1     Running   0          6m45s
resource-5            1/1     Running   0          6s #可正常
web-96d5df5c8-wwc96   1/1     Running   0          41h
[root@k8s-master ~]#

是不会受影响的,就可以正常开通。

  • 因此,最终yaml文件标准输出:
[root@k8s-master ~]#vim resources5.yaml
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: resource-5
  name: resource-5
spec:
  containers:
  - image: nginx
    name: resource-5
    resources:
      #最小资源
      requests:
        cpu: 500m
        memory: 512Mi
      #最大资源,一般是requests 20%左右
      limits:
        cpu: 600m
        memory: 612Mi

实验结束。😘

4.QoS 解析

**首先我们要明确在调度时调度器只会根据 requests 值进行调度。**当系统 OOM 上时对于处理不同 OOMScore 的进程表现不同,OOMScore 是针对 memory 的,当宿主上 memory 不足时系统会优先 kill 掉 OOMScore 值高的进程,可以使用 cat /proc/$PID/oom_score 命令查看进程的 OOMScore。OOMScore 的取值范围为 [-1000, 1000],Guaranteed 类型的 pod 的默认值为 -998,Burstable pod 的值为 2~999,BestEffort pod 的值为 1000。也就是说当系统 OOM 时,首先会 kill 掉 BestEffort pod 的进程,若系统依然处于 OOM 状态,然后才会 kill 掉 Burstable pod,最后是 Guaranteed pod。

Kubernetes 是通过 cgroup 给 pod 设置 QoS 级别的,kubelet 中有一个 --cgroups-per-qos 参数(默认启用),启用后 kubelet 会为不同 QoS 创建对应的 level cgroups,在 Qos level cgroups 下也会为 pod 下的容器创建对应的 level cgroups,从 Qos –> pod –> container,层层限制每个 level cgroups 的资源使用量。

由于我们这里使用的是 containerd 这种容器运行时,则 cgroup 的路径与之前的 docker 不太一样:

  • Guaranteed 类型的 cgroup level 会直接创建在 RootCgroup/system.slice/containerd.service/kubepods-pod<uid>.slice:cri-containerd:<container-id>
  • Burstable 的创建在 RootCgroup/system.slice/containerd.service/kubepods-burstable-pod<uid>.slice:cri-containerd:<container-id>
  • BestEffort 类型的创建在 RootCgroup/system.slice/containerd.service/kubepods-besteffort-pod<uid>.slice:cri-containerd:<container-id>

⚠️ 这里的路径和底层容器运行时及是使用systemd还是cgroupfs都是不一样的。

注意:我下面的实验路径和老师的是有出入来着,但是总体问题不大;
一个pause容器+主容器;

image-20220220102223206

image-20220220152022806

💘 实践:Qos解析(测试成功)-2022.5.17
  • 我们可以通过 mount | grep cgroup 命令查看 RootCgroup:
mount | grep cgroup
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct,cpu)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio,net_cls)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)

在 cgroup 的每个子系统下都会创建 QoS level cgroups, 此外在对应的 QoS level cgroups 还会为 pod 创建 Pod level cgroups。

  • 比如我们创建一个如下所示的 Pod:
# 01-qos-demo.yaml
apiVersion: v1
kind: Pod
metadata:
  name: qos-demo
spec:
  containers:
  - name: nginx
    image: nginx:latest
    resources:
      requests:
        cpu: 250m
        memory: 1Gi
      limits:
        cpu: 500m
        memory: 2Gi

直接创建上面的资源对象即可:

$ kubectl apply -f 01-qos-demo.yaml 
pod/qos-demo created
$ kubectl get po qos-demo -owide
NAME       READY   STATUS    RESTARTS   AGE   IP             NODE    NOMINATED NODE   READINESS GATES
qos-demo   1/1     Running   0          27s   10.244.1.241   node1   <none>           <none>
$ kubectl get po qos-demo -owide -oyaml|grep qosClass
  qosClass: Burstable
$ kubectl get po qos-demo -owide -oyaml|grep uid
  uid: 562d098f-8346-4e8f-b23d-9d8c24b75b6a

由于该 pod 的设置的资源 requests != limits,所以其属于 Burstable 类别的 pod。

kubelet 会在其所属 QoS 下创建 RootCgroup/system.slice/containerd.service/kubepods-burstable-pod<uid>.slice:cri-containerd:<container-id> 这个 cgroup level,比如我们查看内存这个子系统的 cgroup:

# 还有一个 pause 容器的 cgroup levells /sys/fs/cgroup/memory/system.slice/containerd.service/kubepods-burstable-pod489a19f2_8d75_474c_988f_5854b61b839f.slice:cri-containerd:4782243ba3260125513af20689fcea31b52eae1cbabeafeb1f7a52bcdcd5b44b
cgroup.clone_children           memory.kmem.tcp.max_usage_in_bytes  memory.oom_control
cgroup.event_control            memory.kmem.tcp.usage_in_bytes      memory.pressure_level
cgroup.procs                    memory.kmem.usage_in_bytes          memory.soft_limit_in_bytes
memory.failcnt                  memory.limit_in_bytes               memory.stat
memory.force_empty              memory.max_usage_in_bytes           memory.swappiness
memory.kmem.failcnt             memory.memsw.failcnt                memory.usage_in_bytes
memory.kmem.limit_in_bytes      memory.memsw.limit_in_bytes         memory.use_hierarchy
memory.kmem.max_usage_in_bytes  memory.memsw.max_usage_in_bytes     notify_on_release
memory.kmem.slabinfo            memory.memsw.usage_in_bytes         tasks
memory.kmem.tcp.failcnt         memory.move_charge_at_immigrate
memory.kmem.tcp.limit_in_bytes  memory.numa_stat

上面创建的应用容器进程 ID 会被写入到上面的 tasks 文件中:

cat tasks
64133
64170
64171
64172
64173ps -aux |grep nginx
root      64133  0.0  0.0   8840  3488 ?        Ss   15:56   0:00 nginx: master process nginx -g daemon off;
101       64170  0.0  0.0   9228  1532 ?        S    15:56   0:00 nginx: worker process
101       64171  0.0  0.0   9228  1532 ?        S    15:56   0:00 nginx: worker process
101       64172  0.0  0.0   9228  1532 ?        S    15:56   0:00 nginx: worker process
101       64173  0.0  0.0   9228  1532 ?        S    15:56   0:00 nginx: worker process

这样我们的容器进程就会受到该 cgroup 的限制了,在 pod 的资源清单中我们设置了 memory 的 limits 值为 2Gi,kubelet 则会将该限制值写入到 memory.limit_in_bytes 中去:

cat memory.limit_in_bytes
2147483648 # 2147483648 / 1024 / 1024 / 1024 = 2
  • 同样对于 cpu 资源一样可以在对应的子系统中找到创建的对应 cgroup:
ls /sys/fs/cgroup/cpu/system.slice/containerd.service/kubepods-burstable-pod489a19f2_8d75_474c_988f_5854b61b839f.slice:cri-containerd:4782243ba3260125513af20689fcea31b52eae1cbabeafeb1f7a52bcdcd5b44b
cgroup.clone_children  cpuacct.stat          cpu.cfs_period_us  cpu.rt_runtime_us  notify_on_release
cgroup.event_control   cpuacct.usage         cpu.cfs_quota_us   cpu.shares         tasks
cgroup.procs           cpuacct.usage_percpu  cpu.rt_period_us   cpu.stat
➜ cat tasks
64133
64170
64171
64172
64173cat cpu.cfs_quota_us
50000  # 500m

⚠️ 最后关于 QoS 还有一点建议:

如果资源充足,可将 QoS pods 类型均设置为 Guaranteed。用计算资源换业务性能和稳定性,减少排查问题时间和成本。

如果想更好的提高资源利用率,业务服务可以设置为 Guaranteed,而其他服务根据重要程度可分别设置为 Burstable 或 Best-Effort。

实验结束。😘

7、拓扑分布约束

image-20220218152232851

1.概念

⚠️
这个理解起来稍微有些复杂;
这个可能用的不多,但如果你想进行一些细粒度的控制的话,用这个还是很不错的;

在 k8s 集群调度中,亲和性相关的概念本质上都是控制 Pod 如何被调度 – 堆叠或打散

podAffinity 以及 podAntiAffinity 两个特性对 Pod 在不同拓扑域的分布进行了一些控制。

podAffinity 可以将无数个 Pod 调度到特定的某一个拓扑域,这是堆叠的体现;

podAntiAffinity 则可以控制一个拓扑域只存在一个 Pod,这是打散的体现;

但这两种情况都太极端了,在不少场景下都无法达到理想的效果,例如为了实现容灾和高可用,将业务 Pod 尽可能均匀的分布在不同可用区就很难实现。

PodTopologySpread(Pod 拓扑分布约束) 特性的提出正是为了对 Pod 的调度分布提供更精细的控制,以提高服务可用性以及资源利用率。PodTopologySpreadEvenPodsSpread 特性门所控制,在 v1.16 版本第一次发布,并在 v1.18 版本进入 beta 阶段默认启用。

2.使用规范

在 Pod 的 Spec 规范中新增了一个 topologySpreadConstraints 字段即可配置拓扑分布约束,如下所示:

spec:
  topologySpreadConstraints:
  - maxSkew: <integer> #这个属性理解起来不是那么地直接。。。;倾斜度;
    topologyKey: <string>
    whenUnsatisfiable: <string>
    labelSelector: <object>

由于这个新增的字段是在 Pod spec 层面添加,因此更高层级的控制 (Deployment、DaemonSet、StatefulSet) 也能使用 PodTopologySpread 功能。

Pod拓扑分布约束

让我们结合上图来理解 topologySpreadConstraints 中各个字段的含义和作用:

  • labelSelector: 用来查找匹配的 Pod,我们能够计算出每个拓扑域中匹配该 label selector 的 Pod 数量,在上图中,假如 label selector 是 app:foo,那么 zone1 的匹配个数为 2, zone2 的匹配个数为 0。
  • topologyKey: 是 Node label 的 key,如果两个 Node 的 label 同时具有该 key 并且值相同,就说它们在同一个拓扑域。在上图中,指定 topologyKey 为 zone, 则具有 zone=zone1 标签的 Node 被分在一个拓扑域,具有 zone=zone2 标签的 Node 被分在另一个拓扑域。
  • ⚠️ maxSkew: 这个属性理解起来不是很直接,它描述了 Pod在不同拓扑域中不均匀分布的最大程度(指定拓扑类型中任意两个拓扑域中匹配的 Pod 之间的最大允许差值),它必须大于零每个拓扑域都有一个 skew 值,计算的公式是:skew[i] = 拓扑域[i]中匹配的 Pod 个数 - min{其他拓扑域中匹配的 Pod 个数}。在上图中,我们新建一个带有app=foo标签的 Pod:
    • 如果该 Pod 被调度到 zone1,那么 zone1 中 Node 的 skew 值变为 3,zone2 中 Node 的 skew 值变为 0 (zone1 有 3 个匹配的 Pod,zone2 有 0 个匹配的 Pod )
    • 如果该 Pod 被调度到 zone2,那么 zone1 中 Node 的 skew 值变为 2,zone2 中 Node 的 skew 值变为 1(zone2 有 1 个匹配的 Pod,拥有全局最小匹配 Pod 数的拓扑域正是 zone2 自己 ),则它满足maxSkew: 1 的约束(差值为1)
  • whenUnsatisfiable: 描述了如果 Pod 不满足分布约束条件该采取何种策略:
    • DoNotSchedule (默认) 告诉调度器不要调度该 Pod,因此也可以叫作硬策略
    • ScheduleAnyway 告诉调度器根据每个 Node 的 skew 值打分排序后仍然调度,因此也可以叫作软策略

3.单个拓扑约束

假设你拥有一个 4 节点集群,其中标记为 foo:bar 的 3 个 Pod 分别位于 node1、node2 和 node3 中:

单个 TopologySpreadConstraint

如果希望新来的 Pod 均匀分布在现有的可用区域,则可以按如下设置其约束:

kind: Pod
apiVersion: v1
metadata:
  name: mypod
  labels:
    foo: bar
spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        foo: bar
  containers:
  - name: pause
    image: k8s.gcr.io/pause:3.1

topologyKey: zone 意味着均匀分布将只应用于存在标签键值对为 zone:<any value> 的节点。 whenUnsatisfiable: DoNotSchedule 告诉调度器如果新的 Pod 不满足约束,则不可调度。如果调度器将新的 Pod 放入 “zoneA”,Pods 分布将变为 [3, 1],因此实际的偏差为 2(3 - 1),这违反了 maxSkew: 1 的约定。此示例中,新 Pod 只能放置在 “zoneB” 上:

zoneB

或者

zoneB

你可以调整 Pod 约束以满足各种要求:

  • maxSkew 更改为更大的值,比如 “2”,这样新的 Pod 也可以放在 “zoneA” 上。
  • topologyKey 更改为 “node”,以便将 Pod 均匀分布在节点上而不是区域中。 在上面的例子中,如果 maxSkew 保持为 “1”,那么传入的 Pod 只能放在 “node4” 上。
  • whenUnsatisfiable: DoNotSchedule 更改为 whenUnsatisfiable: ScheduleAnyway, 以确保新的 Pod 可以被调度。

4.多个拓扑约束

上面是单个 Pod 拓扑分布约束的情况,下面的例子建立在前面例子的基础上来对多个 Pod 拓扑分布约束进行说明。假设你拥有一个 4 节点集群,其中 3 个标记为 foo:bar 的 Pod 分别位于 node1、node2 和 node3 上:

多个 TopologySpreadConstraint

我们可以使用 2 个 TopologySpreadConstraint 来控制 Pod 在区域和节点两个维度上的分布:

# two-constraints.yaml
kind: Pod
apiVersion: v1
metadata:
  name: mypod
  labels:
    foo: bar
spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        foo: bar
  - maxSkew: 1
    topologyKey: node
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        foo: bar
  containers:
  - name: pause
    image: k8s.gcr.io/pause:3.1

在这种情况下,为了匹配第一个约束,新的 Pod 只能放置在 “zoneB” 中;而在第二个约束中, 新的 Pod 只能放置在 “node4” 上,最后两个约束的结果加在一起,唯一可行的选择是放置 在 “node4” 上。

🍂 多个约束之间是可能存在冲突的,假设有一个跨越 2 个区域的 3 节点集群:

冲突

如果对集群应用 two-constraints.yaml,会发现 “mypod” 处于 Pending 状态,这是因为为了满足第一个约束,“mypod” 只能放在 “zoneB” 中,而第二个约束要求 “mypod” 只能放在 “node2” 上,Pod 调度无法满足这两种约束,所以就冲突了。

为了克服这种情况,你可以增加 maxSkew 或修改其中一个约束,让其使用 whenUnsatisfiable: ScheduleAnyway

5.与NodeSelector /NodeAffinity一起使用

仔细观察可能你会发现我们并没有类似于 topologyValues 的字段来限制 Pod 将被调度到哪些拓扑去,默认情况会搜索所有节点并按 topologyKey 对其进行分组。有时这可能不是理想的情况,比如假设有一个集群,其节点标记为 env=prodenv=stagingenv=qa,现在你想跨区域将 Pod 均匀地放置到 qa 环境中,是否可行?

答案是肯定的,我们可以结合 NodeSelectorNodeAffinity 一起使用,PodTopologySpread 会计算满足选择器的节点之间的传播约束。

高级用法-1

如上图所示我们可以通过指定 spec.affinity.nodeAffinity搜索范围限制为 qa 环境,在该范围内 Pod 将被调度到一个满足 topologySpreadConstraints 的区域,这里就只能被调度到 zone=zone2 的节点上去了。

6.集群默认约束

除了为单个 Pod 设置拓扑分布约束,也可以为集群设置默认的拓扑分布约束,默认拓扑分布约束在且仅在以下条件满足 时才会应用到 Pod 上:

  • Pod 没有在其 .spec.topologySpreadConstraints 设置任何约束;
  • Pod 隶属于某个服务、副本控制器、ReplicaSet 或 StatefulSet。

你可以在 调度方案(Schedulingg Profile)中将默认约束作为 PodTopologySpread 插件参数的一部分来进行设置。 约束的设置采用和前面 Pod 中的规范一致,只是 labelSelector 必须为空。配置的示例可能看起来像下面这个样子:

apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration

profiles:
  - pluginConfig:
      - name: PodTopologySpread
        args:
          defaultConstraints:
            - maxSkew: 1
              topologyKey: topology.kubernetes.io/zone
              whenUnsatisfiable: ScheduleAnyway
          defaultingType: List

思考1:不用 DaemonSet,如何使用 Deployment 是否实现同样的功能?

我们知道 DaemonSet 控制器的功能就是在每个节点上运行一个 Pod,如何要使用 Deployment 来实现,首先就要设置副本数量为节点数,比如我们这里加上 master 节点一共3个节点,则要设置3个副本,要在 master 节点上执行自然要添加容忍,那么要如何保证一个节点上只运行一个 Pod 呢?是不是前面的提到的 Pod 反亲和性就可以实现,以自己 Pod 的标签来进行过滤校验即可,新的 Pod 不能运行在一个已经具有该 Pod 的节点上,是不是就是一个节点只能运行一个?

  • 模拟的资源清单如下所示:
# 06-daemonset-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mock-ds-demo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: mock-ds-demo
  template:
    metadata:
      labels:
        app: mock-ds-demo
    spec:
      tolerations: #加上容忍,这样才有机会运行在master节点上
      - key: "node-role.kubernetes.io/master"
        operator: "Exists"
        effect: "NoSchedule"
      containers:
      - image: nginx
        name: nginx
        ports:
        - containerPort: 80
          name: ngpt
      affinity:
        podAntiAffinity:  # pod反亲合性
          requiredDuringSchedulingIgnoredDuringExecution:  # 硬策略
          - labelSelector:
              matchExpressions:
              - key: app   # Pod的标签
                operator: In
                values: ["mock-ds-demo"]
            topologyKey: kubernetes.io/hostname  # 以hostname为拓扑域,相当于每一个节点都是一个拓扑域
  • 创建上面的资源清单验证:
$ kubectl apply -f 06-daemonset-deployment.yaml 
deployment.apps/mock-ds-demo created
$ kubectl get po -owide
NAME                            READY   STATUS    RESTARTS   AGE   IP             NODE      NOMINATED NODE   READINESS GATES
mock-ds-demo-8694759c69-882fx   1/1     Running   0          18s   10.244.0.15    master1   <none>           <none>
mock-ds-demo-8694759c69-frfhx   1/1     Running   0          18s   10.244.1.108   node1     <none>           <none>
mock-ds-demo-8694759c69-p5cjw   1/1     Running   0          18s   10.244.2.235   node2     <none>           <none>

可以看到我们用 Deployment 部署的服务在每个节点上都运行了一个 Pod,实现的效果和 DaemonSet 是一致的。

思考2:如果想在每个节点(或指定的一些节点)上运行2个(或多个)Pod 副本呢?

🍂 同样的如果想在每个节点(或指定的一些节点)上运行2个(或多个)Pod 副本,如何实现?

DaemonSet 是在每个节点上运行1个 Pod 副本,显然我们去创建2个(或多个)DaemonSet 即可实现该目标,但是这不是一个好的接近方案,PodAntiAffinity 只能将一个 Pod 调度到某个拓扑域中去,所以都不能很好的来解决这个问题。

要实现这种更细粒度的控制,我们可以通过设置拓扑分布约束来进行调度,设置拓扑分布约束来将 Pod 分布到不同的拓扑域下,从而实现高可用性或节省成本,具体实现方式请看下文。

💘 实践:用拓扑分布约束来扩展daemonset功能(测试成功)-2022.5.17

现在我们再去解决上节课留下的一个问题 - 如果想在每个节点(或指定的一些节点)上运行2个(或多个)Pod 副本,如何实现?

  • 这里以我们的集群为例,加上 master 节点一共有3个节点,每个节点运行2个副本,总共就需要6个 Pod 副本,要在 master 节点上运行,则同样需要添加容忍,如果只想在一个节点上运行2个副本,则可以使用我们的拓扑分布约束来进行细粒度控制,对应的资源清单如下所示:
#01-daemonset2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: topo-demo
spec:
  replicas: 6
  selector:
    matchLabels:
      app: topo
  template:
    metadata:
      labels:
        app: topo
    spec:
      tolerations:
      - key: "node-role.kubernetes.io/master"
        operator: "Exists"
        effect: "NoSchedule"
      containers:
      - image: nginx
        name: nginx
        ports:
        - containerPort: 80
          name: ngpt
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: kubernetes.io/hostname
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels:
            app: topo

这里我们重点需要关注的就是 topologySpreadConstraints 部分的配置,我们选择使用 kubernetes.io/hostname 为拓扑域,相当于就是3个节点都是独立的,maxSkew: 1 表示最大的分布不均匀度为1,所以只能出现的调度结果就是每个节点运行2个 Pod。

maxSkew=2这个相当于是软策略,不可取。

image-20220219104104873

maxSkew=1这个满足需求。

maxSkew=1,这个是一轮轮往下调度的;(那么一般情况,maxSkew=1应该是比较常用的)

解析

  • 直接创建上面的资源即可验证:
$ kubectl apply -f 01-daemonset2.yaml 
deployment.apps/topo-demo created
$ kubectl get po -l app=topo -owide
NAME                         READY   STATUS    RESTARTS   AGE   IP             NODE      NOMINATED NODE   READINESS GATES
topo-demo-6bbf65d967-ctf66   1/1     Running   0          43s   10.244.1.110   node1     <none>           <none>
topo-demo-6bbf65d967-gkqpx   1/1     Running   0          43s   10.244.0.16    master1   <none>           <none>
topo-demo-6bbf65d967-jsj4h   1/1     Running   0          43s   10.244.2.236   node2     <none>           <none>
topo-demo-6bbf65d967-kc9x8   1/1     Running   0          43s   10.244.2.237   node2     <none>           <none>
topo-demo-6bbf65d967-nr9b9   1/1     Running   0          43s   10.244.1.109   node1     <none>           <none>
topo-demo-6bbf65d967-wfzmf   1/1     Running   0          43s   10.244.0.17    master1   <none>           <none>

可以看到符合我们的预期,每个节点上运行了2个 Pod 副本。

  • 如果是要求每个节点上运行3个 Pod 副本呢?大家也可以尝试去练习下。
#02-daemonset.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: topo-demo2
spec:
  replicas: 9
  selector:
    matchLabels:
      app: topo2
  template:
    metadata:
      labels:
        app: topo2
    spec:
      tolerations:
      - key: "node-role.kubernetes.io/master"
        operator: "Exists"
        effect: "NoSchedule"
      containers:
      - image: nginx
        name: nginx
        ports:
        - containerPort: 80
          name: ngpt1
      topologySpreadConstraints:
      - maxSkew: 1
        topologyKey: kubernetes.io/hostname
        whenUnsatisfiable: DoNotSchedule
        labelSelector:
          matchLabels:
            app: topo2

查看现象:

hg@LAPTOP-G8TUFE0T:/mnt/c/Users/hg/Desktop/yaml/2022.2.19-40.拓扑分布约束-实验代码$ kubectl apply -f 02-daemonset.yaml 
deployment.apps/topo-demo2 created
hg@LAPTOP-G8TUFE0T:/mnt/c/Users/hg/Desktop/yaml/2022.2.19-40.拓扑分布约束-实验代码$ kubectl get po -l app=topo2 -owide
NAME                         READY   STATUS    RESTARTS   AGE   IP             NODE      NOMINATED NODE   READINESS GATES
topo-demo2-87d77dbbd-25wbw   1/1     Running   0          53s   10.244.2.240   node2     <none>           <none>
topo-demo2-87d77dbbd-77gm8   1/1     Running   0          53s   10.244.1.113   node1     <none>           <none>
topo-demo2-87d77dbbd-97npc   1/1     Running   0          53s   10.244.0.20    master1   <none>           <none>
topo-demo2-87d77dbbd-fspcq   1/1     Running   0          53s   10.244.1.111   node1     <none>           <none>
topo-demo2-87d77dbbd-k5llk   1/1     Running   0          53s   10.244.1.112   node1     <none>           <none>
topo-demo2-87d77dbbd-pc24x   1/1     Running   0          53s   10.244.2.238   node2     <none>           <none>
topo-demo2-87d77dbbd-rdmwn   1/1     Running   0          53s   10.244.2.239   node2     <none>           <none>
topo-demo2-87d77dbbd-s4rjp   1/1     Running   0          53s   10.244.0.18    master1   <none>           <none>
topo-demo2-87d77dbbd-xrwmk   1/1     Running   0          53s   10.244.0.19    master1   <none>           <none>

实验结束。😘

8、Descheduler(重调度器)

image-20220219161107422

1、Descheduler

从 kube-scheduler 的角度来看,它是通过一系列算法计算出最佳节点运行 Pod,当出现新的 Pod 进行调度时,调度程序会根据其当时对 Kubernetes 集群的资源描述做出最佳调度决定,但是 Kubernetes 集群是非常动态的,由于整个集群范围内的变化,比如一个节点为了维护,我们先执行了驱逐操作,这个节点上的所有 Pod 会被驱逐到其他节点去,但是当我们维护完成后,之前的 Pod 并不会自动回到该节点上来,因为 Pod 一旦被绑定了节点是不会触发重新调度的,由于这些变化,Kubernetes 集群在一段时间内就可能会出现不均衡的状态,所以需要均衡器来重新平衡集群。

当然我们可以去手动做一些集群的平衡,比如手动去删掉某些 Pod,触发重新调度就可以了,但是显然这是一个繁琐的过程,也不是解决问题的方式。为了解决实际运行中集群资源无法充分利用或浪费的问题,可以使用 descheduler 组件对集群的 Pod 进行调度优化,descheduler 可以根据一些规则和配置策略来帮助我们重新平衡集群状态,其核心原理是根据其策略配置找到可以被移除的 Pod 并驱逐它们,其本身并不会进行调度被驱逐的 Pod,而是依靠默认的调度器来实现,目前支持的策略有:

  • RemoveDuplicates
  • LowNodeUtilization
  • RemovePodsViolatingInterPodAntiAffinity
  • RemovePodsViolatingNodeAffinity
  • RemovePodsViolatingNodeTaints
  • RemovePodsViolatingTopologySpreadConstraint
  • RemovePodsHavingTooManyRestarts
  • PodLifeTime

这些策略都是可以启用或者禁用的,作为策略的一部分,也可以配置与策略相关的一些参数,默认情况下,所有策略都是启用的。另外,还有一些通用配置,如下:

  • nodeSelector:限制要处理的节点
  • evictLocalStoragePods: 驱逐使用 LocalStorage 的 Pods
  • ignorePvcPods: 是否忽略配置 PVC 的 Pods,默认是 False
  • maxNoOfPodsToEvictPerNode:节点允许的最大驱逐 Pods 数

我们可以通过如下所示的 DeschedulerPolicy 来配置:(这里是要用一个configmap来进行使用的。)

apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
nodeSelector: prod=dev
evictLocalStoragePods: true
maxNoOfPodsToEvictPerNode: 40
ignorePvcPods: false
strategies:  # 配置策略
  ...

🍂 kubernetes-sigs/descheduler官方网站

https://github.com/kubernetes-sigs/descheduler

🍂 重调度器

腾讯内部自研,水位线,干扰率

增加一个模拟调度,先模拟调度一下pod,如果能调度成功的话,就驱逐pod,如果模拟调度失败,就先不驱逐pod。

默认的descheduler可能存在一些问题的!

2、安装

💘 实践:descheduler安装(测试成功)-2022.5.17
  • descheduler 可以以 JobCronJob 或者 Deployment 的形式运行在 k8s 集群内,同样我们可以使用 Helm Chart 来安装 descheduler
[root@master1 ~]#helm repo add descheduler https://kubernetes-sigs.github.io/descheduler/
"descheduler" has been added to your repositories

[root@master1 ~]#helm fetch descheduler/descheduler
[root@master1 ~]#tar xf descheduler-0.23.1.tgz 

通过 Helm Chart 我们可以配置 deschedulerCronJob 或者 Deployment 方式运行。⚠️ 默认情况下 descheduler 会以一个 critical pod 运行,以避免被自己或者 kubelet 驱逐了,需要确保集群中有 system-cluster-critical 这个 Priorityclass:

[root@master1 ~]#kubectl get priorityclass system-cluster-critical
NAME                      VALUE        GLOBAL-DEFAULT   AGE
system-cluster-critical   2000000000   false            111d

⚠️ 注意:descheduler和k8s版本问题

https://github.com/kubernetes-sigs/descheduler

什么样的k8s集群版本就是用什么样的descheduler;

image-20220219223059197

  • 使用 Helm Chart 安装默认情况下会以 CronJob 的形式运行,执行周期为 schedule: "*/2 * * * *",这样每隔两分钟会执行一次 descheduler 任务,默认的配置策略如下所示:
apiVersion: v1
kind: ConfigMap
metadata:
  name: descheduler
data:
  policy.yaml: |
    apiVersion: "descheduler/v1alpha1"
    kind: "DeschedulerPolicy"
    strategies:
      LowNodeUtilization:
        enabled: true
        params:
          nodeResourceUtilizationThresholds:
            targetThresholds:
              cpu: 50
              memory: 50
              pods: 50
            thresholds:
              cpu: 20
              memory: 20
              pods: 20
      RemoveDuplicates:
        enabled: true
      RemovePodsViolatingInterPodAntiAffinity:
        enabled: true
      RemovePodsViolatingNodeAffinity:
        enabled: true
        params:
          nodeAffinityType:
          - requiredDuringSchedulingIgnoredDuringExecution
      RemovePodsViolatingNodeTaints:
        enabled: true
  • 通过配置 DeschedulerPolicystrategies,可以指定 descheduler 的执行策略,这些策略都是可以启用或禁用的。下面我们会详细介绍,这里我们使用默认策略即可,使用如下命令直接安装即可:
[root@master1 ~]#helm upgrade --install descheduler descheduler/descheduler --set image.repository=cnych/descheduler,podSecurityPolicy.create=false -n kube-system
Release "descheduler" does not exist. Installing it now.
NAME: descheduler
LAST DEPLOYED: Sat Feb 19 17:05:31 2022
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
NOTES:
Descheduler installed as a CronJob .   
  • 部署完成后会创建一个 CronJob 资源对象来平衡集群状态:
➜ kubectl get cronjob -n kube-system
NAME          SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
descheduler   */2 * * * *   False     1        27s             31s
➜ kubectl get job -n kube-system
NAME                   COMPLETIONS   DURATION   AGE
descheduler-27378876   1/1           72s        79s
➜ kubectl get pods -n kube-system -l job-name=descheduler-27378876
NAME                            READY   STATUS      RESTARTS   AGE
descheduler-27378876--1-btjmd   0/1     Completed   0          2m21s
  • 正常情况下就会创建一个对应的 Job 来执行 descheduler 任务,我们可以通过查看日志可以了解做了哪些平衡操作:
➜ kubectl logs -f descheduler-27378876--1-btjmd -n kube-system
I0121 02:37:10.127266       1 named_certificates.go:53] "Loaded SNI cert" index=0 certName="self-signed loopback" certDetail="\"apiserver-loopback-client@1642732630\" [serving] validServingFor=[apiserver-loopback-client] issuer=\"apiserver-loopback-client-ca@1642732629\" (2022-01-21 01:37:09 +0000 UTC to 2023-01-21 01:37:09 +0000 UTC (now=2022-01-21 02:37:10.127237 +0000 UTC))"
I0121 02:37:10.127324       1 secure_serving.go:195] Serving securely on [::]:10258
I0121 02:37:10.127363       1 tlsconfig.go:240] "Starting DynamicServingCertificateController"
I0121 02:37:10.138724       1 node.go:46] "Node lister returned empty list, now fetch directly"
I0121 02:37:10.172264       1 nodeutilization.go:167] "Node is overutilized" node="master1" usage=map[cpu:1225m memory:565Mi pods:16] usagePercentage=map[cpu:61.25 memory:15.391786081415567 pods:14.545454545454545]
I0121 02:37:10.172313       1 nodeutilization.go:164] "Node is underutilized" node="node1" usage=map[cpu:675m memory:735Mi pods:16] usagePercentage=map[cpu:16.875 memory:9.542007959787252 pods:14.545454545454545]
I0121 02:37:10.172328       1 nodeutilization.go:170] "Node is appropriately utilized" node="node2" usage=map[cpu:975m memory:1515Mi pods:15] usagePercentage=map[cpu:24.375 memory:19.66820054018583 pods:13.636363636363637]
I0121 02:37:10.172340       1 lownodeutilization.go:100] "Criteria for a node under utilization" CPU=20 Mem=20 Pods=20
I0121 02:37:10.172346       1 lownodeutilization.go:101] "Number of underutilized nodes" totalNumber=1
I0121 02:37:10.172355       1 lownodeutilization.go:114] "Criteria for a node above target utilization" CPU=50 Mem=50 Pods=50
I0121 02:37:10.172360       1 lownodeutilization.go:115] "Number of overutilized nodes" totalNumber=1
I0121 02:37:10.172374       1 nodeutilization.go:223] "Total capacity to be moved" CPU=1325 Mem=3267772416 Pods=39
I0121 02:37:10.172399       1 nodeutilization.go:226] "Evicting pods from node" node="master1" usage=map[cpu:1225m memory:565Mi pods:16]
I0121 02:37:10.172485       1 nodeutilization.go:229] "Pods on node" node="master1" allPods=16 nonRemovablePods=13 removablePods=3
I0121 02:37:10.172495       1 nodeutilization.go:236] "Evicting pods based on priority, if they have same priority, they'll be evicted based on QoS tiers"
I0121 02:37:10.180353       1 evictions.go:130] "Evicted pod" pod="default/topo-demo-6bbf65d967-lzlfh" reason="LowNodeUtilization"
I0121 02:37:10.181506       1 nodeutilization.go:269] "Evicted pods" pod="default/topo-demo-6bbf65d967-lzlfh" err=<nil>
I0121 02:37:10.181541       1 nodeutilization.go:294] "Updated node usage" node="master1" CPU=1225 Mem=592445440 Pods=15
I0121 02:37:10.182496       1 event.go:291] "Event occurred" object="default/topo-demo-6bbf65d967-lzlfh" kind="Pod" apiVersion="v1" type="Normal" reason="Descheduled" message="pod evicted by sigs.k8s.io/deschedulerLowNodeUtilization"
......

从日志中我们就可以清晰的知道因为什么策略驱逐了哪些 Pods。

  • descheduler安装出现的问题

⚠️ 要注意自己helm chart版本和老师转存的镜像是否一致,否则可能拉取镜像失败:

values.yaml内容:

image-20220219185204895

Chart.yaml文件内容:

image-20220219185509176

老师这里已经转存好的镜像如下:

image-20220219185903544

因此这里需要用如下命令来拉取:

helm upgrade --install descheduler descheduler/descheduler --set image.repository=cnych/descheduler,image.tag=v0.22.1,podSecurityPolicy.create=false -n kube-system

[root@master1 ~]#helm upgrade --install descheduler descheduler/descheduler --set image.repository=cnych/descheduler,image.tag=v0.22.1,podSecurityPolicy.create=false -n kube-system
Release "descheduler" does not exist. Installing it now.
NAME: descheduler
LAST DEPLOYED: Sat Feb 19 19:04:52 2022
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
NOTES:
Descheduler installed as a CronJob .

完美。

3、PDB

由于使用 descheduler 会将 Pod 驱逐进行重调度,但是如果一个服务的所有副本都被驱逐的话,则可能导致该服务不可用。如果服务本身存在单点故障,驱逐的时候肯定就会造成服务不可用了,这种情况我们强烈建议使用反亲和性和多副本来避免单点故障。但是如果服务本身就被打散在多个节点上,这些 Pod 都被驱逐的话,这个时候也会造成服务不可用了,这种情况下我们可以通过配置 PDB(PodDisruptionBudget) 对象来避免所有副本同时被删除,比如我们可以设置在驱逐的时候某应用最多只有一个副本不可用,则创建如下所示的资源清单即可:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: pdb-demo
spec:
  maxUnavailable: 1  # 设置最多不可用的副本数量,或者使用 minAvailable,可以使用整数或百分比
  selector:
    matchLabels:    # 匹配Pod标签
      app: demo

关于 PDB 的更多详细信息可以查看官方文档:https://kubernetes.io/docs/tasks/run-application/configure-pdb/。

⚠️ 所以如果我们使用 descheduler 来重新平衡集群状态,那么我们强烈建议给应用创建一个对应的 PodDisruptionBudget 对象进行保护。

4、策略

1.PodLifeTime

该策略用于驱逐比 maxPodLifeTimeSeconds 更旧的 Pods;可以通过 podStatusPhases 来配置哪类状态的 Pods 会被驱逐。建议为每个应用程序创建一个 PDB,以确保应用程序的可用性。

比如我们可以配置如下所示的策略来驱逐运行超过7天的 Pod:

apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
  "PodLifeTime":
    enabled: true
    params:
      maxPodLifeTimeSeconds: 604800  # Pods 运行最多7天
2.RemoveDuplicates

**该策略确保只有一个和 Pod 关联的 RS、Deployment 或者 Job 资源对象运行在同一节点上。**如果还有更多的 Pod 则将这些重复的 Pod 进行驱逐,以便更好地在集群中分散 Pod。如果某些节点由于某些原因崩溃了,这些节点上的 Pod 漂移到了其他节点,导致多个与 RS 关联的 Pod 在同一个节点上运行,就有可能发生这种情况,一旦出现故障的节点再次准备就绪,就可以启用该策略来驱逐这些重复的 Pod。

RemoveDuplicates

配置策略的时候,可以指定参数 excludeOwnerKinds 用于排除类型,这些类型下的 Pod 不会被驱逐:

apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
  "RemoveDuplicates":
     enabled: true
     params:
       removeDuplicates:
         excludeOwnerKinds:
         - "ReplicaSet"
3.LowNodeUtilization

这个策略用的想对比较多一些。

该策略主要用于查找未充分利用的节点,并从其他节点驱逐 Pod,以便 kube-scheudler 重新将它们调度到未充分利用的节点上。该策略的参数可以通过字段 nodeResourceUtilizationThresholds 进行配置。

节点的利用率不足可以通过配置 thresholds 阈值参数来确定,可以通过 CPU、内存和 Pods 数量的百分比进行配置。如果节点的使用率均低于所有阈值,则认为该节点未充分利用。

LowNodeUtilization

  • 此外,还有一个可配置的阈值 targetThresholds,用于计算可能驱逐 Pods 的潜在节点,该参数也可以配置 CPU、内存以及 Pods 数量的百分比进行配置。thresholdstargetThresholds 可以根据你的集群需求进行动态调整,如下所示示例:
apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
  "LowNodeUtilization":
     enabled: true
     params:
       nodeResourceUtilizationThresholds:
         thresholds: #注意:这个是未充分利用节点
           "cpu" : 20
           "memory": 20
           "pods": 20
         targetThresholds: #注意:这个可能驱逐 Pods 的潜在节点
           "cpu" : 50
           "memory": 50
           "pods": 50

⚠️ 注意:这里可以考虑下用p8s来采集node的负载指标然后写在这里进行自定义;

需要注意的是:

  • 仅支持以下三种资源类型:cpu、memory、pods
  • thresholdstargetThresholds 必须配置相同的类型
  • 参数值的访问是0-100(百分制)
  • 相同的资源类型,thresholds 的配置不能高于 targetThresholds 的配置

如果未指定任何资源类型,则默认是100%,以避免节点从未充分利用变为过度利用。

LowNodeUtilization 策略关联的另一个参数是 numberOfNodes,只有当未充分利用的节点数大于该配置值的时候,才可以配置该参数来激活该策略,该参数对于大型集群非常有用,其中有一些节点可能会频繁使用或短期使用不足,默认情况下,numberOfNodes 为0。

4.RemovePodsViolatingInterPodAntiAffinity

该策略可以确保从节点中删除违反 Pod 反亲和性的 Pod,比如某个节点上有 podA 这个 Pod,并且 podB 和 podC(在同一个节点上运行)具有禁止它们在同一个节点上运行的反亲和性规则,则 podA 将被从该节点上驱逐,以便 podB 和 podC 运行正常运行。当 podB 和 podC 已经运行在节点上后,反亲和性规则被创建就会发送这样的问题。

RemovePodsViolatingInterPodAntiAffinity

要禁用该策略,直接配置成 false 即可:

apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
  "RemovePodsViolatingInterPodAntiAffinity":
     enabled: false
5.RemovePodsViolatingNodeTaints

该策略可以确保从节点中删除违反 NoSchedule 污点的 Pod,比如有一个名为 podA 的 Pod,通过配置容忍 key=value:NoSchedule 允许被调度到有该污点配置的节点上,如果节点的污点随后被更新或者删除了,则污点将不再被 Pods 的容忍满足,然后将被驱逐:

apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
  "RemovePodsViolatingNodeTaints":
    enabled: true
6.RemovePodsViolatingNodeAffinity

该策略确保从节点中删除违反节点亲和性的 Pod。比如名为 podA 的 Pod 被调度到了节点 nodeA,podA 在调度的时候满足了节点亲和性规则 requiredDuringSchedulingIgnoredDuringExecution,但是随着时间的推移,节点 nodeA 不再满足该规则了,那么如果另一个满足节点亲和性规则的节点 nodeB 可用,则 podA 将被从节点 nodeA 驱逐,如下所示的策略配置示例:

apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
  "RemovePodsViolatingNodeAffinity":
    enabled: true
    params:
      nodeAffinityType:
      - "requiredDuringSchedulingIgnoredDuringExecution"
7.RemovePodsViolatingTopologySpreadConstraint

该策略确保从节点驱逐违反拓扑分布约束的 Pods,具体来说,它试图驱逐将拓扑域平衡到每个约束的 maxSkew 内所需的最小 Pod 数,不过该策略需要 k8s 版本高于1.18才能使用。

默认情况下,此策略仅处理硬约束,如果将参数 includeSoftConstraints 设置为 True,也将支持软约束。

apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
  "RemovePodsViolatingTopologySpreadConstraint":
     enabled: true
     params:
       includeSoftConstraints: false
8.RemovePodsHavingTooManyRestarts

该策略确保从节点中删除重启次数过多的 Pods,它的参数包括 podRestartThreshold(这是应将 Pod 逐出的重新启动次数),以及包括InitContainers,它确定在计算中是否应考虑初始化容器的重新启动,策略配置如下所示:

apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
  "RemovePodsHavingTooManyRestarts":
     enabled: true
     params:
       podsHavingTooManyRestarts:
         podRestartThreshold: 100
         includingInitContainers: true

5、Filter Pods

在驱逐 Pods 的时候,有时并不需要所有 Pods 都被驱逐,descheduler 提供了两种主要的方式进行过滤:命名空间过滤和优先级过滤。

1.命名空间过滤

该策略可以配置是包含还是排除某些名称空间。可以使用该策略的有:

  • PodLifeTime
  • RemovePodsHavingTooManyRestarts
  • RemovePodsViolatingNodeTaints
  • RemovePodsViolatingNodeAffinity
  • RemovePodsViolatingInterPodAntiAffinity
  • RemoveDuplicates
  • RemovePodsViolatingTopologySpreadConstraint

比如只驱逐某些命令空间下的 Pods,则可以使用 include 参数进行配置,如下所示:

apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
  "PodLifeTime":
     enabled: true
     params:
        podLifeTime:
          maxPodLifeTimeSeconds: 86400
        namespaces:
          include:
          - "namespace1"
          - "namespace2"

又或者要排除掉某些命令空间下的 Pods,则可以使用 exclude 参数配置,如下所示:

apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
  "PodLifeTime":
     enabled: true
     params:
        podLifeTime:
          maxPodLifeTimeSeconds: 86400
        namespaces:
          exclude:
          - "namespace1"
          - "namespace2"
2.优先级过滤

所有策略都可以配置优先级阈值,只有在该阈值以下的 Pod 才会被驱逐。我们可以通过设置 thresholdPriorityClassName(将阈值设置为指定优先级类别的值)或 thresholdPriority(直接设置阈值)参数来指定该阈值。默认情况下,该阈值设置为 system-cluster-critical 这个 PriorityClass 类的值。

比如使用 thresholdPriority

apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
  "PodLifeTime":
     enabled: true
     params:
        podLifeTime:
          maxPodLifeTimeSeconds: 86400
        thresholdPriority: 10000

或者使用 thresholdPriorityClassName 进行过滤:

apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
  "PodLifeTime":
     enabled: true
     params:
        podLifeTime:
          maxPodLifeTimeSeconds: 86400
        thresholdPriorityClassName: "priorityclass1"

不过需要注意不能同时配置 thresholdPrioritythresholdPriorityClassName,如果指定的优先级类不存在,则 descheduler 不会创建它,并且会引发错误。

6、注意事项

当使用descheduler驱除Pods的时候,需要注意以下几点:

  • 关键性 Pod 不会被驱逐,比如 priorityClassName 设置为 system-cluster-criticalsystem-node-critical 的 Pod

  • 不属于 RS、Deployment 或 Job 管理的 Pods 不会被驱逐

  • DaemonSet 创建的 Pods 不会被驱逐

  • 使用 LocalStorage 的 Pod 不会被驱逐,除非设置 evictLocalStoragePods: true

  • 具有 PVC 的 Pods 不会被驱逐,除非设置 ignorePvcPods: true

  • LowNodeUtilizationRemovePodsViolatingInterPodAntiAffinity 策略下,Pods 按优先级从低到高进行驱逐,如果优先级相同,Besteffort 类型的 Pod 要先于 BurstableGuaranteed 类型被驱逐

  • annotations 中带有 descheduler.alpha.kubernetes.io/evict 字段的 Pod 都可以被驱逐,该注释用于覆盖阻止驱逐的检查,用户可以选择驱逐哪个Pods

  • 如果 Pods 驱逐失败,可以设置 --v=4descheduler 日志中查找原因,如果驱逐违反 PDB 约束,则不会驱逐这类 Pods

    image-20220219222613564

问题:node节点资源负载不均衡问题-2022.3.11

🍂 问题:node节点资源负载不均衡问题-2022.3.11.md

image-20220311125540505

🍂 讨论过程

image-20220311125622816

目前开源工具 里面 没有根据node使用率调整pod数量的

deschduler 是根据 请求量调度

但是不会根据 node使用量

阿里根据这个情况 改了 deschduler

image-20220311125718841

limits写好就行了

image-20220311125812576

有个叫 负载感知的组件

image-20220311125840362

我看了下。只能感知cpu

image-20220311125912020

我这边java服务不怎么吃cpu

吃的都是内存。

image-20220311125939251

我现在是requests写多一点,然后多实例的都强制打散了,也没别的了

如果都是单实例感觉没啥特别好的,除非你limit写的比较合理吧

image-20220311130007657

image-20220311130035794

image-20220311130055736

应该是刚启动的 cpu占用高

往后就少了 但内存是一直增长的

image-20220311130117768

md 之前requests写的多了 导致很多任务没有资源使用,无法调度 一致pending 监控一看 节点的资源使用率才到百分之五六十

我们这limit,500%,鸡儿实际使用率才60%左右·

image-20220311130153989

Prometheus有监控检查的接口吗

https://cloud.tencent.com/document/product/457/50921

image-20220311130215068

image-20220311130246228

image-20220311130253485

image-20220311130259700

9、多调度器

10、动态调度器


代码的实现而是很简单的。

🍂
docs.gorance.io

🍂

p8s基本是k8s的标配!
这个调度插件只一个锦上添花的能力,而不会影响主调度能力的。

🍂

webhook,节点超售–实际生产上有大规模使用的!

🍂 动态调度成效

11、自定义调度器

1.自定义方式


1.19版本以后:Schedulling Framework,已经达到了stable状态

2.扩展配置

3.初始化plugin

4.核心逻辑

关于我

我的博客主旨:

  • 排版美观,语言精炼;
  • 文档即手册,步骤明细,拒绝埋坑,提供源码;
  • 本人实战文档都是亲测成功的,各位小伙伴在实际操作过程中如有什么疑问,可随时联系本人帮您解决问题,让我们一起进步!
  1. 微信二维码
    x2675263825 (舍得), qq:2675263825。
    image-20211002091450217
  2. 微信公众号
    《云原生架构师实战》
    image-20211002141739664
  3. csdn
    https://blog.csdn.net/weixin_39246554?spm=1010.2135.3001.5421
    image-20211002092344616
  4. 博客
    www.onlyyou520.com
    image-20220513150311181
  5. 开源干货
    语雀:https://www.yuque.com/go/doc/73723298?#
    image-20220520080306203
  6. 知乎
    https://www.zhihu.com/people/foryouone

最后

好了,关于本次就到这里了,感谢大家阅读,最后贴上我女神的photo,祝大家生活快乐,每天都过的有意义哦,我们下期见!

image-20220217191118541

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值