测试开发之路 k8s 详解之资源管理

孙高飞 · 2021年11月01日 · 最后由 孙高飞 回复于 2021年11月01日 · 8048 次阅读

前言

之前的一篇帖子里我讲过容器技术是如何利用 cgroups 来做资源限制的, 帖子链接:http://testerhome.com/articles/18471 。 这篇帖子主要是从 linux 底层的技术来讲解如何在容器生态中来控制资源使用。 那么今天我们从 k8s 的应用角度来看一下资源管理在 k8s 中的玩法。

资源模型

在 kubernetes 中,任何可以被申请、分配,最终被使用的对象,都是 kubernetes 中的资源,k8s 默认 只支持 CPU 和内存的定义, 后续可以通过 device plugin 来扩展其他资源的使用,比如 GPU。

可压缩资源和不可压缩资源

所有的资源类型,可以被划分为两大类:可压缩和不可压缩的。

  • 可压缩: 如果系统限制或者缩小容器对可压缩资源的使用的话,只会影响服务对外的服务性能,比如 CPU 就是一种非常典型的可压缩资源。 如果容器的 CPU 使用草果了申请的上限, linux 会通过公平调度算法和 cgroups 对这个容器进行限速。 限速行为并不会影响容器的运行, 只是申请不到更多的 CPU 会让服务性能跟不上去。
  • 对于不可压缩资源来说,资源的紧缺是有可能导致服务对外不可用的,比如内存就是一种非常典型的不可压缩资源。 如果内存的使用超过了限制, 就会触发 OOMKilled。 因为它是不可压缩的资源,申请不到新的内存就会直接跪掉。

所以当资源超过了设置的值, 会触发什么样的行为, 都要看它属于什么资源类型以及 cgroups 如何对其进行处理。

资源申请

kubernetes 中 pod 对资源的申请是以容器为最小单位进行的,针对每个容器,它都可以通过如下两个信息指定它所希望的资源量:

resources:  
 requests:    
   cpu: 2.5   
   memory: "40Mi"  
 limits:     
   cpu: 4.0    
   memory: "99Mi"
  • request:可以理解为 k8s 为容器预留的资源量。 即便容器没有实际使用到这些资源, k8s 也会为容器预留好这些资源, 也就是说其他容器是无法申请这些资源的。
  • limit:可理解为 k8s 限制容器使用的资源上限。 也就是限制容器在实际运行的时候不能超过的资源数值。 如果容器使用的资源超过了这个值, 就会触发后续对应的操作。 对于 CPU 来说, 由于 CPU 是可压缩资源, 所以如果容器使用的 CPU 超过了 limit 设置的值, 操作系统只会对其进行限速,不让容器的 cpu 使用量超过 limit。 但对于内存这种不可压缩资源来说, 如果内存的使用量超过了 limit 的值, 则会触发 OOMKilled(我们在 k8s 中容器状态里有时候会看到容器的 last state 是 OOMKilled,表明上次重启是因为内存用超了)。

注意

  • k8s 在计算资源时是使用 request 字段进行计算的。 一个 k8s 集群如果有 10 个 CPU 的资源。 POD A 申请 request:5,limit:10, POD B 申请 request 5,limit 10. 是可以申请成功的。 而如果在启动一个 POD C 申请 request 5, limit 10. 就会失败(POD 处于 pending 状态, 一直在等待资源释放)。 因为 k8s 并不是按 limit 字段来计算资源用量而是使用 request 字段进行计算
  • request 字段的资源申请是一种逻辑概念上的申请。 是 k8s 内部进行计算的,无法影响外部。 比如还是上面的场景, A 和 B 申请的 request 已经把整个集群的 CPU 都沾满了, 这时候通过 k8s 启动一个 POD 申请 CPU 肯定是不会成功的。 但是不同过 k8s,比如使用原生 docker 命令或者通过 k8s 启动 pod 但是 request 和 limit 都设置为空(不申请资源,也就是这个 pod 可以使用当前节点的所有资源)去启动一个新的容器是可以成功的。 所以 reqeust 字段的资源控制只存在于 k8s 中,并不是操作系统级别的资源申请。
  • limit 的资源限制是通过 cgroups 来进行限制的。 每个 POD,每个容器都会在/sys/fs/cgroup 下留有对应的记录。

资源超卖

通过上面讲解的资源模型, 我们就可以有一种常用的玩法:超卖。 超卖的意思也就是说本来系统只有 10 个 CPU 的资源, 但是容器 A,B,C,D 都各自需要申请 5 个 CPU 的资源,这明显不够用。 但是我们又知道 A, B, C 不可能都在同一时刻都占满 5 个 CPU 的资源的, 因为每个服务都是有它业务的高峰期和低谷期的。 高峰期的时候可以占满 5 个 CPU, 但是服务大部分都处于低谷期,可能只占用 1,2 个 CPU。 所以如果直接写 request:5 的话,很多时候资源是浪费的(上面说过 k8s 里即便容器没有使用到那么多资源, k8s 也会为容器预留 request 字段的资源)。 所以我们可以为容器申请这样的资源: request:2, limit:5. 这样上面 4 个容器加起来只申请了 8 个 CPU 的资源, 而系统里有 10 个 CPU, 是完全可以申请的到的。 而每个容器的 limit 又设置成了 5, 所以每个容器又都可以去使用 5 个 CPU 的资源。

