Kubernetes笔记(七) Kuberetes调度


所谓调度就是按照一系列的需求、规则,将 Pod 调度到合适的 Node 上。下面是 Kubernetes 提供的一些调度方式:

1. 手动调度

Pod 的定义中有 nodeName 属性,调度器就是在选择出最合适的节点后修改 Pod 的 nodeName 来指定 Pod 的运行节点,我们可以在定义 Pod 时直接指定,示例如下,这样该 Pod 就会被调度到 node02 节点。

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx
  # 指定节点名称
  nodeName: node02

2. NodeSlector

可以通过在 Node 打标签,然后使用标签选择器将 Pod 调度到对应节点。比如我们希望某些执行 IO 任务的 Pod 调度到磁盘类型为 ssd 的节点上,可以先在节点上打标签 disktype: ssd 然后使用 NodeSelector 将 Pod 调度到对应节点,示例如下:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    env: test
spec:
  containers:
  - name: nginx
    image: nginx
    imagePullPolicy: IfNotPresent
  # 配置 nodeSelector
  nodeSelector:
    disktype: ssd

3. Node & Pod Affinity

nodeSelector 只能简单的根据标签是否相等来进行调度,会被逐渐弃用。现在更推荐使用拥有更强大的节点关联规则,调度更加灵活的 Node/Pod Affinty (亲和性)。
亲和性规则分为 Node Affinity 和 Pod Affinity 两种,下面是 Node Affinity (节点亲和度)的示例:

apiVersion: v1
kind: Pod
metadata:
  name: with-node-affinity
Spec:
  # 设置亲和度
  affinity:
    # 设置节点亲和度
    nodeAffinity:
      # 指定 affinity 类型
      requiredDuringSchedulingIgnoredDuringExecution:
        # 指定若干个规则
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/e2e-az-name
            operator: In
            values:
            - e2e-az1
            - e2e-az2
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: another-node-label-key
            operator: In
            values:
            - another-node-label-value
  containers:
  - name: with-node-affinity
    image: k8s.gcr.io/pause:2.0

首先要指定 Affinity 的规则类型,主要有下面三类
已有类型
requiredDuringSchedulingIgnoredDuringExecution:表示节点只能在满足匹配规则的情况下,Pod 才会被调度上去。
preferredDuringSchedulingIgnoredDuringExecution:表示会优先将 Pod 调度到那些满足匹配规则的节点上,实在没有的话也可以调度到其他节点。
计划类型
requiredDuringSchedulingRequiredDuringExecution:这是在计划中的特性,目前还没有实际使用,和下面要讲到的 Taint 的 NoExecute 效果很像,其会影响到已经运行的 Pod。

虽然 affinity 规则类型的名字看着很长,但其语义还是很清晰的,由 类型 和 作用时期 组成。如图:
在这里插入图片描述

DuringSchedulingDuringExecution 分别表示对调度期和运行期的要求,调度期只会在部署调度新的 Pod 时生效,而运行期则会影响正在运行的 Pod。
required 表示必须满足条件;preferred 表示尽量满足,也就是说会优先将 Pod 调度到满足亲和性规则的节点上,如果找不到也可以调度到其他节点。

NodeAffinity 的 Operator 支持 In, NotIn, Exists, DoesNotExist, Gt, Lt 这几种操作,我们可以通过 NotIn、DoesNotExist 支持反亲和操作。

另外有下面几条亲和规则需要注意

  • 如果同时指定 nodeSelector 和 nodeAffinity,则必须同时满足两个条件,才能将Pod调度到候选节点上。
  • 如果 nodeAffinity 的某个类型关联了多个 nodeSelectorTerms,只需要满足其中之一,就可以将 Pod 调度到节点上。
  • 如果 nodeSelectorTerms 下有多个 matchExpressions,则只有在满足所有matchExpressions的情况下,才可以将 Pod 调度到一个节点上。

最后对于 preferredDuringSchedulingIgnoredDuringExecution 还会有一个 weight 权重字段,用来在调度时结合其他条件计算 Node 的优先级,Pod 最终会调度到优先级最高的 Node 上。
除了节点亲和度,还有 Pod 亲和度、反亲和度(anti-affinity)来指定使 Pod 优先与某些 Pod 部署到一起或者不与某些 Pod 部署到一起,下面是一个官网的例子:

apiVersion: v1
kind: Pod
metadata:
  name: with-pod-affinity
spec:
  affinity:
    # Pod 亲和度,与标签匹配的 Pod 部署在一起
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: security
            operator: In
            values:
            - S1
        topologyKey: topology.kubernetes.io/zone
    # 反亲和,优先部署在没有对应标签的 Pod 运行的节点上
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: security
              operator: In
              values:
              - S2
          topologyKey: topology.kubernetes.io/zone
  containers:
  - name: with-pod-affinity
    image: k8s.gcr.io/pause:2.0

