k8s 准入控制器之ResourceQuota

发布时间 2023-08-08 15:35:07作者: 小吉猫

资源配额概述

尽管LimitRange资源能在名称空间上限制单个容器、Pod或PVC相关的系统资源用量,但用户依然可以创建出无数的资源对象,进而侵占集群上所有的可用系统资源。ResourceQuota资源能够定义名称空间级别的资源配额,从而在名称空间上限制聚合资源消耗的边界,它支持以资源类型来限制用户可在本地名称空间中创建的相关资源的对象数量,以及这些对象可消耗的计算资源总量等。
而同名的ResourceQuota准入控制器负责观察传入的请求,并确保它没有违反相应名称空间中ResourceQuota资源定义的任何约束。ResourceQuota准入控制器属于“验证”类型的控制器,用户创建或更新资源的操作违反配额约束时将会被拒绝,API Server会响应以HTTP状态代码403 FORBIDDEN,并显示一条消息以提示违反的约束条件。
ResourceQuota资源可限制名称空间中处于非终止状态的所有Pod对象的计算资源需求及计算资源限制总量。

资源配额工作模式

资源配额的工作方式如下:

  不同的团队可以在不同的命名空间下工作。这可以通过 RBAC 强制执行。

  集群管理员可以为每个命名空间创建一个或多个 ResourceQuota 对象。

  当用户在命名空间下创建资源(如 Pod、Service 等)时,Kubernetes 的配额系统会跟踪集群的资源使用情况, 以确保使用的资源用量不超过 ResourceQuota 中定义的硬性资源限额。

  如果资源创建或者更新请求违反了配额约束,那么该请求会报错(HTTP 403 FORBIDDEN), 并在消息中给出有可能违反的约束。

  如果命名空间下的计算资源 (如 cpu 和 memory)的配额被启用, 则用户必须为这些资源设定请求值(request)和约束值(limit),否则配额系统将拒绝 Pod 的创建。 提示: 可使用 LimitRanger 准入控制器来为没有设置计算资源需求的 Pod 设置默认值。
说明:
  对于 cpu 和 memory 资源:ResourceQuota 强制该命名空间中的每个(新)Pod 为该资源设置限制。 如果你在命名空间中为 cpu 和 memory 实施资源配额, 你或其他客户端必须为你提交的每个新 Pod 指定该资源的 requests 或 limits。 否则,控制平面可能会拒绝接纳该 Pod。
  对于其他资源:ResourceQuota 可以工作,并且会忽略命名空间中的 Pod,而无需为该资源设置限制或请求。 这意味着,如果资源配额限制了此命名空间的临时存储,则可以创建没有限制/请求临时存储的新 Pod。 你可以使用限制范围自动设置对这些资源的默认请求。

启用资源配额

资源配额的支持在很多 Kubernetes 版本中是默认启用的。 当 API 服务器 的命令行标志 --enable-admission-plugins= 中包含 ResourceQuota 时, 资源配额会被启用。

当命名空间中存在一个 ResourceQuota 对象时,对于该命名空间而言,资源配额就是开启的。

计算资源配额

用户可以对给定命名空间下的可被请求的 计算资源 总量进行限制。

配额机制所支持的资源类型:
资源名称 描述
limits.cpu 所有非终止状态的 Pod,其 CPU 限额总量不能超过该值。
limits.memory 所有非终止状态的 Pod,其内存限额总量不能超过该值。
requests.cpu 所有非终止状态的 Pod,其 CPU 需求总量不能超过该值。
requests.memory 所有非终止状态的 Pod,其内存需求总量不能超过该值。
hugepages-<size> 对于所有非终止状态的 Pod,针对指定尺寸的巨页请求总数不能超过此值。
cpu 与 requests.cpu 相同。
memory 与 requests.memory 相同。

扩展资源的资源配额

除上述资源外,在 Kubernetes 1.10 版本中,还添加了对 扩展资源 的支持。

