19-资源限制:如何保障你的 Kubernete 集群资源不会被打爆

发布时间 2024-01-11 13:44:09作者: miao酱

前面的课时中,我们曾提到通过 HPA 控制业务的资源水位,通过 ClusterAutoscaler 自动扩充集群的资源。但如果集群资源本身就是受限的情况下,或者一时无法短时间内扩容,那么我们该如何控制集群的整体资源水位,保障集群资源不会被“打爆”?

今天我们就来看看 Kubernetes 中都有哪些能力可以帮助我们保障集群资源?

设置 Requests 和 Limits

Kubernetes 中对容器的资源限制实际上是通过 CGroup 来实现的。CGroup 是 Linux 内核的一个功能,用来限制、控制与分离一个进程组的资源(如 CPU、内存、磁盘输入输出等)。每一种资源比如 CPU、内存等,都有对应的 CGroup 。如果我们没有给 Pod 设置任何的 CPU 和 内存限制,这就意味着 Pod 可以消耗宿主机节点上足够多的 CPU 和 内存。

所以一般来说,我们都会对 Pod 进行资源限制, Kubernetes 通过给 Pod 设置资源请求(Requests)和资源限制(Limits)来实现这个资源限制。

  • Requests 表示容器可以得到的资源,或者可以理解为 Pod 运行的最低资源要求。

  • Limits 表示着容器最多可以得到的资源。Pod 运行过程中,比如 CPU 使用量会增加,那么最多能使用多少内存,这就是资源限制。

这里有一点需要注意的就是,Limits 永远不要低于 Requests,如果设置不对,Kubernetes 也会拒绝 Pod 的创建。

通过设置 Requests 和 Limits,我们既保证了 Pod 可以运行,又限制 Pod 能使用多少资源。这样能避免某些恶意的容器“吞噬”宿主机的资源,也可以避免某些容器异常导致宿主机 OOM,从而引起该节点上的所有 Pod 异常,甚至导致整个集群“雪崩”。

我们来看看个 Requests 和 Limits 的例子:

apiVersion: v1
kind: Pod
metadata:
  name: pod-resource-demo
  namespace: demo
spec:
  containers:
  - name: demo-container-1
    image: nginx:1.19
    resources:
    requests:
      memory: "64Mi"
      cpu: "250m"
    limits:
      memory: "128Mi"
      cpu: "500m"
  - name: demo-container-2
    image: nginx:1.19
    resources:
    requests:
      memory: "64Mi"
      cpu: "250m"
    limits:
      memory: "128Mi"
      cpu: "500m"

如上所示,Pod 中的每个容器都可以设置自己的 Requests 和 Limits,每个容器使用的资源都不能超过各自的限制。当 Pod 在调度时,会把这些容器的 Requests 和 Limits 进行相加,当作整个 Pod 的资源申请量。因此在上面的示例中,Pod 的总 Requests 为 500 mCPU,128 MiB 内存,总 Limits 为 1 CPU和 256 MiB。关于单位的含义,官方文档有更详细的说明。

一旦 Pod 成功被调度后,Kubernetes 会将其调度到可以为其提供该资源的节点上。

而根据设置的 Requests 和 Limit,Kubernetes 又将其分为不同的 QoS (Quality of Service)级别。Kubernetes 中 Pod 是最小的单元,所以 QoS 是对整个 Pod 而言而非某个容器。

Kubernetes 支持了三种 QoS 级别,分别为BestEffort、Burstable 和 Guranteed,当资源紧张时 Kubernetes 会根据它们的分级决定调度和驱逐策略(这个我会在后面的课程中单独说明,在此略过),这三个分级分别代表:

  • BestEffort表示 Pod 中没有一个容器设置了 Requests 或 Limits,它的优先级最低;

  • Burstable表示 Pod 中每个容器至少定义了 CPU 或 Memory 的 Requests,或者 Requests 和 Limits 不相等,它属于中等优先级;

  • Guranteed则表示 Pod 中每个容器 Requests 和 Limits 都相等,这类 Pod 的运行优先级最高。简单来说就是cpu.limits = cpu.requests,memory.limits = memory.requests。

你可以通过 QoS 的代码来研究下 Kubernetes 是如何确定 Pod 对应的 QoS 的。这里,我们通过一个 Burstable Pod 的例子,来直观感受下 Kubernetes 的资源限制能力。

一个 Burstable Pod 的例子