不过在官网的建议中,Pod 亲和、反亲和的调度规则会降低集群的调度速度,因此不建议在超过数百个节点中的集群中使用。

4. Resource Request

在定义 Pod 时可以选择性地为每个容器设定所需要的资源数量。 最常见的可设定资源是 CPU 和内存(RAM)大小,从而使得 Pod 调度到符合资源需求的节点上,示例如下:

apiVersion: v1
kind: Pod
metadata:
  name: frontend
spec:
  containers:
  - name: app
    image: images.my-company.example/app:v4
    env:
    - name: MYSQL_ROOT_PASSWORD
      value: "password"
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"

这里有 requests 和 limits 两个设置项:

  • requests:给调度器使用,scheduler 根据该值进行调度决策,在执行调度时,会以 Pod 中所有容器的 request 值总和作为判断。
  • limits:资源使用配额,给 cGroups 使用,用来限制容器资源的使用。

Pod 对特定资源类型的请求/约束值是 Pod 中各容器对该类型资源的请求/约束值的总和。

下面的例子中, Pod 有两个 Container,每个 Container 的请求为 0.25 cpu 和 64MiB 内存, 每个容器的资源约束为 0.5 cpu 和 128MiB 内存。 可以认为该 Pod 的资源请求为 0.5 cpu 和 128 MiB 内存,资源限制为 1 cpu 和 256MiB 内存。当执行调度时,资源是否充足的依据也是基于节点上各个 Pod 的 request 之和来计算的,而不是依据实际使用的情况。

apiVersion: v1
kind: Pod
metadata:
  name: frontend
spec:
  containers:
  - name: app
    image: images.my-company.example/app:v4
    env:
    - name: MYSQL_ROOT_PASSWORD
      value: "password"
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"
  - name: log-aggregator
    image: images.my-company.example/log-aggregator:v6
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"

根据谷歌的 brog 论文,在实际操作中人们往往会过度请求资源,大多数实际运行的应用真正用到的资源往往远小于其所请求的配额。

Kubernetes 使用上述两个配置项来设定资源的使用,并且基于该配置项确定 Pod 的服务质量等级(Quality of Service Level,QoS Level):

QoS 等级有三类,基于 limits 和 requests 确定:

  • Guaranteed:最高服务等级,当 Pod 中所有容器都设置了limits 和 requests 并且值相等时,此时 Pod 的 Qos 是 Guaranteed,资源不足时优先保证该类 Pod 的运行。

  • Burstable:Pod 中有容器只设置了 requests 没有设置 limits,或者 requests 的值小于 limits 值,此时 QoS 为 Burstable。

  • BestEffort: Pod 中容器都没有设置 limits 和 requests,资源不足时优先杀死这类 Pod。

不难看出,Kuberetes 鼓励我们按实际需要分配资源,如果我们随意设置甚至不设置资源,则 Kubernetes 会做出“惩罚”,优先将这类 Pod 驱逐。

下图是 容器 的 QoS 等级与 Pod 的关系

在这里插入图片描述

5. Taints & Tolerations

上面提到的规则基本都是表示将 Pod 调度到哪个节点,而对于某些节点,我们希望 Pod 不要调度到该节点上去。此时可以通过给 Node 打 Taint(污点) 的方式实现。

给 Node 打污点的命令格式如下:

$ kubectl taint nodes node name key=value:taint effect
  • key 代表污点的键

  • value 代表污点的值,可以省略

  • taint effect 代表污点的效果,有下面三个可选值

    • NoSchedule:如果 Pod 没有容忍该污点,则不会被调度到打上该污点的 Node 上,但已运行的 Pod 不受影响。
    • PreferNoSchedule:如果 Pod 没有容忍该污点,则尽量不让其调度到打上该污点的节点上。 实在没有其他 Node 可用了才会调度。
    • NoExecute:前两种效果影响的只是调度期,而该效果会影响运行期,如果向节点添加了该作用的污点,则已运行在该 Node 上的没有忍受该污点的 Pod 会被驱逐。

下面看几个示例。

  1. 添加、查看与移除污点

给 node2 节点添加两个污点

# key 为 node-type,value 为 production,效果为 NoSchedule
$ kubectl taint node node2 node-type=production:NoSchedule
node/node2 tainted

# key 为 isProduct,value 省略,效果为 NoSchedule
$ kubectl taint node node2 isProduct=:NoSchedule
node/node2 tainted

新建两个 Pod 并且没有容忍上述的污点,可以看到新的 Pod 都调度到了 vm-0-4-ubuntu 节点上。

$ kubectl run redis --image=redis --labels="app=redis"
pod/redis created

