15. Kubernetes调度和资源管理

本文由 CNCF + Alibaba 云原生技术公开课 整理而来

Kubernetes 调度过程

现在假设有一个 Kubernetes 集群,如果要向这个 Kubernetes 集群提交一个 Pod,那它的调度过程是什么样的一个流程?

首先需要写一个 yaml 文件,然后往 ApiServer 里面提交这个 yaml 文件。此时,ApiServer 会先把这个待创建的请求路由给 Kube-Controller 进行校验。

在通过校验之后,ApiServer 会在集群里面生成一个 Pod,但此时生成的 PodnodeName 是空的,并且它是 Pending 状态。在生成了这个 Pod 之后,Kube-Scheduler 以及 Kubelet 都能 watch 到这个 Pod 的生成事件,Kube-Scheduler 发现这个 PodnodeName 是空的之后,会认为这个 Pod 是处于未调度状态。

接下来,Kube-Scheduler 会把这个 Pod 拿到自己里面进行调度,通过一系列的调度算法,包括一系列的过滤和打分的算法后,Kube-Scheduler 会选出一台最合适的节点,并且把这一台节点的名称绑定在这个 Podspec 上,完成一次调度的过程。

此时可以看到,Podspec 上,nodeName 已经更新成了集群的某一个 Node,更新完 nodeName 之后,在这个 Node 上的这台 Kubeletwatch 到这个 Pod 是属于自己节点上的一个 Pod

然后它会把这个 Pod 拿到节点上进行操作,包括创建一些容器 storage 以及 network,最后等所有的资源都准备完成,Kubelet 会把状态更新为 Running,这样一个完整的调度过程就结束了。

上面 Pod 的整个调度流程,可以用一句话来概括:把 Pod 放到“合适”的 Node 上。那什么才是“合适”呢?这里给出几点“合适”的要求:

1. 首先要满足 Pod 的资源要求;

2. 其次要满足 Pod 的一些特殊关系的要求;

3. 再次要满足 Node 的一些限制条件的要求;

4. 最后还要做到整个集群资源的合理利用。

满足这些要求之后,就可以认为把 Pod 放到了一个合适的 Node 上了。


Kubernetes 基础调度

  • Pod 的资源配置:
Pod 的资源类型:

    cpu资源(1核 = 1000m)
    
    memory资源(1Gi = 1024Mi)
    
    ephemeral-storage(临时存储)
    
    extended-resource(扩展资源,必须是整数)

demo-pod Pod yaml 文件示例:

apiVersion: v1
kind: Pod
metadata:
  name: demo-pod
  namespace: demo-ns
spec:
  containers:
  - name: demo-container
    image: nginx:latest
    ports:
    - containerPort: 80
    resources:
      limits:
        cpu: 2
        memory: 1Gi
      requests:
        cpu: 2
        memory: 1Gi

对于 Pod 中容器的资源由 .spec.containers.resources 字段配置,.spec.containers.resources 其实包含两个部分:requestslimitsrequests 代表的是这个 Pod 最小的资源要求;limits 代表的是这个 Pod 最大的资源限制。

上面例子中申请的是 2 个 CPU,其实也可以写成 2000m,有时对 CPU 可能是更小的需求,例如 0.2 个 即 200m。在 Memory 和 Storage 之上,它是一个二进制的表达方式。在扩展资源上,Kubernetes 有一个要求,即扩展资源必须是整数的,所以无法申请到 0.5 的 GPU 资源,只能申请 1 个 GPU 或者 2 个 GPU。

  • Pod QoS 类型:

Kubernetes 在 .spec.containers.resources 里面配置了 requestslimits,它其实是为 Pod 提供了一种资源的弹性定义。例如可以对 requests 填 2 个 CPU,limits 填 4 个 CPU,这样其实代表希望是有 2 个 CPU 的保底能力,但其实是在闲置的时候,可以使用 4 个 CPU。