这是一个 Burstable Pod 的 YAML 文件,该 Pod 内只有一个容器,且为容器的内存设置了 Requests 和 Limits,分别为 50 Mi 和 100 Mi。

apiVersion: v1
kind: Pod
metadata:
  name: memory-burstable-demo
  namespace: demo
spec:
  containers:
  - name: memory-demo
    image: polinux/stress
    resources:
      requests:
        memory: "50Mi"
      limits:
        memory: "100Mi"
    command: ["stress"]
    args: ["--vm", "1", "--vm-bytes", "250M", "--vm-hang", "1"]

我们通过kubectl create创建好了以后,来查看该 Pod 的状态:

$ kubectl -n demo get po
NAME                                  READY     STATUS        RESTARTS   AGE
memory-burstable-demo      0/1       OOMKilled     1          11s

可以看到该 Pod 被 OOM 杀掉了,因为限制使用100M,而实际使用 250M。那么如果是 CPU 使用超过了 Limits 呢?

这是一个为容器的 CPU 资源设置了 Requests 和 Limits 的 Pod YAML 文件:

apiVersion: v1
kind: Pod
metadata:
  name: cpu-burstable-demo
  namespace: demo
spec:
  containers:
  - name: cpu-demo
    image: vish/stress
    resources:
      limits:
        cpu: "1"
      requests:
        cpu: "0.5"
    args:
    - -cpus
    - "2"

这里我们同样先用kubectl create创建,然后用kubectl top来查看容器 cpu-demo 的资源使用情况:

kubectl -n demo top cpu-burstable-demo cpu-demo
NAME       CPU(cores)   MEMORY(bytes)
cpu-demo   1000m        0Mi

可以看到 Pod 的内存使用虽然超过了 Limits,实际使用的 CPU 被限制只有 1000 m,但是不会被 OOM 掉,这是因为 CPU 不同于内存,CPU 是可压缩资源(Compressible Resource),而内存是不可压缩资源(Incompressible Resource)。
如果只是为了限制资源,用 Requests 和 Limits 就足够了,那么为何 Kubernetes 还要单独引入 QoS 的概念呢?要回答这个问题,我们就要来看看 QoS 的主要作用。

QoS 的主要作用

集群运行一段时间以后,Node 上会有很多 Running 的 Pod。当 Node 上的资源紧张时,可能由于某些BestEffort的 Pod 使用的 CPU 和 Memory 越来越多,或者宿主机某些进程(例如 Kubelet、Docker)占用了 CPU 和 Memory,这个时候Kubernetes 就会根据 QoS 的优先级来选择 Kill 掉一部分 Pod,哪些会先被 Kill 掉呢?

当然是优先级最低的,即BestEffort类型的 Pod,占用的资源越多越优先被 Kill 掉。如果所有BestEffort的 Pod 都被杀死了但是资源依旧紧张,那么接下来会选择 Kill 中等优先级的,即Burstable类型的,之后以此类推。

这里 QoS 的一个作用就是跟oom_score进行挂钩。Kubernetes 会根据 QoS 设置 OOM 的评分调整参数oom_score_adj,有兴趣可以阅读详细的计算代码。当发生 OOM 时,oom_score_adj数值越高就越优先被 Kill。这里我给你展示了三个 QoS 对应的oom_score_adj计算公式。

image.png

除此之外,QoS 还与 Pod 驱逐有关系。当节点的内存、CPU 资源不足时,Kubelet 会开始驱逐节点上的 Pod,它会依据 QoS 的优先级确定驱逐的顺序,跟上面 OOM kill 的次序一样。我们会在后续的课程中单独讲这部分。

在实际使用的时候,我们可能会担心某些 Pod 申请了过大的资源,恶意占用,那么我们又该如何避免呢?

通过 LimitRange 设置资源防线

Kubernetes 提供了 LimitRange 可以帮助你限定 CPU 和 Memory 的申请范围。

这是一个完整的 LimitRange 定义,你可以根据需要按需选择进行配置。

apiVersion: v1
kind: LimitRange
metadata:
  name: mem-limit-range
  namespace: example