$ kubectl run nginx --image=nginx
pod/nginx created

$ kubectl get pods -o wide
NAME    READY   STATUS    RESTARTS   AGE   IP          NODE            NOMINATED NODE   READINESS GATES
nginx   1/1     Running   0          55s   10.32.0.7   vm-0-4-ubuntu   <none>           <none>
redis   1/1     Running   0          58s   10.32.0.8   vm-0-4-ubuntu   <none>           <none>

现在去掉 node2 上的两个污点,然后将 vm-0-4-ubuntu 节点打上 NoExecute 效果的污点,看上面的 Pod 是否被驱逐。

移除污点的方式很简单,在污点最后面加 - 即可,如下:

$ kubectl taint node node2 isProduct=:NoSchedule-
node/node2 untainted

现在将 vm-0-4-ubuntu 打上新的效果为 NoExecute 的污点

$ kubectl taint node vm-0-4-ubuntu node-type=production:NoExecute
node/vm-0-4-ubuntu tainted

可以看到 Pod 已经被驱逐了,如果 Pod 是由 Deployment 等控制的,那应该会重新调度到 node2 上。

  1. 设置 Pod 容忍污点

如果我们不想移除污点但是依然想让 Pod 调度到该节点的话,就需要给 Pod 添加 Tolerations(容忍度) 了。示例如下:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    env: test
spec:
  containers:
  - name: nginx
    image: nginx
    imagePullPolicy: IfNotPresent
  tolerations:
  - key: "key1"
    operator: "Equal"
    value: "value1"
    effect: "NoSchedule"
  - key: "example-key"
    operator: "Exists"
    effect: "NoSchedule"

operator 有两种:

  • Equal:这是默认值,表示容忍某个 key 等于 value,并且 effect 为对应效果的污点。
  • Exists:用于判断没有 value 的污点,表示容忍如果某个 key 存在且 effect 为对应效果的污点。

另外这里还有两种特殊情况:

  • key 为空并且 operator 为 Exists,表示容忍所有污点
  • effect 为空,表示容忍所有与 key 匹配的污点。
  tolerations:
  - key: "key1"
    operator: "Equal"
    value: "value1"
    effect: ""
  - key: ""
    operator: "Exists"
    effect: "NoSchedule"

针对 NoExecute 类型的污点,还有一个 tolerationSeconds 的配置,表示可以容忍某个污点多长时间。

tolerations:
- key: "key1"
  operator: "Equal"
  value: "value1"
  effect: "NoExecute"
  tolerationSeconds: 3600

上面我们提到如果打上 NoExecute 效果的污点,会将正在运行的没有容忍该污点的 Pod 驱逐出去,如果加上 tolerationSeconds 配置,则 Pod 会继续运行,如果超出 tolerationSeconds 时间后还没有结束的话则会被驱逐。

比如,一个使用了很多本地状态的应用程序在网络断开时,仍然希望停留在当前节点上运行一段较长的时间, 愿意等待网络恢复以避免被驱逐。在这种情况下,Pod 的容忍度可能是下面这样的:

tolerations:
- key: "node.kubernetes.io/unreachable"
  operator: "Exists"
  effect: "NoExecute"
  tolerationSeconds: 6000

6. Pod 驱逐

在基于资源进行调度一节中提到,当资源不足时 Kubernetes 会将 QoS 等级较低的 Pod 杀死,该过程在 Kubernetes 中称为驱逐(Eviction)。

计算机资源可以分为两类:

  • 可压缩资源:像 CPU 这类资源,当资源不足时,Pod 会运行变慢,但不会被杀死。
  • 不可压缩资源:像磁盘、内存等资源,当资源不足时 Pod 会被杀死,比如发生内存溢出时 Pod 被直接终止。

Kubernetes 默认设置了一系列阈值,当不可压缩资源达到阈值时,kubelet 就会执行驱逐机制。
主要的阈值有下面几个:

- memory.available < 100Mi # 可用内存
- nodefs.available < 10%   # 可用磁盘空间
- nodefs.inodesFree < 5%   # 文件系统可用 inode 是数量
- imagefs.available < 15%  # 可用的容器运行时镜像存储空间

另外驱逐机制中还有软驱逐(soft eviction)硬驱逐(hard eviction)以及优雅退出期(grace period)的概念:

  • 软驱逐:一个较低的警戒线,资源持续超过该警戒线一段时间后,会触发 Pod 的优雅退出,系统通知 Pod 做必要的善后清理,然后自行结束。超出优雅退出期后,系统会强行杀死未自动退出的 Pod。

  • 硬驱逐:配置一个较高的警戒线,一旦触及此红线,则立即强行杀死 Pod,不会优雅退出。