那什么是 Qos 呢?Qos 全称是 Quality of Service,它其实是 Kubernetes 用来表达一个 Pod 在资源能力上的服务质量的标准,Kubernetes 提供了三类的 Qos Class:

第一类是 Guaranteed,它是一类高的 Qos Class,一般用 Guaranteed 来为一些需要资源保障能力的 Pod 进行配置;

第二类是 Burstable,它其实是中等的一个 Qos label,一般会为一些希望有弹性能力的 Pod 来配置 Burstable;

第三类是 BestEffort,通过名字也知道,它是一种尽力而为式的服务质量。

Kubernetes 中,用户无法指定自己的 Pod 是属于哪一类 Qos,而是通过 requestslimits 的组合来自动地映射上 Qos Class。在 spec 提交成功之后,Kubernetes 会自动给补上一个 status,里面是相应的 Qos Class。这就是隐性的 Qos class 用法。

  • Pod QoS 配置:

那怎么通过 requestslimits 的组合来确定想要的 Qos Class呢?Kubernetes 中是这样定义的:

Guaranteed:要求 CPU/Memory 的 requests = limits,其余资源无要求

Burstable:CPU/Memory 的 requests 和 limits 不相等

BestEffort:所有资源的 requests 和 limits 必须没有指定

如果想创建出一个 Guaranteed Pod,那么基础资源(CPU 和 Memory)必须 requests = limits,其他的资源可以不相等。只有在这种条件下,它创建出来的 Pod 才是 Guaranteed Pod,否则它会属于 Burstable PodBestEffort Pod

Guaranteed Pod yaml 文件示例:

apiVersion: v1
kind: Pod
metadata:
  name: demo-pod
  namespace: demo-ns
spec:
  containers:
  - name: demo-container
    image: nginx:latest
    ports:
    - containerPort: 80
    resources:
      limits:
        cpu: 2
        memory: 1Gi
      requests:
        cpu: 2
        memory: 1Gi

Burstable Pod yaml 文件示例:

apiVersion: v1
kind: Pod
metadata:
  name: demo-pod
  namespace: demo-ns
spec:
  containers:
  - name: demo-container
    image: nginx:latest
    ports:
    - containerPort: 80
    resources:
      limits:
        cpu: 2
      requests:
        cpu: 1

BestEffort Pod yaml 文件示例:

apiVersion: v1
kind: Pod
metadata:
  name: demo-pod
  namespace: demo-ns
spec:
  containers:
  - name: demo-container
    image: nginx:latest
    ports:
    - containerPort: 80
  • 不同的 QoS 表现:

不同的 Qos 在调度和底层表现上都会不一样。在调度上,调度器只会使用 requests 进行调度,不管配置了多大的 limits,它都不会进行调度使用,它只会使用 requests 进行调度。

在底层上,不同的 Qos 表现更不相同。对于CPU,它其实是按 requests 来划分权重的,BurstableBestEffort 可能 requests 可以填很小的数字或者不填,这样它的权重其实是非常低的。可能 BestEffort 的权重可能是只有 2,而 BurstableGuaranteed 的权重可以多到几千。当开启了 Kubeletcpu-manager-policy=static 特性时,如果 Guaranteed Pod 的 CPU requests 是一个整数,它会对 Guaranteed Pod 进行绑核。

Memory 上,也会按照不同的 Qos 进行划分:OOMScoreGuaranteed 会配置默认的 -998 的 OOMScoreBurstable 会根据内存设计的大小和节点的关系来分配 2-999 的 OOMScoreBestEffort 会固定分配 1000 的 OOMScoreOOMScore 得分越高的话,在节点出现 OOM 的时候会优先被 kill 掉。

另外在节点上的 eviction 动作上,不同的 Qos 也是不一样的:发生 eviction 的时候,会优先考虑驱逐 BestEffortPod。所以不同的 Qos 其实在底层的表现是截然不同的。这也反过来要求在生产环境中,要根据不同业务的要求和属性来配置资源的 requestslimits,做到合理的规划 Qos Class

如何满足 Pod 资源要求:

Guaranteed:敏感型,需要业务保障

