---
title: 'Kubernetes 控制平面 - 调度器'
ShowRssButtonInSectionTermList: true
date: '2023-09-28T20:29:30+08:00'
draft: false
showtoc: true
tocopen: false
type: posts
author: '熊鑫伟，我'
keywords: ['Kubernetes', '调度器', '容器编排', 'K8s 架构', '集群管理']
tags: ["Blog", "Go", "Kubernetes"]
categories: ["AI & Technology"]
description: '探讨 Kubernetes 的调度器组件如何工作，它是如何决定将容器放置在哪个节点上的，以及这对集群管理和容器编排有何重要意义。'
tldr:
  - "kube-scheduler通过predicate过滤和priority排序两阶段为Pod分配节点,综合考虑资源、亲和性、污点等多维因素实现高效调度。"
  - "CPU和内存的request用于调度决策,limit通过cgroup实现硬限制;init容器只计算资源最多的单个,临时存储由kubelet监控驱逐而非cgroup限制。"
  - "生产环境中可通过nodeSelector、NodeAffinity、podAffinity和Taints实现Pod到指定节点的精准调度,多租户隔离需补充认证防护防止资源被非法占用。"
---

## 调度器

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

调度器需要充分考虑诸多的因素：

+ 公平调度;
+ 资源高效利用;
+ QoS;
+ affinity 和 anti-affinity;
+ 数据本地化(data locality) ;
+ 内部负载干扰(inter-workload interference) ;
+ deadlines。

kube-scheduler 调度分为两个阶段, predicate 和 priority:

+ predicate: 过滤不符合条件的节点;
+ priority:优先级排序，选择优先级最高的节点。.

### predicate 策略

+ PodFitsHostPorts：检查是否有 Host Ports 冲突。
+ PodFitsPorts：同P odFitsHostPorts。
+ PodFitsResources：检查 Node 的资源是否充足，包括允许的Pod数量、CPU、内存、GPU个数以及其他的OpaqueIntResources。
+ HostName：检查 pod.Spec.NodeName 是否与候选节点一致。
+ MatchNodeSelector：检查候选节点的 pod.Spec.NodeSelector 是否匹配
+ NoVolumeZoneConflict：检查 volume zone 是否冲突。
+ MatchInterPodAffinity：检查是否匹配 Pod 的亲和性要求。
+ NoDiskConflict：检查是否存在 Volume 冲突，仅限于 GCE PD、AWS EBS、Ceph RBD以及 iSCSI。
+ PodToleratesNodeTaints：检查 Pod 是否容忍 Node Taints。
+ CheckNodeMemoryPressure：检查 Pod 是否可以调度到 MemoryPressure 的节点上。
+ CheckNodeDiskPressure：检查 Pod 是否可以调度到 DiskPressure 的节点上。
+ NoVolumeNodeConflict：检查节点是否满足 Pod 所引用的 Volume 的条件。

### priority 策略

+ SelectorSpreadPriority：优先减少节点上属于同一个 Service 或 Replication Controller 的 Pod 数量。
  + 尽量将同一个 rc 下的多个副本分散到不同节点，增加可用性
+ InterPodAffinityPriority：优先将Pod调度到相同的拓扑上(如同一个节点、Rack、Zone等)。
+ LeastRequestedPriority：优先调度到请求资源少的节点上。
+ BalancedResourceAllocation：优先平衡各节点的资源使用。
+ NodePreferAvoidPodsPriority：alpha.kubernetes.io/preferAvoidPods字段判断，权重为10000，避免其他优先级策略的影响
+ NodeAffinityPriority：优先调度到匹配NodeAffinity的节点上。
+ TaintTolerationPriority：优先调度到匹配TaintToleration的节点上。
+ ServiceSpreadingPriority：尽量将同一个service的Pod分布到不同节点上，已经被SelectorSpreadPriority替代( 默认未使用)。
+ EqualPriority：将所有节点的优先级设置为1 (默认未使用)
+ ImageLocalityPriority：尽量将使用大镜像的容器调度到已经下拉了该镜像的节点上(默认未使用)
+ MostRequestedPriority：尽量调度到已经使用过的Node.上，特别适用于cluster-autoscaler (默认未使用)

### 资源需求

CPU

+ requests
  + Kubernetes 调度 Pod 时，会判断当前节点正在运行的 Pod 的 CPU Request 的总和，再加上当前调度Pod 的 CPU request,计算其是否超过节点的 CPU 的可分配资源