由于扩展资源不可超量分配,因此没有必要在配额中为同一扩展资源同时指定 requests 和 limits。 对于扩展资源而言,目前仅允许使用前缀为 requests. 的配额项。

以 GPU 拓展资源为例,如果资源名称为 nvidia.com/gpu,并且要将命名空间中请求的 GPU 资源总数限制为 4,则可以如下定义配额:

  requests.nvidia.com/gpu: 4

存储资源配额

资源名称 描述
requests.storage 所有 PVC,存储资源的需求总量不能超过该值。
persistentvolumeclaims 在该命名空间中所允许的 PVC 总量。
<storage-class-name>.storageclass.storage.k8s.io/requests.storage 在所有与 <storage-class-name> 相关的持久卷申领中,存储请求的总和不能超过该值。
<storage-class-name>.storageclass.storage.k8s.io/persistentvolumeclaims 在与 storage-class-name 相关的所有持久卷申领中,命名空间中可以存在的PVC总数。
requests.ephemeral-storage 在命名空间的所有 Pod 中,本地临时存储请求的总和不能超过此值。
limits.ephemeral-storage 在命名空间的所有 Pod 中,本地临时存储限制值的总和不能超过此值。
ephemeral-storage 与 requests.ephemeral-storage 相同。

 

说明:
如果所使用的是 CRI 容器运行时,容器日志会被计入临时存储配额。 这可能会导致存储配额耗尽的 Pods 被意外地驱逐出节点。

示例

例如,如果一个操作人员针对 gold 存储类型与 bronze 存储类型设置配额, 操作人员可以定义如下配额:

gold.storageclass.storage.k8s.io/requests.storage: 500Gi
bronze.storageclass.storage.k8s.io/requests.storage: 100Gi

配额作用域

每个配额都有一组相关的 scope(作用域),配额只会对作用域内的资源生效。 配额机制仅统计所列举的作用域的交集中的资源用量。

当一个作用域被添加到配额中后,它会对作用域相关的资源数量作限制。 如配额中指定了允许(作用域)集合之外的资源,会导致验证错误。
作用域 描述
Terminating 匹配所有 spec.activeDeadlineSeconds 不小于 0 的 Pod。
NotTerminating 匹配所有 spec.activeDeadlineSeconds 是 nil 的 Pod。
BestEffort 匹配所有 Qos 是 BestEffort 的 Pod。
NotBestEffort 匹配所有 Qos 不是 BestEffort 的 Pod。
PriorityClass 匹配所有引用了所指定的优先级类的 Pods。
CrossNamespacePodAffinity 匹配那些设置了跨名字空间(反)亲和性条件 的 Pod。
BestEffort 作用域限制配额跟踪以下资源:
  pods
Terminating、NotTerminating、NotBestEffort 和 PriorityClass 这些作用域限制配额跟踪以下资源:
  pods
  cpu
  memory
  requests.cpu
  requests.memory
  limits.cpu
  limits.memory
  
需要注意的是,你不可以在同一个配额对象中同时设置 Terminating 和 NotTerminating 作用域,你也不可以在同一个配额中同时设置 BestEffort 和 NotBestEffort 作用域。
scopeSelector 支持在 operator 字段中使用以下值:
  In
  NotIn
  Exists
  DoesNotExist
定义 scopeSelector 时,如果使用以下值之一作为 scopeName 的值,则对应的 operator 只能是 Exists。
  Terminating
  NotTerminating
  BestEffort
  NotBestEffort
如果 operator 是 In 或 NotIn 之一,则 values 字段必须至少包含一个值。 例如:
  scopeSelector:
    matchExpressions:
      - scopeName: PriorityClass
        operator: In
        values:
          - middle
如果 operator 为 Exists 或 DoesNotExist,则不可以设置 values 字段。

基于优先级类(PriorityClass)来设置资源配额

Pod 可以创建为特定的优先级。 通过使用配额规约中的 scopeSelector 字段,用户可以根据 Pod 的优先级控制其系统资源消耗。