spec:
  limits:
  - default:  # 默认 limit
      memory: 512Mi
      cpu: 2
    defaultRequest:  # 默认 request
      memory: 256Mi
      cpu: 0.5
    max:  # 最大 limit
      memory: 800Mi
      cpu: 3
    min:  # 最小 request
      memory: 100Mi
      cpu: 0.3
    maxLimitRequestRatio:  # limit/request 的最大比率
      memory: 2
      cpu: 2
    type: Container # 支持 Container / Pod / PersistentVolumeClaim 三种类型
  • default 字段可以设置 Pod 中容器的默认 Limits;

  • defaulRequest 字段可以设置 Pod 中容器的默认 Requests;

  • max 字段可以设置 Pod 中容器可以设置的最大 Limits,default 字段不能高于此值。同样,在容器上设置的 Limits 也不能高于此值。在使用的时候需要注意的是,如果设置了该字段而又没有设置 default,那么所有未显式设置这些值的容器都将使用此处的最大值作为 Limits。

  • min 字段可以设置 Pod 中容器可以设置的最小 Requests。defaulRequest 字段不能低于此值。同样,在容器上设置的 Requests 也不能低于此值。同样需要注意的是,如果设置了该字段而又没有设置 defaulRequest,那么所有未显式设置这些值的容器都将使用此处的最小值作为 Requests。

LimitRange 会设置默认的申请、限制的值,它会自动在 Pod 创建时就注入 Container 中。

你可以参照如下几个官方文档中的详细例子学习体会一下:
如何配置每个命名空间最小和最大的 CPU 约束
如何配置每个命名空间最小和最大的内存约束
如何配置每个命名空间默认的 CPU 申请值和限制值
如何配置每个命名空间默认的内存申请值和限制值
如何配置每个命名空间最小和最大存储使用量

除了对单个 Pod、Container、PVC 做资源限制外,我们还可以对某个 namespace 下的资源总量进行限制。

ResourceQuota 设置资源总量限制

我们可以使用 ResourceQuota 对 namespace 内的资源总量进行限制,比如这个例子:

apiVersion: v1
kind: ResourceQuota 
metadata: 
  name: compute-resources
  namespace: demo                  #在demo空间下
spec: 
  hard:
    requests.cpu: "10"             #cpu预配置10
    requests.memory: 100Gi         #内存预配置100Gi
    limits.cpu: "40"               #cpu最大不超过40
    limits.memory: 200Gi           #内存最大不超过200Gi

你可以看到它有四个部分,每个部分都是可选的,你可以根据自己的需要进行组合。

  • requests.cpu 是该命名空间中所有容器的 CPU Requests 总和。在上面的例子中,你可以拥有10 个具有 1 个 CPU 请求的容器,或者 5 个具有 2 个 CPU 请求的容器。只要命名空间 demo 中所有容器的 CPU Requests 总和小于 10 即可。

  • requests.memory 是该命名空间中所有容器的 Memory Requests 总和。同 CPU 一样,只要该命名空间中内存的总请求小于100Gi 即可。

  • limits.cpu 是命名空间中所有容器的 CPU Limits 的总和。和 requests.cpu 一样,只不过这里是 Limits。

  • limits.memory 是命名空间中所有容器的内存 Limits 的总和。和 requests.memory 一样,这里也是指 Limits。

除了 CPU 和内存这类资源以外,ResourceQuota 还支持扩展资源,详见官方文档的说明

ResourceQuota 的功能非常强大,还可以对对象的数量进行限制。比如这个例子:

apiVersion: v1 
kind: ResourceQuota 
metadata: 
  name: object-counts 
  namespace: demo                  #在demo命名空间下
spec: 
  hard: 
    configmaps: "10"               #最多10个configmap
    pods: "20"                     #最多20个pod
    persistentvolumeclaims: "4"    #最多10个pvc
    replicationcontrollers: "20"   #最多20个rc
    secrets: "10"                  #最多10个secrets
    services: "10"                 #最多10个service
    services.loadbalancers: "2"    #最多10个lb类型的service
    requests.nvidia.com/gpu: 4        #最多10个GPU

我们就可以限制该命名空间下最多可以创建 20 个 Pod,10 个 Configmap 等。

写在最后

对于一些重要的线上应用,我们要合理地设置 Requests 和 Limits,且最好使两者的设置相等,当节点资源不足时,Kubernetes 会优先保证这些 Pod 的正常运行。

此外,你可以用 ResourceQuota 限制命名空间中所有容器的内存请求总量、内存限制总量、CPU 请求总量、CPU 限制总量等。而如果你想对单个容器而不是所有容器进行限制,就可以使用 LimitRange。

到这里这节课就结束了,如果你对本节课有什么想法或者疑问,欢迎你在留言区留言,我们一起讨论。