Burstable:次敏感型,需要弹性业务

BestEffort:可容忍性业务
  • 资源 Quota

在生产中可能会遇到一个场景:假如一个集群由多个人同时提交或者多个业务同时在使用,那肯定要限制某个业务或某个人提交的总量,防止整个集群的资源都会被使用掉,导致另一个业务没有资源使用。

Kubernetes 给提供了一个资源配额管理的对象叫:ResourceQuota,它可以限制 namespace 的资源用量。每个 ResourceQuota 都可以单独配置一组作用域,配置了作用域的 ResourceQuota 只会对符合其作用域的资源使用情况进行计量和限制,作用域范围内超过了资源配额管理的请求都会报验证错误。

demo-quota ResourceQuota yaml 文件示例:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: demo-quota
  namespace: demo-ns
spec:
  hard:
    cpu: "1000"
    memory: 200Gi
    pods: "10"
    configmap: "10"
  scopeSelector:
    matchExpressions:
    - operator: Exists
      scopeName: NotBestEffort

文件解析:

apiVersion: v1      表示 ResourceQuota 的 API 版本是 v1

kind        表示 Kubernetes 资源类型是 ResourceQuota

metadata    表示 ResourceQuota 的元数据,元数据通常包含 name、namespace、annotations 等

spec    表示 ResourceQuota 的期望状态,hard 表示基础资源限制,如限制 Pod 数量为 10;
        scopeSelector 表示作用域,包括 Terminating、NotTerminating、BestEffort、NotBestEffort

ResourceQuota 的 4 种作用域:

Terminating         匹配所有 spec.activeDeadlineSeconds 不小于 0 的 Pod

NotTerminating      匹配所有 spec.activeDeadlineSeconds 是 nil 的 Pod

BestEffort          匹配所有 Qos 是 BestEffort 的 Pod

NotBestEffort       匹配所有 Qos 不是 BestEffort 的 Pod
  • PodPod 的关系调度:

PodPod 的关系调度分为 PodAffinity(Pod 亲和调度)和 PodAntAffinity(Pod 反亲和调度)。当想让一个 Pod 和 另一个 Pod 在同一个节点上时,可以使用 PodAffinity 进行 Pod 亲和调度,反之则使用 PodAntAffinity 进行 Pod 反亲和调度。

Pod 亲和调度 PodAffinity:

    requiredDuringSchedulingIgnoredDuringExecution              必须和某些 Pod 调度到一起
    
    preferredDuringSchedulingIgnoredDuringExecution             优先和某些 Pod 调度到一起
    
Pod 反亲和调度 PodAntAffinity:

    requiredDuringSchedulingIgnoredDuringExecution              禁止和某些 Pod 调度到一起
    
    preferredDuringSchedulingIgnoredDuringExecution             优先不和某些 Pod 调度到一起
    
Operator:

    In、NotIn、Exists、DoesNotExist

demo-pod Pod yaml 文件 PodAffinity 示例:

apiVersion: v1
kind: Pod
metadata:
  name: demo-pod
  namespace: demo-ns
spec:
  containers:
  - name: demo-container
    image: nginx:latest
    ports:
    - containerPort: 80
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: k1
            operator: In
            values:
            - v1
        topologykey: "kubernetes.io/hostname"

文件解析:

.spec.affinity  声明 Pod 的 affinity 信息

.spec.affinity.podAffinity  声明 Pod 的 affinity 为 Pod 亲和调度

.spec.affinity.podAffinity.requiredDuringSchedulingIgnoredDuringExecution   声明 Pod 的 Pod 亲和调度的条件必须满足

.spec.affinity.podAffinity.requiredDuringSchedulingIgnoredDuringExecution.labelSelector 声明 Pod 的 Pod 亲和调度必须满足的标签是 k1:v1

.spec.affinity.podAffinity.requiredDuringSchedulingIgnoredDuringExecution.labelSelector.matchExpressions.operator   声明 Pod 的 Pod 亲和调度中标签选择器的运算符,有 In、NotIn、Exists、DoesNotExist

