Kubernetes 的资源模型与资源管理

而作为 Kubernetes 的资源管理与调度部分的基础,我们要从它的资源模型开始说起。

在 Kubernetes 里,Pod 是最小的原子调度单位。这也就意味着,所有跟调度和资源管理相关的属性都应该是属于 Pod 对象的字段。而这其中最重要的部分,就是 Pod 的 CPU 和内存配置,如下所示:


apiVersion: v1
kind: Pod
metadata:
  name: frontend
spec:
  containers:
  - name: db
    image: mysql
    env:
    - name: MYSQL_ROOT_PASSWORD
      value: "password"
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"
  - name: wp
    image: wordpress
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"

在 Kubernetes 中,像 CPU 这样的资源被称作“可压缩资源”(compressible resources)。它的典型特点是,当可压缩资源不足时,Pod 只会“饥饿”,但不会退出。

而像内存这样的资源,则被称作“不可压缩资源(incompressible resources)。当不可压缩资源不足时,Pod 就会因为 OOM(Out-Of-Memory)被内核杀掉。

而由于 Pod 可以由多个 Container 组成,所以 CPU 和内存资源的限额,是要配置在每个 Container 的定义上的。这样,Pod 整体的资源配置,就由这些 Container 的配置值累加得到

其中,Kubernetes 里为 CPU 设置的单位是“CPU 的个数”。比如,cpu=1 指的就是,这个 Pod 的 CPU 限额是 1 个 CPU。当然,具体“1 个 CPU”在宿主机上如何解释,是 1 个 CPU 核心,还是 1 个 vCPU,还是 1 个 CPU 的超线程(Hyperthread),完全取决于宿主机的 CPU 实现方式。Kubernetes 只负责保证 Pod 能够使用到“1 个 CPU”的计算能力。

此外,Kubernetes 允许你将 CPU 限额设置为分数,比如在我们的例子里,CPU limits 的值就是 500m。所谓 500m,指的就是 500 millicpu,也就是 0.5 个 CPU 的意思。这样,这个 Pod 就会被分配到 1 个 CPU 一半的计算能力。

当然,你也可以直接把这个配置写成 cpu=0.5。但在实际使用时,我还是推荐你使用 500m 的写法,毕竟这才是 Kubernetes 内部通用的 CPU 表示方式。 

而对于内存资源来说,它的单位自然就是 bytes。Kubernetes 支持你使用 Ei、Pi、Ti、Gi、Mi、Ki(或者 E、P、T、G、M、K)的方式来作为 bytes 的值。比如,在我们的例子里,Memory requests 的值就是 64MiB (2 的 26 次方 bytes) 。这里要注意区分 MiB(mebibyte)和 MB(megabyte)的区别。

备注:1Mi=1024*1024;1M=1000*1000

此外,不难看到,Kubernetes 里 Pod 的 CPU 和内存资源,实际上还要分为 limits 和 requests 两种情况,如下所示:

spec.containers[].resources.limits.cpu
spec.containers[].resources.limits.memory
spec.containers[].resources.requests.cpu
spec.containers[].resources.requests.memory

 这两者的区别其实非常简单:在调度的时候,kube-scheduler 只会按照 requests 的值进行计算。而在真正设置 Cgroups 限制的时候,kubelet 则会按照 limits 的值来进行设置。

更确切地说,当你指定了 requests.cpu=250m 之后,相当于将 Cgroups 的 cpu.shares 的值设置为 (250/1000)*1024。而当你没有设置 requests.cpu 的时候,cpu.shares 默认则是 1024。这样,Kubernetes 就通过 cpu.shares 完成了对 CPU 时间的按比例分配。 

而如果你指定了 limits.cpu=500m 之后,则相当于将 Cgroups 的 cpu.cfs_quota_us 的值设置为 (500/1000)*100ms,而 cpu.cfs_period_us 的值始终是 100ms。这样,Kubernetes 就为你设置了这个容器只能用到 CPU 的 50%。 

而对于内存来说,当你指定了 limits.memory=128Mi 之后,相当于将 Cgroups 的 memory.limit_in_bytes 设置为 128 * 1024 * 1024。而需要注意的是,在调度的时候,调度器只会使用 requests.memory=64Mi 来进行判断。

Kubernetes 这种对 CPU 和内存资源限额的设计,实际上参考了 Borg 论文中对“动态资源边界”的定义,既:容器化作业在提交时所设置的资源边界,并不一定是调度系统所必须严格遵守的,这是因为在实际场景中,大多数作业使用到的资源其实远小于它所请求的资源限额。