仅当配额规范中的 scopeSelector 字段选择到某 Pod 时,配额机制才会匹配和计量 Pod 的资源消耗。

如果配额对象通过 scopeSelector 字段设置其作用域为优先级类, 则配额对象只能跟踪以下资源:
  pods
  cpu
  memory
  ephemeral-storage
  limits.cpu
  limits.memory
  limits.ephemeral-storage
  requests.cpu
  requests.memory
  requests.ephemeral-storage

quota.yml

本示例创建一个配额对象,并将其与具有特定优先级的 Pod 进行匹配。 该示例的工作方式如下:

  集群中的 Pod 可取三个优先级类之一,即 "low"、"medium"、"high"。
  为每个优先级创建一个配额对象。
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"]

high-priority-pod.yml

创建优先级为 "high" 的 Pod。
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

跨名称空间的 Pod 亲和性配额

集群运维人员可以使用 CrossNamespacePodAffinity 配额作用域来限制哪个名字空间中可以存在包含跨名字空间亲和性规则的 Pod。 更为具体一点,此作用域用来配置哪些 Pod 可以在其 Pod 亲和性规则中设置 namespaces 或 namespaceSelector 字段。

禁止用户使用跨名字空间的亲和性规则可能是一种被需要的能力, 因为带有反亲和性约束的 Pod 可能会阻止所有其他名字空间的 Pod 被调度到某失效域中。

使用此作用域操作符可以避免某些名字空间(例如下面例子中的 foo-ns)运行特别的 Pod, 这类 Pod 使用跨名字空间的 Pod 亲和性约束,在该名字空间中创建了作用域为 CrossNamespaceAffinity 的、硬性约束为 0 的资源配额对象。
apiVersion: v1
kind: ResourceQuota
metadata:
  name: disable-cross-namespace-affinity
  namespace: foo-ns
spec:
  hard:
    pods: "0"
  scopeSelector:
    matchExpressions:
    - scopeName: CrossNamespaceAffinity
如果集群运维人员希望默认禁止使用 namespaces 和 namespaceSelector, 而仅仅允许在特定名字空间中这样做,他们可以将 CrossNamespaceAffinity 作为一个被约束的资源。方法是为 kube-apiserver 设置标志 --admission-control-config-file
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: "ResourceQuota"
  configuration:
    apiVersion: apiserver.config.k8s.io/v1
    kind: ResourceQuotaConfiguration
    limitedResources:
    - resource: pods
      matchScopes:
      - scopeName: CrossNamespaceAffinity
基于上面的配置,只有名字空间中包含作用域为 CrossNamespaceAffinity 且硬性约束大于或等于使用 namespaces 和 namespaceSelector 字段的 Pod 个数时,才可以在该名字空间中继续创建在其 Pod 亲和性规则中设置 namespaces 或 namespaceSelector 的新 Pod。

请求与限制的比较

分配计算资源时,每个容器可以为 CPU 或内存指定请求和约束。 配额可以针对二者之一进行设置。

如果配额中指定了 requests.cpu 或 requests.memory 的值,则它要求每个容器都显式给出对这些资源的请求。 同理,如果配额中指定了 limits.cpu 或 limits.memory 的值,那么它要求每个容器都显式设定对应资源的限制。

设置配额

compute-resources.yaml

apiVersion: v1
kind: ResourceQuota
metadata:
  name: compute-resources
  namespace: myspace
spec:
  hard:
    requests.cpu: "1"
    requests.memory: 1Gi
    limits.cpu: "2"
    limits.memory: 2Gi
    requests.nvidia.com/gpu: 4

object-counts.yaml

apiVersion: v1
kind: ResourceQuota
metadata:
  name: object-counts
  namespace: myspace
spec:
  hard:
    configmaps: "10"
    persistentvolumeclaims: "4"
    pods: "4"
    replicationcontrollers: "20"
    secrets: "10"
    services: "10"
    services.loadbalancers: "2"

查看配额