上面示例中,demo-pod 必须要调度到带 k1:v1 标签的 Pod 所在的节点上,只有找到带 k1:v1 标签的 Pod 所在的节点,demo-pod 才会调度成功。

Kubernetes 除了 In 这个 Operator之外,同样的功能也可以使用 ExistsExists 范围可能会比 In 范围更大,当 OperatorExists 时,就不需要再填写 values

  • PodNode 的关系调度:

PodNode 的关系调度也称为 Node 亲和调度,主要有 NodeSelectorNodeAffinityNode Taints 等用法:

NodeSelector:对 Node 的标签进行选择,然后将 Pod 调度到满足条件的 Node 上

NodeAffinity:和 PodAffinity 类似,可以对 Pod 选择必须调度或优先调度到满足条件的 Node 上

Node Taints:给 Node 打上污点,然后 Pod 必须能容忍 Node 的污点才能调度到该 Node 上
Node Taints:
    一个 Node 可以有多个 Taints
    
    Effect(不能为空):
    
        NoSchedule  禁止新 Pod 调度到该 Node 上
        
        PreferNoSchedule    尽量不调度 Pod 到该 Node 上
        
        NoExecute   Node 会驱逐没有对应 toleration 的 Pod,并且禁止新 Pod 调度到该 Node 上

Pod Tolerations

    一个 Pod 可以有多个 Tolerations
    
    Effect(可以为空,即匹配所有):取值和 Taints 的 Effect 一致
    
    Operator:Exists、Equal

demo-pod Pod yaml 文件 NodeSelector 示例:

apiVersion: v1
kind: Pod
metadata:
  name: demo-pod
  namespace: demo-ns
spec:
  containers:
  - name: demo-container
    image: nginx:latest
    ports:
    - containerPort: 80
  nodeSelector:
    k1: v1

上面示例中,demo-pod 必须要调度到带 k1:v1 标签的节点上,只有找到带 k1:v1 标签的节点,demo-pod 才会调度成功。

demo-pod Pod yaml 文件 NodeAffinity 示例:

apiVersion: v1
kind: Pod
metadata:
  name: demo-pod
  namespace: demo-ns
spec:
  containers:
  - name: demo-container
    image: nginx:latest
    ports:
    - containerPort: 80
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: k1
            operator: In
            values:
            - v1
            - v2

文件解析:

.spec.affinity  声明 Pod 的 affinity 信息

.spec.affinity.podAffinity  声明 Pod 的 affinity 为 Node 亲和调度

.spec.affinity.podAffinity.requiredDuringSchedulingIgnoredDuringExecution   声明 Pod 的 Node 亲和调度的条件必须满足

.spec.affinity.podAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms 声明 Pod 的 Node 亲和调度必须满足的标签是 k1:v1 或 k1:v2

.spec.affinity.podAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.matchExpressions.operator   声明 Pod 的 Node 亲和调度中标签选择器的运算符,有 In、NotIn、Exists、DoesNotExist、Gt、Lt

上面示例中,demo-pod 必须要调度到带 k1:v1k1:v2 标签的节点上,只有找到带 k1:v1k1:v2 标签的节点,demo-pod 才会调度成功。

demo-node Node yaml 文件 Node Taints 示例:

apiVersion: v1
kind: Node
metadata:
  name: demo-node
spec:
  taints:
    - key: "k1"
      value: "v1"
      effect: "NoSchedule"
apiVersion: v1
kind: Pod
metadata:
  name: demo-pod
  namespace: demo-ns
spec:
  containers:
  - name: demo-container
    image: nginx:latest
    ports:
    - containerPort: 80
  tolerations:
  - key: "k1"
    operator: "Equal"               #当 operator 为 Exists 时 value 可为空
    value: "v1"
    effect: "NoSchedule"                # effect 可以为空,即匹配所有

上面示例中,demo-pod 能够容忍 demo-node 的污点。只有 Podtolerations 能够满足 NodetaintsPod 才能够调度成功。


