前言

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

资源模型

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

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

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

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

资源申请

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

resources:  
 requests:    
   cpu: 2.5   
   memory: "40Mi"  
 limits:     
   cpu: 4.0    
   memory: "99Mi"

注意

资源超卖

通过上面讲解的资源模型, 我们就可以有一种常用的玩法:超卖。 超卖的意思也就是说本来系统只有 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 的资源。

注意

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 的数量等等。 比如:

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

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

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

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

Burstable

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

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

BestEffort

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

这种类型的 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 的值要写多少是有讲究的, 也是需要专门的容量测试的。


↙↙↙阅读原文可查看相关链接,并与作者交流