+ limits
  + 配置 cgroup 以限制资源上限

内存

+ requests
  + 判断节点的剩余内存是否满足 Pod 的内存请求量，以确定是否可以将 Pod 调度到该节点
+ limits
  + 配置 cgroup 以限制资源上限

### request & limit 和 cgroups

k8s 中 request 作为调度用，节点剩余资源满足 request 值即可调度，limit 在 k8s 系统中没有作用，只是会传递给 cri。

在 cri 中，使用 cgroup 限制资源时，是如何对应的呢？

以 cpu 资源为例：

+ 1）request 中的值会体现在 cpu.shares 中
  + 比如 cpu request 为 0.5，那么 cgroups 中的 cpu.shares 就是 0.5*1024 = 512
  + 如果是 2 那么 cpu.shares 就是 2048
+ 2）limits 中的值会体现在 cpu.cfs_period_us 和 cpu.cfs_quota_us 中
  + 二者是绝对值，因此可以用于做硬限制

### 磁盘资源需求

容器临时存储(ephemeral storage)包含日志和可写层数据，可以通过定义 Pod Spec 中的 limits.ephemeral-storage 和 requests.ephemeral-storage来申请。 Pod 调度完成后，**计算节点对临时存储的限制不是基于 CGroup的，而是由 kubelet 定时获取容器的日志 和容器可写层的磁盘使用情况，如果超过限制，则会对 Pod 进行驱逐**。

### init container 资源需求

当 kube-scheduler 调度带有多个 init 容器的 Pod 时，**只计算 cpu.request 最多的 init 容器**，而不是计 算所有的 init 容器总和。 **由于多个 init 容器按顺序执行，并且执行完成立即退出，所以申请最多的资源 init 容器中的所需资源即可满足所有 init 容器需求。** kube-scheduler 在计算该节点被占用的资源时，init 容器的资源依然会被纳入计算。因为 init 容器在 特定情况下可能会被再次执行，比如由于更换镜像而引起 Sandbox 重建时。

### 把 Pod 调度到指定 Node 上

可以通过 nodeSelector、nodeAffinity、 podAffinity 以及 Taints 和 tolerations 等来将 Pod 调度到需要的 Node上。也可以通过设置 nodeName 参数，将 Pod 调度到指定 node 节点上。

比如，使用 nodeSelector,首先给 Node 加上标签

```
kubectl label nodes <your-node-name> disktype=ssd
```

接着，指定该 Pod 只想运行在带有 disktype=ssd 标签的 Node 上。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx
      nodeSelector:
        disktype: ssd
```

#### nodeSelector

首先给Node打上标签:

```bash
kubectl label nodes node-01 disktype=ssd
```

然后在daemonset中指定nodeSelector为disktype: =ssd:

```yaml
spec:
  nodeSelector:
    disktype: ssd
```



#### NodeAffinity

NodeAffinity 目前支持两种: `requiredDuringSchedulinglgnoredDuringExecution` 和 `preferredDuringSchedulinglgnoredDuringExecution`。

分别代表必须满足条件和优选条件。 比如下面的例子代表**优先**调度到包含标签 disktype=ssd 的 node 上。

> 如果所有节点都不满足该条件，也可以调度。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 1
            preference:
              matchExpressions:
                - key: disktype
                  operator: In
                  values:
                    - ssd
      containers:
        - name: nginx
          image: nginx
```

比如下面的例子代表**只能**调度到包含标签 disktype=ssd 的 node 上。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: disktype
                    operator: In
                    values:
                      - ssd
      containers:
        - name: nginx
          image: nginx
```



#### podAffinity

**podAffinity 基于 Pod 的标签来选择 Node**,仅调度到满足条件 Pod 所在的 Node上，支持 podAffinity 和 podAntiAffinity。

这个功能比较绕，以下面的例子为例:

如果一个"Node 上运行的 pod 中包含至少一个带有 a=b 标签"，那么可以调到该 Node,同时还不能调度到“包含至少一个带有 app=anti-nginx 标签且运行中 Pod"的 Node 上。

```
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-anti
spec:
  replicas: 2
  selector:
    matchLabels:
      app: anti-nginx
  template:
    metadata:
      labels:
        app: anti-nginx
    spec:
      affinity:
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
                - key: a
                  operator: In
                  values:
                    - b
            topologyKey: kubernetes.io/hostname
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
                - key: app
                  operator: In
                  values:
                    - anti-nginx
            topologyKey: kubernetes.io/hostname
      containers:
        - name: with-pod-affinity
          image: nginx