注意

  • 超卖的玩法赌的就是容器不会在同一时刻都处于高峰期,利用超卖来达到资源在进行限制的同时又能最大化的提高资源利用率。 但是这种玩法比较容易玩脱, 如果 request 的值设置的过于小,limit 的值设置的过于大, 那么在业务高峰期把资源玩炸了的情况还是有可能的。 毕竟我们经常能看到一个 k8s 节点上的 limit 资源总量超过 200%,甚至 300% 的。 一旦这些容器的压力上来, 这个节点就要炸了。 所以在超卖场景中, 对于 request 和 limit 具体要设置什么值是合理的, 是需要一段时间的业务压测来验证并计算的。
  • 一般设置 request 和 limit 的策略是, 如果这个服务特别重要,容不得出半点闪失, 那么 request 和 limit 的值要设置成一样的。保证这块资源一定会留给这个容器来使用(这里也涉及到驱逐策略,后面说)。 如果这个服务不那么重要, 可以 request 的值设置成这个服务的最小消耗量或者业务压力处于平稳期的资源用量, 而 limit 设置为服务高峰期的值。 如果这个服务本身占用的资源可以忽略不计, 或者这个服务根本不重要, 挂了就挂了。 那可以把 reqeust 和 limit 都设置为 0, 这样平时它可以肆意使用资源, 但在资源紧俏的时候 k8s 的驱逐策略就会优先把这种服务驱逐掉节省资源(驱逐策略后面说)。

ResourceQuota(资源配额)

k8s 有多种管理资源的策略, 资源配合是其中一种, 在 k8s 中 namespace 可以当做成一个租户。 我们可以针对这个租户的资源用量进行限制。 比如下面的配置定义。

apiVersion: v1
kind: Namespace
metadata:
  name: myspace
---
apiVersion: v1
kind: ResourceQuota
metadata:
  name: compute-quota
  namespace: myspace
spec:
  hard:
    requests.cpu: "1"
    requests.memory: 1Gi
    limits.cpu: "2"
    limits.memory: 2Gi

这个配置规定了在这个 namespace 下所有 pod 所能够使用的 reqeust 和 limit 的值的总和上线。 一旦该 namespace 下所有 pod 申请的资源用量超过了这个值,部署 pod 的时候就会失败。 当然 ResourceQuota 其实是比较复杂的, 除了能够限制内存和 CPU, 还可以限制 pod 的数量和 PV 的存储总量, PVC 的数量等等。 比如:

  • requests.storage:所有 PVC,存储资源的需求总量不能超过该值。
  • persistentvolumeclaims: 在该命名空间中所允许的 PVC 总量。
  • pods:在该命名空间中允许存在的非终止状态的 Pod 总数上限。Pod 终止状态等价于 Pod 的 .status.phase in (Failed, Succeeded) 为真。
  • services: 在该命名空间中允许存在的 Service 总数上限。

其他限制类型可以自行百度, 有很多资料的

同样资源配额的配置中也有对应的作用于配置。 比如下面的配置:

cat << EOF | kubectl -n quota-object-example create -f -
apiVersion: v1
kind: List
items:
- apiVersion: v1
  kind: ResourceQuota
  metadata:
    name: pods-high
  spec:
    hard:
      cpu: "1000"
      memory: 200Gi
      pods: "10"
    scopeSelector:
      matchExpressions:
      - operator : In
        scopeName: PriorityClass
        values: ["high"]
- apiVersion: v1
  kind: ResourceQuota
  metadata:
    name: pods-medium
  spec:
    hard:
      cpu: "10"
      memory: 20Gi
      pods: "10"
    scopeSelector:
      matchExpressions:
      - operator : In
        scopeName: PriorityClass
        values: ["medium"]
- apiVersion: v1
  kind: ResourceQuota
  metadata:
    name: pods-low
  spec:
    hard:
      cpu: "5"
      memory: 10Gi
      pods: "10"
    scopeSelector:
      matchExpressions:
      - operator : In
        scopeName: PriorityClass
        values: ["low"]
EOF

cat << EOF | kubectl -n quota-object-example create -f -
apiVersion: v1
kind: Pod
metadata:
  name: high-priority
spec:
  containers:
  - name: high-priority
    image: ubuntu
    command: ["/bin/sh"]
    args: ["-c", "while true; do echo hello; sleep 10;done"]
    resources:
      requests:
        memory: "10Gi"
        cpu: "500m"
      limits:
        memory: "10Gi"
        cpu: "500m"
  priorityClassName: high
EOF