基于这种假设,Borg 在作业被提交后,会主动减小它的资源限额配置,以便容纳更多的作业、提升资源利用率。而当作业资源使用量增加到一定阈值时,Borg 会通过“快速恢复”过程,还原作业原始的资源限额,防止出现异常情况。

而 Kubernetes 的 requests+limits 的做法,其实就是上述思路的一个简化版:用户在提交 Pod 时,可以声明一个相对较小的 requests 值供调度器使用,而 Kubernetes 真正设置给容器 Cgroups 的,则是相对较大的 limits 值。不难看到,这跟 Borg 的思路相通的。

在理解了 Kubernetes 资源模型的设计之后,我再来和你谈谈 Kubernetes 里的 QoS 模型。在 Kubernetes 中,不同的 requests 和 limits 的设置方式,其实会将这个 Pod 划分到不同的 QoS 级别当中。

当 Pod 里的每一个 Container 都同时设置了 requests 和 limits,并且 requests 和 limits 值相等的时候,这个 Pod 就属于 Guaranteed 类别,如下所示:

apiVersion: v1
kind: Pod
metadata:
  name: qos-demo
  namespace: qos-example
spec:
  containers:
  - name: qos-demo-ctr
    image: nginx
    resources:
      limits:
        memory: "200Mi"
        cpu: "700m"
      requests:
        memory: "200Mi"
        cpu: "700m"

 当这个 Pod 创建之后,它的 qosClass 字段就会被 Kubernetes 自动设置为 Guaranteed。需要注意的是,当 Pod 仅设置了 limits 没有设置 requests 的时候,Kubernetes 会自动为它设置与 limits 相同的 requests 值,所以,这也属于 Guaranteed 情况。

而当 Pod 不满足 Guaranteed 的条件,但至少有一个 Container 设置了 requests。那么这个 Pod 就会被划分到 Burstable 类别。比如下面这个例子:

apiVersion: v1
kind: Pod
metadata:
  name: qos-demo-2
  namespace: qos-example
spec:
  containers:
  - name: qos-demo-2-ctr
    image: nginx
    resources:
      limits
        memory: "200Mi"
      requests:
        memory: "100Mi"

而如果一个 Pod 既没有设置 requests,也没有设置 limits,那么它的 QoS 类别就是 BestEffort。比如下面这个例子:

apiVersion: v1
kind: Pod
metadata:
  name: qos-demo-3
  namespace: qos-example
spec:
  containers:
  - name: qos-demo-3-ctr
    image: nginx

那么,Kubernetes 为 Pod 设置这样三种 QoS 类别,具体有什么作用呢?

实际上,QoS 划分的主要应用场景,是当宿主机资源紧张的时候,kubelet 对 Pod 进行 Eviction(即资源回收)时需要用到的。

Kubelet Eviction


对于内存和磁盘这种不可压缩的资源,紧缺就相当于不稳定。

驱逐策略

Kubelet 能够监控资源消耗,来防止计算资源被耗尽。一旦出现资源紧缺的迹象,Kubelet 就会主动终止一或多个 Pod 的运行,以回收紧俏资源。当一个 Pod 被终止时,其中的容器会全部停止,Pod 状态会被置为 Failed

驱逐信号

下文中提到了一些信号,kubelet 能够利用这些信号作为决策依据来触发驱逐行为。描述列中的内容来自于 Kubelet summary API。

驱逐信号 描述
memory.available memory.available := node.status.capacity[memory] – node.stats.memory.workingSet
nodefs.available nodefs.available := node.stats.fs.available
nodefs.inodesFree nodefs.inodesFree := node.stats.fs.inodesFree
imagefs.available imagefs.available := node.stats.runtime.imagefs.available
imagefs.inodesFree imµagefs.inodesFree := node.stats.runtime.imagefs.inodesFree

上面的每个信号都支持整数值或者百分比。百分比的分母部分就是各个信号的总量。kubelet 支持两种文件系统分区。

  1. nodefs:保存 kubelet 的卷和守护进程日志等。
  2. imagefs:在容器运行时,用于保存镜像以及可写入层。

imagefs 是可选的。Kubelet 能够利用 cAdvisor 自动发现这些文件系统。Kubelet 不关注其他的文件系统。所有其他类型的配置,例如保存在独立文件系统的卷和日志,都不被支持。

因为磁盘压力已经被驱逐策略接管,因此未来将会停止对现有 垃圾收集 方式的支持。