```

#### Taints & Tolerations

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

目前支持的Taint类型:

+ **NoSchedule**: 新的 Pod 不调度到该Node.上，不影响正在运行的 Pod;
+ **PreferNoSchedule**: soft 版的 NoSchedule,尽量不调度到该 Node 上;
+ **NoExecute**：新的 Pod 不调度到该 Node 上，并且删除(evict) 已在运行的 Pod。Pod可以增加一个时间(tolerationSeconds) ，在该时间到之后才被移除掉

然而，当 Pod 的 Tolerations 匹配 Node 的所有 Taints 的时候可以调度到该 Node 上;当 Pod 是已经运行的时候，也不会被删除(evicted) 。另外对于NoExecute,如果Pod增加了一个 tolerationSeconds,则会在该时间之后才删除Pod。

### 多租户Kubernetes集群-计算资源隔离

Kubernete s集群一般是通用集群，可被所有用户共享，用户无需关心计算节点细节。 但往往某些自带计算资源的客户要求:

+ 带着计算资源加入Kubernetes集群;
+ 要求资源隔离。

实现方案:

+ 将要隔离的计算节点打上Taints;
+ 在用户创建创建Pod时，定义tolerations来指定要调度到node taints。

该方案有漏洞吗?如何堵住?

+ 其他用户如果可以 get nodes 或者 pods,可以看到 taints 信息， 也可以用相同的 tolerations 占用资源。
+ 不让用户get node detail?
+ 不让用户get别人的pod detail?
+ 企业内部，也可以通过规范管理，通过统计数据看谁占用了哪些node;
+ 数据平面上的隔离还需要其他方案配合。

来自生产系统的经验

+ 用户会忘记打tolerance,导致pod无法调度, pending;
+ 新员工常犯的错误，通过聊天机器人的Q&A解决;
+ 其他用户会get node detail,查到Taints,偷用资源。
+ 通过dashboard,能看到哪些用户的什么应用跑在哪些节点上;
+ 对于违规用户，批评教育为主。

### 优先级调度

从v1.8开始，kube-scheduler 支持定义 Pod 的优先级，从而保证高优先级的 Pod 优先调度。开启方 法为:

+ apiserver 配置--feature-gates=PodPriority=true和--runtime-config=scheduling.k8s.io/v1alpha1=true
+ kube-scheduler 配置--feature-gates=PodPriority=true

### priorityClass

在指定 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.'
```

然后为 pod 设置 priorityClass：

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

### 多调度器

如果默认的调度器不满足要求，还可以部署自定义的调度器。并且，在整个集群中还可以同时运行多个调度器实例，通过 **podSpec.schedulerName** 来选择使用哪一个调度器( 默认使用内置的调度器)。

### 来自生产的经验

小集群:

+ 100 个 node,并发创建 8000 个 Pod 的最大调度耗时大概是 2 分钟左右，发生过 node 删除后，scheduler cache 还有信息的情况，导致 Pod 调度失败。

放大效应:

+ 当一个 node 出现问题所以 load 较小时，通常用户的 Pod 都会优先调度到该 node 上，导致用户所有创建的新 Pod 都失败的情况。

应用炸弹:

+ 存在危险的用户 Pod (比如fork bomb)，在调度到某个 node 上以后，会因为打开文件句柄过多导致 node 宕机，Pod 会被 evict 到其他节点，再对其他节点造成伤害，依次循环会导致整个 cluster 所有节点不可用。

**调度器可以说是运营过程中稳定性最好的组件之一, 基本没有太大的维护 effort。**

## FAQ

### init container 资源回收

根据 init container 资源需求章节可知，pod 启动后，init container 的资源还是不会回收。

场景：**init container 初始化时需要大量资源，等初始化完成后就降下去了，这种情况如何进行优化？**

> CPU 资源可以进行压缩，大不了初始化慢一点，但是内存资源则不行，少了直接 OOM。

原生 k8s 对这种情况没有优化，社区有一个**纵向扩容**的 feature，可以动态调整 pod 需要的资源，不过支持度不如横向扩容。

### 计算密集型Pod如何锁死 CPU

cpuset，将 Pod 和某个 CPU 核进行绑定，kubelet 支持 static cpu config

+ 将 pod 的 request 和 limits 配置为一样的，该 pod 在 k8s 中的 Qos 等级就是 BestEffort，对应该等级的 Pod，如果 kubelet 配置了 static cpu config，就会自动绑定