上面我在同一个 ns 下面创建了 3 个 resource quota, 使用的 scope 是 PriorityClass。 在创建 pod 的时候, priorityClassName 这个字段里指定了 high, 那么它就只受第一个 resource quota 的资源限制。 这是一种在同一个 ns 下又把 pod 划分成了三六九等并做更细粒度的资源限制的手段。

驱逐策略

当 k8s 中实际使用的资源吃紧的时候, 本着保证节点不炸掉导致所有服务都挂了,要优先保证优先级更高的服务的原则, k8s 会在资源紧俏的时候触发驱逐策略, 驱逐掉优先级低的 pod, 保证优先级高的 POD 继续正常工作。所以首先, k8s 会按 pod 声明的 request 和 limit 的情况把 pod 划分成三个类别。

Guaranteed

对于满足以下条件的 POD,被定义为 Guaranteed

  • Pod 中的每个容器,包含初始化容器,必须指定内存请求和内存限制,并且两者要相等。
  • Pod 中的每个容器,包含初始化容器,必须指定 CPU 请求和 CPU 限制,并且两者要相等。

被定义为 Guaranteed 的 POD 优先级是最高的,pod 明确了 request 和 limit 的数字并且是相等的, 等于告诉 k8s 不管什么情况, 我都要使用这么多资源。 而 k8s 会最优先保证这种 pod 的资源使用

Burstable

对于满足一下条件的 POD, 被定义为 Burstable

  • Pod 不符合 Guaranteed QoS 类的标准。
  • Pod 中至少一个容器具有内存或 CPU 请求

这类 POD 的优先级没有 Guaranteed 的高。 一般这类 POD 在超卖场景下比较常见, 上面说过超卖场景就是 limit 的值比 request 大, 完全满足这类 POD 的定义。 所以超卖的 pod 都属于 Burstable 的 pod

BestEffort

对于这类 pod,只需要满足:

  • Pod 中的容器必须没有设置内存和 CPU 限制或请求。

这种类型的 pod 的优先级最低, 也最危险, 因为它没有声明任何资源的使用, 包括 request 和 limit。 所以理论上它可以占用整个节点的资源。 k8s 在触发驱逐策略的时候, 最优先驱逐此类 POD。

资源预留

我们通常使用 kubectl describe node 的时候能够看到如下的信息:

allocatable:    
  cpu: "40"    
  memory: 263927444Ki    
  pods: "110"  
capacity:    
  cpu: "40"    
  memory: 264029844Ki    
  pods: "110"

其中 capacity 就是这台 Node 的资源真实量,比如这台机器是 8 核 32G 内存,那么在 capacity 这一栏中就会显示 CPU 资源有 8 核,内存资源有 32G(内存可能是通过 Ki 单位展示的)。而 allocatable 指的则是这台机器可以被容器所使用的资源量。 我们在部署某台 k8s 的节点的时候, 启动 kubelet 时会有参数打开 Kube-Reserved 机制,来为系统预留资源。 这么做的目的是什么呢? 我们假设一台机器有 40 个 CPU, 如果这些 CPU 都让 k8s 分配给 POD 的话会造成什么影响呢? 有可能启动的 POD 真的把这些 CPU 全都吃掉了,导致这台机器直接挂掉,因为操作系统的运行也需要 CPU, docker, kubelet,kubeproxy 等等一些不在集群内的进程也需要 CPU,如果 POD 把这些资源都吃掉了,这些东西就运行不起来当然就会挂掉。 所以在部署 k8s 集群的时候可以为这些服务预留一些资源。 所以我们在 node 的信息中才会看到 capacity 和 allocatable 两种资源。

再谈不可压缩资源

当机器上面的内存以及磁盘资源这两种不可压缩资源严重不足时,k8s 就会触发驱逐策略特性,该特性允许用户为每台机器针对内存、磁盘这两种不可压缩资源分别指定一个 eviction hard threshold, 即资源量的阈值。 比如我们可以设定内存的 eviction hard threshold 为 100M,那么当这台机器的内存可用资源不足 100M 时,kubelet 就会根据这台机器上面所有 pod 的 QoS 级别(上面介绍的 3 类 POD),以及他们的内存使用情况,进行一个综合排名,把排名最靠前的 pod 进行驱逐(如果有其他可用节点,会迁移到其他节点进行部署),从而释放出足够的内存资源。

总结

上面就是 k8s 中常用的资源管理模型的介绍了。 超卖是一种非常常见的场景, 但也是最容易玩脱的场景。 所以后面才会有驱逐策略出现, 一大超卖没玩好, 就牺牲部分优先级低的 pod 来保证高优先级的 POD 能够正常工作。 虽然超卖这种玩法比较危险, 但也确实是能够提升资源利用率的杀气。 每个 POD 关于 requeset 和 limit 的值要写多少是有讲究的, 也是需要专门的容量测试的。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 3 条回复 时间 点赞

测试

飞哥的产出太高效了,666

在路上 回复

就是赶上这两天有空了😂

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册