驱逐阈(yù,音同“预”)值:

一旦超出阈值,就会触发 kubelet 进行资源回收的动作。阈值的定义方式如下:

  • 上面的表格中列出了可用的 eviction-signal.
  • 仅有一个 operator 可用:<
  • quantity 需要符合 Kubernetes 中的描述方式。

例如如果一个 Node 有 10Gi 内存,我们希望在可用内存不足 1Gi 时进行驱逐,就可以选取下面的一种方式来定义驱逐阈值:

  • memory.available<10%
  • memory.available<1Gi

驱逐软阈值

软阈值需要和一个宽限期参数协同工作。当系统资源消耗达到软阈值时,这一状况的持续时间超过了宽限期之前,Kubelet 不会触发任何动作。如果没有定义宽限期,Kubelet 会拒绝启动。

另外还可以定义一个 Pod 结束的宽限期。如果定义了这一宽限期,那么 Kubelet 会使用 pod.Spec.TerminationGracePeriodSeconds 和最大宽限期这两个值之间较小的那个(进行宽限),如果没有指定的话,kubelet 会不留宽限立即杀死 Pod。

软阈值的定义包括以下几个参数:

  • eviction-soft:描述一套驱逐阈值(例如 memory.available<1.5Gi ),如果满足这一条件的持续时间超过宽限期,就会触发对 Pod 的驱逐动作。
  • eviction-soft-grace-period:包含一套驱逐宽限期(例如 memory.available=1m30s),用于定义达到软阈值之后,持续时间超过多久才进行驱逐。
  • eviction-max-pod-grace-period:在因为达到软阈值之后,到驱逐一个 Pod 之前的最大宽限时间(单位是秒),

驱逐硬阈值

硬阈值没有宽限期,如果达到了硬阈值,kubelet 会立即杀掉 Pod 并进行资源回收。

硬阈值的定义:

  • eviction-hard:描述一系列的驱逐阈值(比如说 memory.available<1Gi),一旦达到这一阈值,就会触发对 Pod 的驱逐,缺省的硬阈值定义是:

--eviction-hard=memory.available<100Mi

驱逐监控频率

Housekeeping interval 参数定义一个时间间隔,Kubelet 每隔这一段就会对驱逐阈值进行评估。

  • housekeeping-interval:容器检查的时间间隔。

节点状况


Kubelet 会把驱逐信号跟节点状况对应起来。

如果触发了硬阈值,或者符合软阈值的时间持续了与其对应的宽限期,Kubelet 就会认为当前节点压力太大,下面的节点状态定义描述了这种对应关系。

节点状况 驱逐信号 描述
MemoryPressure memory.available 节点的可用内存达到了驱逐阈值
DiskPressure nodefs.available, nodefs.inodesFree, imagefs.available, imagefs.inodesFree 节点的 root 文件系统或者镜像文件系统的可用空间达到了驱逐阈值

Kubelet 会持续报告节点状态的更新过程,这一频率由参数 —node-status-update-frequency 指定,缺省情况下取值为 10s

节点状况的波动

如果一个节点的状况在软阈值的上下波动,但是又不会超过他的宽限期,将会导致该节点的状态持续的在是否之间徘徊,最终会影响降低调度的决策过程。

要防止这种状况,下面的标志可以用来通知 Kubelet,在脱离压力状态之前,必须等待。

eviction-pressure-transition-period 定义了在跳出压力状态之前要等待的时间。

Kubelet 在把压力状态设置为 False 之前,会确认在周期之内,该节点没有达到逐出阈值。

回收节点级别的资源


如果达到了驱逐阈值,并且超出了宽限期,那么 Kubelet 会开始回收超出限量的资源,直到驱逐信号量回到阈值以内。

Kubelet 在驱逐用户 Pod 之前,会尝试回收节点级别的资源。如果服务器为容器定义了独立的 imagefs,他的回收过程会有所不同。

有 Imagefs

如果 nodefs 文件系统到达了驱逐阈值,kubelet 会按照下面的顺序来清理空间。

  1. 删除死掉的 Pod/容器

如果 imagefs 文件系统到达了驱逐阈值,kubelet 会按照下面的顺序来清理空间。

  1. 删掉所有无用镜像

没有 Imagefs

如果 nodefs 文件系统到达了驱逐阈值,kubelet 会按照下面的顺序来清理空间。

  1. 删除死掉的 Pod/容器
  2. 删掉所有无用镜像


 具体地说,当 Kubernetes 所管理的宿主机上不可压缩资源短缺时,就有可能触发 Eviction。比如,可用内存(memory.available)、可用的宿主机磁盘空间(nodefs.available),以及容器运行时镜像存储空间(imagefs.available)等等。