之所以出现这样更加细化的概念,是因为驱逐 Pod 是一种严重的破坏行为,可能导致服务中断,因此需要兼顾系统短时间的资源波动和资源剧烈消耗影响到高服务质量的 Pod 甚至集群节点的情况。

Kubelet 启动时默认配置文件是 /etc/kubernetes/kubelet-config.yaml,可以通过修改该文件 然后重启 Kubelet 来修改上述阈值配置,示例如下:

apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
nodeStatusUpdateFrequency: "10s"
failSwapOn: True
...
...
# 配置硬驱逐阈值
eventRecordQPS: 5
evictionHard:
  nodefs.available:  "5%"
  imagefs.available:  "5%"

7, 调度过程

Kubernetes 调度过程图所示:
在这里插入图片描述

图片来自:https://icyfenix.cn/immutable-infrastructure/schedule/hardware-schedule.html

主要有下面几个步骤:

  • Informer Loop: 持续监听 etcd 中的资源信息,当 Pod、Node 信息发生变化时触发监听,更新调度缓存和调度队列。

  • Scheduler Loop: 该步骤主要是从优先级调度队列中获取要调度的 Pod,并基于调度缓存中的信息进行调度决策,主要有如下过程:

    • Predicates: 过滤阶段,本质上是一组节点过滤器,基于一系列的过滤策略,包括我们上面提到的这些调度规则的设定,比如亲和度都是在这里起作用。只有满足条件的节点才会被筛选出来。

    • Priorities: 打分阶段,所有可用节点被过滤筛选出来后会进入打分阶段,基于各种打分规则给 Node 打分以选出最合适的节点后进行调度。具体的过滤、打分策略可以参考文档 Scheduling Policies

    • Bind:经过过滤打分最终选出合适的 Node 后,会更新本地调度缓存闭关通过异步请求的方式更新 Etcd 中 Pod 的 nodeName 属性。这样如果调度成功则本地缓存与 Etcd 中的信息向保持一致,如果调度失败,则会通过 Informer 循环更新本地缓存,重新调度。

另外为了提升调度性能:

  • 调度过程全程只和本地缓存通信,只有在最后的 bind 阶段才会向 api-server 发起异步通信。
  • 调度器不会处理所有的节点,而是选择一部分节点进行过滤、打分操作。

8. 自定义调度器

除了默认的 Kubernetes 默认提供的调度器,我们还可以自定义调度器并在集群中部署多个调度器,然后在创建 Pod 选择使用的调度器。

下面是一个基于官方的 scheduler 的例子,在集群中部署另一个调度器。

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    component: scheduler
    tier: control-plane
  name: my-scheduler
  namespace: kube-system
spec:
  selector:
    matchLabels:
      component: scheduler
      tier: control-plane
  replicas: 1
  template:
    metadata:
      labels:
        component: scheduler
        tier: control-plane
        version: second
    spec:
      serviceAccountName: my-scheduler
      containers:
      - command:
        - /usr/local/bin/kube-scheduler
        - --config=/etc/kubernetes/my-scheduler/my-scheduler-config.yaml
        image: gcr.io/my-gcp-project/my-kube-scheduler:1.0
        livenessProbe:
          httpGet:
            path: /healthz
            port: 10251
          initialDelaySeconds: 15
        name: kube-second-scheduler
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    component: scheduler
    tier: control-plane
  name: my-scheduler
  namespace: kube-system
spec:
  selector:
    matchLabels:
      component: scheduler
      tier: control-plane
  replicas: 1
  template:
    metadata:
      labels:
        component: scheduler
        tier: control-plane
        version: second
    spec:
      serviceAccountName: my-scheduler
      containers:
      - command:
        - /usr/local/bin/kube-scheduler
        - --config=/etc/kubernetes/my-scheduler/my-scheduler-config.yaml
        image: gcr.io/my-gcp-project/my-kube-scheduler:1.0
        livenessProbe:
          httpGet:
            path: /healthz
            port: 10251
          initialDelaySeconds: 15
        name: kube-second-scheduler

上面是如何配置的一个新的调度器。对于如何按需实现自己的服务器,Kubernetes 还提供了 Kubernetes Scheduling Framework 来帮我们进行开发。

上面提到 Kubernetes 调度过程分为过滤、打分等一系列阶段,对于这些阶段 Scheduling Framework 提供了一系列的接口使得我们可以自己实现对应的处理,从而实现自定义调度。
下面是主要的扩展点。

在这里插入图片描述

图片来自 https://kubernetes.io/docs/concepts/scheduling-eviction/scheduling-framework/

具体到代码中就是实现相应的接口,因为是内部扩展机制,因此在修改后需要重新编译部署。具体代码可以参考 scheduler-plugins 的代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值