# kubectl get quota --namespace=myspace
NAME                    AGE
compute-resources       30s
object-counts           32s
# kubectl describe quota compute-resources --namespace=myspace
Name:                    compute-resources
Namespace:               myspace
Resource                 Used  Hard
--------                 ----  ----
limits.cpu               0     2
limits.memory            0     2Gi
requests.cpu             0     1
requests.memory          0     1Gi
requests.nvidia.com/gpu  0     4
# kubectl describe quota object-counts --namespace=myspace
Name:                   object-counts
Namespace:              myspace
Resource                Used    Hard
--------                ----    ----
configmaps              0       10
persistentvolumeclaims  0       4
pods                    0       4
replicationcontrollers  0       20
secrets                 1       10
services                0       10
services.loadbalancers  0       2

配额和集群容量

ResourceQuota 与集群资源总量是完全独立的。它们通过绝对的单位来配置。 所以,为集群添加节点时,资源配额不会自动赋予每个命名空间消耗更多资源的能力。

有时可能需要资源配额支持更复杂的策略,比如:

  在几个团队中按比例划分总的集群资源。
  允许每个租户根据需要增加资源使用量,但要有足够的限制以防止资源意外耗尽。
  探测某个命名空间的需求,添加物理节点并扩大资源配额值。
这些策略可以通过将资源配额作为一个组成模块、手动编写一个控制器来监控资源使用情况, 并结合其他信号调整命名空间上的硬性资源配额来实现。

注意:资源配额对集群资源总体进行划分,但它对节点没有限制:来自不同命名空间的 Pod 可能在同一节点上运行。

默认情况下限制特定优先级的资源消耗

有时候可能希望当且仅当某名字空间中存在匹配的配额对象时,才可以创建特定优先级 (例如 "cluster-services")的 Pod。

通过这种机制,操作人员能够限制某些高优先级类仅出现在有限数量的命名空间中, 而并非每个命名空间默认情况下都能够使用这些优先级类。

要实现此目的,应设置 kube-apiserver 的标志 --admission-control-config-file 指向如下配置文件:
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: "ResourceQuota"
  configuration:
    apiVersion: apiserver.config.k8s.io/v1
    kind: ResourceQuotaConfiguration
    limitedResources:
    - resource: pods
      matchScopes:
      - scopeName: PriorityClass
        operator: In
        values: ["cluster-services"]
现在在 kube-system 名字空间中创建一个资源配额对象:
apiVersion: v1
kind: ResourceQuota
metadata:
  name: pods-cluster-services
spec:
  scopeSelector:
    matchExpressions:
      - operator : In
        scopeName: PriorityClass
        values: ["cluster-services"]
在这里,当以下条件满足时可以创建 Pod:

1. Pod 未设置 priorityClassName
2. Pod 的 priorityClassName 设置值不是 cluster-services
3. Pod 的 priorityClassName 设置值为 cluster-services,它将被创建于 kube-system 名字空间中,并且它已经通过了资源配额检查。
如果 Pod 的 priorityClassName 设置为 cluster-services,但要被创建到 kube-system 之外的别的名字空间,则 Pod 创建请求也被拒绝。

ResourceQuota 示例

apiVersion: v1
kind: ResourceQuota
metadata:
  name: resourcequota-demo
  namespace: demoapp
spec:
  hard:
    pods: "5"
    count/services: "5"
    count/configmaps: "5"
    count/secrets: "5"
    count/cronjobs.batch: "2"
    requests.cpu: "2"
    requests.memory: "4Gi"
    limits.cpu: "4"
    limits.memory: "8Gi"
    count/deployments.apps: "2"
    count/statefulsets.apps: "2"
    persistentvolumeclaims: "6"
    requests.storage: "20Gi"
    fast.rbd.storageclass.storage.k8s.io/requests.storage: "20Gi"
    fast.rbd.storageclass.storage.k8s.io/persistentvolumeclaims: "6"

参考文档

https://kubernetes.io/docs/concepts/policy/resource-quotas/