目前,Kubernetes 为你设置的 Eviction 的默认阈值如下所示:

memory.available<100Mi
nodefs.available<10%
nodefs.inodesFree<5%
imagefs.available<15%

当然,上述各个触发条件在 kubelet 里都是可配置的。比如下面这个例子:

kubelet --eviction-hard=imagefs.available<10%,memory.available<500Mi,nodefs.available<5%,nodefs.inodesFree<5% --eviction-soft=imagefs.available<30%,nodefs.available<10% --eviction-soft-grace-period=imagefs.available=2m,nodefs.available=2m --eviction-max-pod-grace-period=600

在这个配置中,你可以看到 Eviction 在 Kubernetes 里其实分为 Soft 和 Hard 两种模式。

其中,Soft Eviction 允许你为 Eviction 过程设置一段“优雅时间”,比如上面例子里的 imagefs.available=2m,就意味着当 imagefs 不足的阈值达到 2 分钟之后,kubelet 才会开始 Eviction 的过程。

而 Hard Eviction 模式下,Eviction 过程就会在阈值达到之后立刻开始。

Kubernetes 计算 Eviction 阈值的数据来源,主要依赖于从 Cgroups 读取到的值,以及使用 cAdvisor 监控到的数据。

 当宿主机的 Eviction 阈值达到后,就会进入 MemoryPressure 或者 DiskPressure 状态,从而避免新的 Pod 被调度到这台宿主机上。

而当 Eviction 发生的时候,kubelet 具体会挑选哪些 Pod 进行删除操作,就需要参考这些 Pod 的 QoS 类别了。

  • 首当其冲的,自然是 BestEffort 类别的 Pod。
  • 其次,是属于 Burstable 类别、并且发生“饥饿”的资源使用量已经超出了 requests 的 Pod。
  • 最后,才是 Guaranteed 类别。并且,Kubernetes 会保证只有当 Guaranteed 类别的 Pod 的资源使用量超过了其 limits 的限制,或者宿主机本身正处于 Memory Pressure 状态时,Guaranteed 的 Pod 才可能被选中进行 Eviction 操作。 

当然,对于同 QoS 类别的 Pod 来说,Kubernetes 还会根据 Pod 的优先级来进行进一步地排序和选择。

在理解了 Kubernetes 里的 QoS 类别的设计之后,我再来为你讲解一下Kubernetes 里一个非常有用的特性:cpuset 的设置。

cpuset 的设置


 我们知道,在使用容器的时候,你可以通过设置 cpuset 把容器绑定到某个 CPU 的核上,而不是像 cpushare 那样共享 CPU 的计算能力。

这种情况下,由于操作系统在 CPU 之间进行上下文切换的次数大大减少,容器里应用的性能会得到大幅提升。事实上,cpuset 方式,是生产环境里部署在线应用类型的 Pod 时,非常常用的一种方式。

可是,这样的需求在 Kubernetes 里又该如何实现呢?

其实非常简单。

  • 首先,你的 Pod 必须是 Guaranteed 的 QoS 类型
  • 然后,你只需要将 Pod 的 CPU 资源的 requests 和 limits 设置为同一个相等的整数值即可。

 比如下面这个例子: 

spec:
  containers:
  - name: nginx
    image: nginx
    resources:
      limits:
        memory: "200Mi"
        cpu: "2"
      requests:
        memory: "200Mi"
        cpu: "2"

这时候,该 Pod 就会被绑定在 2 个独占的 CPU 核上。当然,具体是哪两个 CPU 核,是由 kubelet 为你分配的。

以上,就是 Kubernetes 的资源模型和 QoS 类别相关的主要内容。

总结


在本篇文章中,我先为你详细讲解了 Kubernetes 里对资源的定义方式和资源模型的设计。然后,我为你讲述了 Kubernetes 里对 Pod 进行 Eviction 的具体策略和实践方式。

正是基于上述讲述,在实际的使用中,我强烈建议你将 DaemonSet 的 Pod 都设置为 Guaranteed 的 QoS 类型。否则,一旦 DaemonSet 的 Pod 被回收,它又会立即在原宿主机上被重建出来,这就使得前面资源回收的动作,完全没有意义了。

猜你喜欢

转载自blog.csdn.net/qq_34556414/article/details/121030904