一、初识kubernetes调度

1、Automated Placement为kubernetes带来的问题

Automated Placement是kubernetes调度器的核心功能,那么在实现Automated Placement时,kubernetes需要面对哪些问题呢?

1.1、如何找到一个合适的节点

在一个分布式系统中,常常会面对数十个甚至数百个乃至更多的进程。虽然pod将打包和部署进行了抽象,提供了完善的部署方案,但是如何寻找到一个合适的节点却一直没有解决。要知道容器之间存在依赖关系,容器对节点所提供的资源也有一定的需求,从而使容器对节点也有依赖。而节点在使用过程中,资源消耗也会不断的发生变化,那么如果处理这些问题呢?

1.2、如何满足用户各种各样的调度需求,使pod分布更加完美

在一个分布式系统内,我们要面对的需求有很多,例如:

  • 我们需要满足pod尽可能均匀的分布到节点上,避免因为节点异常而导致大量pod无法提供服务。
  • 频繁互相调用的pod尽可能分配在同一节点上来降低时延。
  • 稀有资源,如GPU需要实现独享,避免无关pod抢占节点,浪费资源。
  • 集群长期运行后,节点出现内存、CPU使用比不均衡,导致CPU或内存大量闲置二其余资源耗尽的问题。

那么,kubenetes时如何解决这一系列的问题的呢?这就需要深入理解schedule这一核心组件了,下面让我们来深入学习kube-scheduler。

2、scheduler架构和调度流程

kubernetes调度是基于队列的调度器,在调度过程中,每次进调度一个pod,通过这一机制保证调度时刻的全局最优。在一次完整的调度流程中,scheduler将对pod进行一次筛选及打分。在筛选中,将符合需求的node过滤出来,之后对每一个node进行打分,之后将pod调度到最高分的node上,若存在多个等分的node,则会随机选择一个node进行调度。关于scheduler的架构及详细调度流程如下:
深入理解Kube-scheduler - 图1

  1. 通过kubectl交互apiserver后,informer list/watch到资源变化,会更新调度队列和调度缓存
  2. 队列排序插件用于对调度队列中的 Pod 进行排序。 队列排序插件本质上提供 less(Pod1, Pod2) 函数。 一次只能启动一个队列插件。
  3. 前置过滤插件用于预处理 Pod 的相关信息,或者检查集群或 Pod 必须满足的某些条件。 如果 PreFilter 插件返回错误,则调度周期将终止。
  4. 过滤插件用于过滤出不能运行该 Pod 的节点。对于每个节点, 调度器将按照其配置顺序调用这些过滤插件。如果任何过滤插件将节点标记为不可行, 则不会为该节点调用剩下的过滤插件。节点可以被同时进行评估。
  5. 这些插件在筛选阶段后调用,但仅在该 Pod 没有可行的节点时调用。 插件按其配置的顺序调用。如果任何后过滤器插件标记节点为“可调度”, 则其余的插件不会调用。典型的后筛选实现是抢占,试图通过抢占其他 Pod 的资源使该 Pod 可以调度。
  6. 前置评分插件用于执行 “前置评分” 工作,即生成一个可共享状态供评分插件使用。 如果 PreScore 插件返回错误,则调度周期将终止。
  7. 评分插件用于对通过过滤阶段的节点进行排名。调度器将为每个节点调用每个评分插件。 将有一个定义明确的整数范围,代表最小和最大分数。 在标准化评分阶段之后,调度器将根据配置的插件权重 合并所有插件的节点分数。
  8. 预绑定插件用于执行 Pod 绑定前所需的任何工作。 例如,一个预绑定插件可能需要提供网络卷并且在允许 Pod 运行在该节点之前 将其挂载到目标节点上。
  9. Bind 插件用于将 Pod 绑定到节点上。直到所有的 PreBind 插件都完成,Bind 插件才会被调用。 各绑定插件按照配置顺序被调用。绑定插件可以选择是否处理指定的 Pod。 如果绑定插件选择处理 Pod,剩余的绑定插件将被跳过。
  10. 这是个信息性的扩展点。 绑定后插件在 Pod 成功绑定后被调用。这是绑定周期的结尾,可用于清理相关的资源。

    3、深入理解过滤策略

    过滤算法是在kubernetes调度过程中,对于节点进行粗过滤,在这个过程中过滤掉不符合需求的节点,保留下可用节点。具体算法如下:
算法名称 功 能
GeneralPredicates 检查node是否与pod所需资源匹配,包括cpu、mem、gpu、pod上限等
NoDiskConflict 检查node是否满足存储需求,是否存在卷冲突。
CheckVolumeBinding 检查是否满足pod资源对象pvc的挂载
NoVolumeZoneConflict 检查pod资源对象挂载pvc是否跨区域挂载
CheckNodeMemoryPressure 检查node是否满足pod的内存需求
CheckNodePidPressure 检查node是否进程压力过大
PodToleratesNodeTaints 检查pod是否可以容忍污点
MatchInterPodAffinity 检查亲和性