Kubernetes 高级调度

  • 优先级调度:

当集群资源足够的话,只需要通过基础调度就能组合出合理的使用方式。但如果资源不够,怎么做到集群的合理利用呢?通常的策略有两类:

先到先得策略 (FIFO) -简单、相对公平,上手快

优先级策略 (Priority) - 符合日常公司业务特点

假设有一个 Node 已经被一个 Pod 占用了,这个 Node 只有 2 个 CPU。另一个高优先级 Pod 调度过来的时候,低优先级的 Pod 应该把这 2 个 CPU 让给高优先级的 Pod 去使用。低优先级的 Pod 需要回到等待队列,或者是业务重新提交。这样的流程就是优先级抢占调度的流程。

在 Kubernetes 里,PodPriorityPreemption 分别代表优先级和抢占,在 v1.14 版本中变成了 stable,并且 PodPriorityPreemption 默认都是开启的。

  • 优先级调度配置:

如何使用优先级调度呢?需要创建一个 priorityClass,然后再为每个 Pod 配置上不同的 priorityClassName,这样就完成了优先级以及优先级调度的配置。

创建一个名为 highpriorityClass,得分为10000:

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high
value: 10000
globalDefault: false

创建一个名为 lowpriorityClass,得分为100:

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: low
value: 100
globalDefault: false

Pod 配置上 priorityClassName

apiVersion: v1
kind: Pod
metadata:
  name: demo-pod
  namespace: demo-ns
spec:
  containers:
  - name: demo-container
    image: nginx:latest
    ports:
    - containerPort: 80
  priorityClassName: high

Kubernetes 中还内置了默认的优先级,如 DefaultpriorityWhenNoDefaultClassExistis,如果集群中没有配置 DefaultpriorityWhenNoDefaultClassExistis,那所有的 Pod 此项数值都会被设置成 0。

系统级别优先级:SystemCriticalPriority = 20000000000(20 亿),用户可配置最大优先级限制:HighestUserDefinablePriority = 10000000000(10 亿)。

内置系统级别优先级还有:

system-cluster-critical

system-node-critical

可以将特定 PodpriorityClassName 设置为 system-cluster-criticalsystem-node-critical,这就是优先级调度的基本配置以及内置的优先级配置。

  • 优先级调度过程:

当完成优先级配置之后,整个优先级调度又是怎样的流程呢?优先级调度流程可以分为 只触发优先级调度但是没有触发抢占调度的流程 和 触发优先级调度且触发抢占调度的流程。

只触发优先级调度但是没有触发抢占调度的流程:

1. Pod2 和 Pod1 先后进入调度队列,但均未被调度

2. 当进行一轮调度时,调度队列会让 优先级更大的 Pod1 出队列调度

3. 调度成功后,将 Pod1 绑定到节点上,并开始下一轮调度 Pod2

触发优先级调度且触发抢占调度的流程:

1. Pod2 先进行调度,调度成功后绑定至节点上运行

2. 之后 Pod1 再进行调度,由于节点资源不足出现调度失败,此时进入抢占流程

3. 在经过抢占算法计算后,选中 Pod2 作为 Pod1 的让渡者

4. 驱逐节点上运行的 Pod2,并将 Pod1 调度到节点上运行
  • 优先级抢占策略:

整个优先级抢占的调度流程,也就是 Kube-Scheduler 的工作流程。具体的抢占策略和抢占的流程:

1. 优先选择打破 PDB 最少的节点

2. 其次选择待抢占 Pod 中最大优先级最小的节点

3. 再次选择待抢占 Pod 中优先级之和最小的节点

4. 接下来选择待抢占 Pod 数量最小的节点

5. 最后选择拥有最晚启动 Pod 的节点

通过这 5 步串行策略过滤之后,会选出一个最合适的节点,然后对这个节点上待抢占的 Pod 进行 delete,这样就完成了一次抢占的流程。


猜你喜欢

转载自blog.csdn.net/miss1181248983/article/details/112630506