根据以上算法已经可以筛选出来一批可用节点,那么如何将pod调度到最适合的节点呢?

4、深入理解优选策略

在过滤策略筛选后的node是完全可以符合pod需求的,但是在这么多的node中五和选择一个最适配pod的节点,就需要通过优选策略了。具体算法如下:

算法名称 功 能
LeastRequestedPriority 按node计算资源,挑选最空闲node
MostRequestedPriority 按node计算资源,挑选消耗最大的node
BalancedResourceAllocation 补充LeastRequestedPriority,平衡men和cpu剩余
SelectorSpreadPriority 同一个svc或rc下的pod尽可能打散
NodeAffinityPriority 按亲节点和性打分,命中规则越高分越高
TaintTolerationPriority 按污点匹配,越多的污点不匹配,分数越低
InterPodAffinityPriority 按照pod亲和性和反亲和性规则匹配进行打分
ImageLocalityPriority 若节点是否已有当前所需镜像来打分
  1. 通过上述算法进行打分后,pod将调度到最高分节点,至此,一次pod调度循环结束。至此,我们学习到了,kubernetes时如何实现Automated Placement中最基本的需求,如何找到一个合适的节点,那么其他的调度需求又是如何实现的呢?

二、kube-scheduler的个性化调度

1、selector机制

当我们的kubernetes集群中有大量节点,并且节点功能不同时,我们可以给节点添加label进行区分,一边对节点进行分组管理,例如:

  • 针对稀有资源添加label,对使用SSD的节点,添加DISK=SSD的label,对使用GPU的节点accelerator=nvidia的label
  • 根据服务前端后端添加label,前端APP=front,后端APP=backend

当我们通过标签对节点进行分组后,在编排pod时,就可以通过nodeSelector来筛选节点,将pod调度到期望的节点上了。

2、Affinity与Anti-Affinity机制

虽然selector可以帮我们实现对节点的进一步管理,但是在分布式系统内,仍然会出现多个pod调度到同一节点上这一现象,一旦节点异常,便不得不面对大量pod无法提供服务的困境,针对这一问题,kubernetes提供了Anti-affinity这一机制,实现代码如下

apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-cache
spec:
  selector:
    matchLabels:
      app: store
  replicas: 3
  template:
    metadata:
      labels:
        app: store
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - store
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: redis-server
        image: redis:3.2-alpine

上述代码实现了同一标签的pod不在同一节点上这一需求,同样的,也可以针对某些需要调度到同一节点的应用,我们也可以通过pod Affinity来实现,代码如下

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-server
spec:
  selector:
    matchLabels:
      app: web-store
  replicas: 3
  template:
    metadata:
      labels:
        app: web-store
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - web-store
            topologyKey: "kubernetes.io/hostname"
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - store
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: web-app
        image: nginx:1.16-alpine

特别需要注意的是,Affinity和AntiAffinity不仅可以作用于pod和pod之前,还可以实现节点的亲和性一反亲和性,而且还支持requiredDuringSchedulingIgnoredDuringExecution 和 preferredDuringSchedulingIgnoredDuringExecution,也就是硬亲和性和软亲和性,这使得 Affinity和Anti-Affinity使用的越来越广泛。

3、Taint机制

前文提到过,我们在节点中会提供例如SSD、GPU等昂贵的资源,虽然有selector Anti-Affinity等机制,但仍然无法避免pod调度到这类稀有资源的节点上,从而占用稀有资源产生浪费,那么这个时候就需要在用Taint来实现进一步的约束。
假设这里有一个节点node1,上面存在有GPU,那么为了保护这个节点,我们可以先执行如下命令

# 添加 GPU 标签
kubectl label nodes node1 accelerator=nvidia
# 添加污点
kubectl taint nodes node1 special=true:NoSchedule

之后我们就可以通过如下代码,将需要CPU的pod调度到该节点,而其他pod则无法调度到该节了

tolerations:
- key: "special"
  operator: "Equal"
  value: "true"
  effect: "NoExecute"
  tolerationSeconds: 3600

4、Descheduler

在kubernetes中,一个pod一旦调度到一个node上,除非发生node异常,或者是手动删除pod,否则,pod上不会不会迁移。那么随着时间的迁移,调度工作可能会引发资源碎片化,集群资源利用率低,部分节点负载过高等问题,同时修改节点的label也不会修正之前的调度结果。为了修正这一问题,kubernetes sigs社区推出了Descheduler 工具。
Descheduler 支持多种解调度策略:

4.1、RemoveDuplicates

此策略确保每个副本集(RS)、副本控制器(RC)、部署(Deployment)或任务(Job)只有一个 pod 被分配到同一台 node 节点上。如果有多个则会被驱逐到其它节点以便更好的在集群内分散 pod。
可用字段如下:

Name Type
excludeOwnerKinds list(string)
namespaces (see namespace filtering)
thresholdPriority int (see priority filtering)
thresholdPriorityClassName string (see priority filtering)
nodeFit bool (see node fit filtering)

模版:

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

4.2、LowNodeUtilization

此策略会查找未充分利用的节点,并在可能的情况下从其他节点驱逐pod。可以对noode配置threshold,当一个节点上mem、cpu、pod数量多资源均低于threshold时,则认为此node资源利用率低。
可用参数如下:

Name Type
thresholds map(string:int)
targetThresholds map(string:int)
numberOfNodes int
thresholdPriority int (see priority filtering)
thresholdPriorityClassName string (see priority filtering)
nodeFit bool (see node fit filtering)

配置模版

apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
  "LowNodeUtilization":
     enabled: true
     params:
       nodeResourceUtilizationThresholds:
         thresholds:
           "cpu" : 20
           "memory": 20
           "pods": 20
         targetThresholds:
           "cpu" : 50
           "memory": 50
           "pods": 50
# 支持原生资源类型为 cpu、mem、pod数,默认thresholds为100
# 可扩展资源类型
# thresholds不可超过targetThresholds,并且范围为[0-100]

4.3、HighNodeUtilization

此策略是发现为充分利用的node,并驱逐此node上的pod。节点为充分利用的thresholds是可以配置的,thresholds可以配置为 mem、cpu、pod数量的百分比,如果节点低于此thresholds,就认为此节点为充分利用。可用字段为:

Name Type
thresholds map(string:int)
numberOfNodes int
thresholdPriority int (see priority filtering)
thresholdPriorityClassName string (see priority filtering)
nodeFit bool (see node fit filtering)

配置模板:

apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
  "HighNodeUtilization":
     enabled: true
     params:
       nodeResourceUtilizationThresholds:
         thresholds:
           "cpu" : 20
           "memory": 20
           "pods": 20

# 支持原生资源类型为 cpu、mem、pod数,默认thresholds为100
# 可扩展资源类型
# thresholds不可超过targetThresholds,并且范围为(0-100]

4.4、RemovePodsViolatingInterPodAntiAffinity

此策略会暴力清除node中违反pod亲和性的pod。
可用参数:
Parameters:

Name Type
thresholdPriority int (see priority filtering)
thresholdPriorityClassName string (see priority filtering)
namespaces (see namespace filtering)
labelSelector (see label filtering)
nodeFit bool (see node fit filtering)

代码模板:

apiVersion: "descheduler/v1alpha1"
kind: "DeschedulerPolicy"
strategies:
  "RemovePodsViolatingInterPodAntiAffinity":
     enabled: true

4.5、RemovePodsViolatingNodeAffinity

此策略会移除node中所有违反node亲和性的pod,并在下次调度中遵守node亲和性。
可用字段:

Name Type
nodeAffinityType list(string)
thresholdPriority int (see priority filtering)
thresholdPriorityClassName string (see priority filtering)
namespaces (see namespace filtering)
labelSelector (see label filtering)
nodeFit bool (see node fit filtering)

代码模板:

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

4.6、RemovePodsViolatingNodeTaints

此策略会移除该node上违反node 污点容忍的pod。
可以参数:

Name Type
thresholdPriority int (see priority filtering)
thresholdPriorityClassName string (see priority filtering)
namespaces (see namespace filtering)
labelSelector (see label filtering)
nodeFit bool (see node fit filtering)

代码模板:

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

4.7、RemovePodsViolatingTopologySpreadConstraint

该策略确保 从节点驱逐违反拓扑扩展约束的node。具体而言,它尝试逐出将拓扑域平衡到每个约束的内所需的最小Pod数maxSkew。此策略至少需要k8s 1.18版。
可用参数:

Name Type
includeSoftConstraints bool
thresholdPriority int (see priority filtering)
thresholdPriorityClassName string (see priority filtering)
namespaces (see namespace filtering)
labelSelector (see label filtering)
nodeFit bool (see node fit filtering)

代码模板:

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

4.8、RemovePodsHavingTooManyRestarts

此策略用于移除该node上重启次数过多的pod。
可用参数:
Parameters:

Name Type
podRestartThreshold int
includingInitContainers bool
thresholdPriority int (see priority filtering)
thresholdPriorityClassName string (see priority filtering)
namespaces (see namespace filtering)
nodeFit bool (see node fit filtering)

代码模板:

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

4.9、PodLifeTime

此策略驱逐了超过maxPodLifeTimeSeconds的pod

Name Type
maxPodLifeTimeSeconds int
podStatusPhases list(string)
thresholdPriority int (see priority filtering)
thresholdPriorityClassName string (see priority filtering)
namespaces (see namespace filtering)
labelSelector (see label filtering)

至此我们已经充分的了解了kubenetes调度的原理。