Kubernetes 基础体系 · 第 53/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

ResourceQuota 与 LimitRange:租户预算、默认值、对象数量和治理

在 Kubernetes 中,ResourceQuotaLimitRange 都是 Namespace 级别的准入控制资源,但它们解决的是两个不同问题:

  • LimitRange 约束单个对象或单个容器的资源形态,并可以补齐默认值;
  • ResourceQuota 约束整个 Namespace 的累计使用量,包括计算资源、存储资源和对象数量。

可以先用一句形式化描述区分它们:

LimitRange:di[mini,maxi],并可能为缺失字段生成默认值\text{LimitRange}: d_i \in [min_i, max_i], \quad \text{并可能为缺失字段生成默认值}

ResourceQuota:Ur+ΔrHr\text{ResourceQuota}: U_r + \Delta_r \leq H_r

其中:

  • did_i 是某个容器或 Pod 对资源的请求、限制或其他属性;
  • minimin_imaximax_iLimitRange 对单个对象的约束;
  • UrU_r 是 Namespace 当前已经使用的资源 rr
  • Δr\Delta_r 是本次创建或更新带来的增量;
  • HrH_rResourceQuota 为资源 rr 设置的硬上限。

因此,Namespace 可能出现以下状态:

  1. 每个容器都符合 LimitRange,但 Namespace 总 CPU 已超过 ResourceQuota
  2. Namespace 还有充足的总预算,但某个容器超过了 LimitRange 的单对象上限;
  3. Pod 没有显式写 resources,但 LimitRange 自动补齐默认值,使它开始消耗 ResourceQuota
  4. 计算资源没有超额,但 Pod 数量或 PVC 数量达到对象数量配额。

1. Namespace 是预算和默认策略的边界

ResourceQuotaLimitRange 都是 Namespace-scoped 资源。它们不会跨 Namespace 统计,也不会直接限制集群级对象。

例如:

  • ResourceQuotateam-a 中统计 Pod、PVC 和 CPU;
  • team-b 有自己的配额;
  • ClusterRoleNodeStorageClassCustomResourceDefinition 等集群级对象不属于某个 Namespace,不能通过普通 Namespace ResourceQuota 直接限制。

这意味着 Namespace 通常是 Kubernetes 多租户中的预算边界,但不是完整的安全边界。它不能单独提供:

  • 用户身份隔离;
  • 对 API 操作的权限控制;
  • Pod 之间的网络隔离;
  • 节点、内核、设备和宿主机层面的强隔离;
  • 对集群级资源创建的限制。

生产租户模型通常还需要 RBAC、NetworkPolicy、Pod Security Admission,以及必要时的独立节点池、虚拟集群或独立集群。


2. LimitRange:限制单个对象,并在准入时补默认值

2.1 LimitRange 的作用范围

LimitRange 的每一个条目通过 type 指定作用对象。当前稳定的 v1 API 中,常见类型包括:

  • Container:约束容器的 CPU、内存、临时存储等资源;
  • Pod:约束 Pod 内所有容器资源请求或限制的聚合值;
  • PersistentVolumeClaim:约束 PVC 的存储请求。

一个典型的容器级 LimitRange 如下:

apiVersion: v1
kind: LimitRange
metadata:
  name: tenant-defaults
  namespace: team-a
spec:
  limits:
  - type: Container
    default:
      cpu: "500m"
      memory: "512Mi"
    defaultRequest:
      cpu: "100m"
      memory: "128Mi"
    min:
      cpu: "50m"
      memory: "64Mi"
    max:
      cpu: "2"
      memory: "2Gi"
    maxLimitRequestRatio:
      cpu: "10"
      memory: "4"

这些字段含义不同:

字段 含义
default 容器未指定 resources.limits 时使用的默认限制
defaultRequest 容器未指定 resources.requests 时使用的默认请求
min 单个容器允许设置的最小值
max 单个容器允许设置的最大值
maxLimitRequestRatio limit / request 的最大比例

例如,上面的配置要求:

50mrequest.cpu250m \leq request.cpu \leq 2

并且:

limit.cpurequest.cpu10\frac{limit.cpu}{request.cpu} \leq 10

如果某个容器请求 100m CPU,限制为 2 CPU,那么比例为:

20.1=20\frac{2}{0.1}=20

这会被拒绝,因为超过了 maxLimitRequestRatio.cpu=10

2.2 默认值如何生成

下面的 Pod 没有写 resources

apiVersion: v1
kind: Pod
metadata:
  name: web
  namespace: team-a
spec:
  containers:
  - name: app
    image: nginx:1.27

LimitRange 生效后,准入阶段可能得到等价的资源配置:

resources:
  requests:
    cpu: "100m"
    memory: "128Mi"
  limits:
    cpu: "500m"
    memory: "512Mi"

这里有两个重要规则。

第一,默认值是在对象进入集群时写入对象的,不是 kubelet 在节点上临时计算出来的。可以通过以下命令观察最终对象:

kubectl -n team-a get pod web -o yaml

第二,如果只配置了 default,没有配置 defaultRequest,Kubernetes 的常见行为是把默认限制同时作为默认请求使用。例如:

default:
  cpu: "500m"
  memory: "512Mi"

可能导致:

requests:
  cpu: "500m"
  memory: "512Mi"
limits:
  cpu: "500m"
  memory: "512Mi"

这会显著影响:

  • ResourceQuota 的请求资源消耗;
  • 调度器为 Pod 预留的节点容量;
  • Pod 的 QoS 分类。

因此,生产环境不能把 default 当作“仅仅限制最大值”的配置。若希望请求和限制不同,应显式设置 defaultRequest

2.3 LimitRange 不会修改现有对象

LimitRange 主要在对象创建或相关更新的准入过程中生效。创建它之后,已有 Pod 不会自动被重写:

kubectl apply -f limitrange.yaml

这不会遍历 Namespace 内的 Pod 并补上 resources

如果工作负载由 Deployment、StatefulSet 或 Job 管理,需要修改控制器的 Pod 模板,使后续创建的 Pod 带上符合规则的配置。已有 Pod 是否被替换,则取决于控制器的滚动更新或重建行为。

这也解释了一个常见现象:管理员创建 LimitRange 后,旧 Pod 仍然没有请求和限制,而新发布的 Pod 开始出现默认资源字段。


3. LimitRange 的校验对象:容器、Pod 和 PVC

3.1 容器级资源

type: Container 而言,minmax 和比例约束通常应用于每个容器,而不是整个 Pod。

如果一个 Pod 有两个容器:

containers:
- name: app
  resources:
    requests:
      cpu: "500m"
- name: sidecar
  resources:
    requests:
      cpu: "100m"

那么容器级请求分别是 500m100m,Pod 的总请求是:

500m+100m=600m500m + 100m = 600m

某个容器的请求符合限制,不代表 Pod 总请求一定符合 Namespace 配额;反过来也一样。

3.2 Pod 级资源

type: Pod 可以表达 Pod 内容器资源总和的约束。例如:

apiVersion: v1
kind: LimitRange
metadata:
  name: pod-budget
  namespace: team-a
spec:
  limits:
  - type: Pod
    max:
      cpu: "4"
      memory: "8Gi"

对于一个有两个容器的 Pod,假设它们的 CPU limit 分别为 23

2+3=52 + 3 = 5

那么即使每个容器都没有超过各自的 Container 级最大值,Pod 仍可能因为超过 Pod 级最大值而被拒绝。

Pod 级条目适合防止“多个合法容器组合成一个过大的 Pod”。但它与调度器、节点容量和 Namespace 总配额仍是不同层次的约束。

3.3 PVC 级存储限制

LimitRange 也可以限制单个 PVC:

apiVersion: v1
kind: LimitRange
metadata:
  name: pvc-size-range
  namespace: team-a
spec:
  limits:
  - type: PersistentVolumeClaim
    min:
      storage: "1Gi"
    max:
      storage: "100Gi"

这表示每个 PVC 的请求存储量必须满足:

1Girequests.storage100Gi1Gi \leq requests.storage \leq 100Gi

它不表示 Namespace 总共只能使用 100Gi。Namespace 总量需要由 ResourceQuota 约束。


4. ResourceQuota:限制 Namespace 的累计预算

4.1 基本计算模型

一个资源配额可以表示为:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: tenant-budget
  namespace: team-a
spec:
  hard:
    requests.cpu: "4"
    requests.memory: 8Gi
    limits.cpu: "8"
    limits.memory: 16Gi
    requests.storage: 100Gi
    persistentvolumeclaims: "10"
    pods: "20"

对 CPU 请求配额,Namespace 的使用量近似为:

Urequests.cpu=p符合范围的 Podcp.containersrequest(p,c,cpu)U_{\text{requests.cpu}} = \sum_{p \in \text{符合范围的 Pod}} \sum_{c \in p.containers} request(p,c,\text{cpu})

一次创建或更新带来的增量为 Δ\Delta。准入检查要求:

Urequests.cpu+Δrequests.cpu4U_{\text{requests.cpu}}+\Delta_{\text{requests.cpu}} \leq 4

如果结果大于 4,对象会被拒绝。

requests.memorylimits.cpulimits.memory 采用同样的累计逻辑。对 Pod 而言,资源需求通常按 Pod 内容器的总和参与统计;实际还应考虑 Kubernetes 对 init container、终止状态对象等资源统计规则,不能简单把所有 YAML 字段机械相加。

4.2 完整算例:默认值如何消耗配额

假设 team-a 有以下配额:

hard:
  requests.cpu: "1"
  requests.memory: 2Gi
  limits.cpu: "2"
  limits.memory: 4Gi
  pods: "5"

Namespace 中已运行两个 Pod:

Pod CPU request 内存 request CPU limit 内存 limit
api-1 250m 512Mi 500m 1Gi
api-2 250m 512Mi 500m 1Gi

当前使用量为:

Urequests.cpu=250m+250m=500mU_{\text{requests.cpu}}=250m+250m=500m

Urequests.memory=512Mi+512Mi=1GiU_{\text{requests.memory}}=512Mi+512Mi=1Gi

Ulimits.cpu=500m+500m=1U_{\text{limits.cpu}}=500m+500m=1

Ulimits.memory=1Gi+1Gi=2GiU_{\text{limits.memory}}=1Gi+1Gi=2Gi

此时再创建一个没有 resources 的 Pod。如果 LimitRange 自动补齐:

requests:
  cpu: 100m
  memory: 128Mi
limits:
  cpu: 500m
  memory: 512Mi

新的使用量为:

Urequests.cpu=500m+100m=600mU'_{\text{requests.cpu}}=500m+100m=600m

Urequests.memory=1Gi+128MiU'_{\text{requests.memory}}=1Gi+128Mi

Ulimits.cpu=1+500m=1.5U'_{\text{limits.cpu}}=1+500m=1.5

Ulimits.memory=2Gi+512MiU'_{\text{limits.memory}}=2Gi+512Mi

这个 Pod 可以创建,因为所有结果都没有超过硬上限。

但如果同一个 Pod 显式设置:

resources:
  requests:
    cpu: "600m"
    memory: "1Gi"
  limits:
    cpu: "2"
    memory: "2Gi"

则:

Urequests.cpu=500m+600m=1.1U'_{\text{requests.cpu}}=500m+600m=1.1

已经超过 1,创建请求会被 ResourceQuota 拒绝。此时问题不是单个容器违反了 LimitRange,而是 Namespace 没有足够的累计预算。

4.3 ResourceQuota 不只限制 CPU 和内存

常见资源名称包括:

hard:
  requests.cpu: "4"
  limits.cpu: "8"
  requests.memory: 8Gi
  limits.memory: 16Gi
  requests.ephemeral-storage: 50Gi
  limits.ephemeral-storage: 100Gi
  requests.storage: 500Gi
  persistentvolumeclaims: "20"
  pods: "50"
  services: "20"
  secrets: "100"
  configmaps: "100"

这些资源含义不同:

  • requests.cpurequests.memory:累计调度请求;
  • limits.cpulimits.memory:累计资源限制;
  • requests.ephemeral-storage:容器本地临时存储请求;
  • requests.storage:PVC 请求的存储总量;
  • pods:Namespace 中匹配范围的 Pod 数量;
  • servicessecretsconfigmaps:对应对象数量。

ResourceQuota 记录的是 Kubernetes 对象声明的资源请求或对象数量,并不是实时测量到的 CPU 使用率、内存工作集或磁盘实际写入量。它不能替代监控系统,也不保证底层云盘、节点磁盘或集群总容量一定充足。


5. 对象数量配额:防止控制面和资源对象无限增长

5.1 数量配额的语法

对象数量可以使用短名称:

hard:
  pods: "20"
  services: "10"
  secrets: "50"
  configmaps: "100"
  persistentvolumeclaims: "10"

也可以使用资源组形式限制其他 Namespaced 资源,例如:

hard:
  count/deployments.apps: "10"
  count/statefulsets.apps: "5"
  count/jobs.batch: "50"
  count/ingresses.networking.k8s.io: "20"

实际可用资源名称取决于 API server 发现到的 Namespaced API 资源,以及当前 Kubernetes 版本对配额评估器的支持。可以先检查资源名称:

kubectl api-resources --namespaced=true

对象数量配额限制的是 API 对象的数量,不是对象背后的运行实例数量。例如:

  • deployments.apps: 10 限制 Deployment 对象数量;
  • pods: 20 限制 Pod 对象数量;
  • 一个 Deployment 的 replicas: 20 不会消耗 20 个 Deployment 配额,但会创建最多 20 个 Pod,消耗 Pod 配额。

5.2 Deployment 与 Pod 配额的失败路径

假设:

hard:
  count/deployments.apps: "10"
  pods: "3"

创建一个:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: team-a
spec:
  replicas: 5
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: nginx
        image: nginx:1.27

Deployment 对象本身可能能够成功创建,因为当前 Deployment 数量没有超过 10。随后 Deployment 控制器尝试创建 Pod:

  1. 第一个 Pod 创建成功;
  2. 第二个 Pod 创建成功;
  3. 第三个 Pod 创建成功;
  4. 第四、第五个 Pod 的创建请求被 ResourceQuota 拒绝;
  5. Deployment 持续处于未达到期望副本数的状态,并产生事件。

此时不能仅检查:

kubectl get deployment -n team-a

还需要查看:

kubectl describe deployment web -n team-a
kubectl get events -n team-a --sort-by=.lastTimestamp
kubectl get quota -n team-a

常见事件会包含类似“exceeded quota”的信息。恢复方法取决于目标:

  • 降低 spec.replicas
  • 删除不再需要的 Pod 或其他对象;
  • 增加 Pod 配额;
  • 为该租户分配更多预算。

滚动更新也可能临时需要更多 Pod。新 ReplicaSet 和旧 ReplicaSet 在切换期间可能同时存在,因而 pods 配额过紧时,发布会卡住,即使最终副本数理论上并不超限。


6. 配额匹配范围:Scopes 和 ScopeSelector

一个 Namespace 可以有多个 ResourceQuota,每个配额可以针对不同对象集合。常见范围包括:

  • BestEffort:没有任何资源请求和限制的 Pod;
  • NotBestEffort:不是 BestEffort 的 Pod;
  • TerminatingNotTerminating:是否设置了 activeDeadlineSeconds
  • PriorityClass:匹配指定优先级;
  • 某些版本支持基于 Pod 优先级的 scopeSelector
  • 与跨 Namespace Pod 亲和性相关的范围能力也受版本和配置影响。

例如,单独限制 BestEffort Pod:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: besteffort-pods
  namespace: team-a
spec:
  hard:
    pods: "2"
  scopes:
  - BestEffort

再限制带资源请求的 Pod:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: requested-pods
  namespace: team-a
spec:
  hard:
    requests.cpu: "4"
    requests.memory: 8Gi
  scopes:
  - NotBestEffort

一个对象可能同时匹配多个配额。此时必须同时满足所有匹配到的配额:

qQmatched,Uq,r+Δq,rHq,r\forall q \in Q_{\text{matched}},\quad U_{q,r}+\Delta_{q,r}\leq H_{q,r}

因此,添加一个更窄范围的配额不会绕过已有的通用配额。

scopeSelector 可进一步按 PriorityClass 等条件匹配。配置前应使用当前集群版本的 API 文档和:

kubectl explain resourcequota.spec.scopeSelector
kubectl explain resourcequota.spec.scopes

确认字段和可用值。不同 Kubernetes 版本、启用的特性门控和托管平台发行版可能存在差异。


7. ResourceQuota、LimitRange 与准入流程

一个简化的请求路径如下:

sequenceDiagram
    participant C as kubectl/控制器
    participant A as kube-apiserver
    participant L as LimitRanger
    participant Q as ResourceQuota
    participant S as etcd
    participant K as 控制器/kubelet

    C->>A: 创建或更新 Namespace 内对象
    A->>L: 校验并补齐 LimitRange 默认值
    L-->>A: 返回修改后的对象或拒绝
    A->>Q: 计算配额增量并检查 ResourceQuota
    Q-->>A: 允许或拒绝
    A->>S: 持久化对象
    S-->>A: 返回成功
    A-->>C: 请求结果
    K->>A: 读取已接受的 Pod
    K->>K: 调度、启动并执行容器

这个图表达的是语义顺序,而不是所有内部 admission 插件的完整实现细节。实际 API server 还可能经过:

  • Kubernetes 内置字段默认化;
  • MutatingAdmissionWebhook;
  • ValidatingAdmissionWebhook;
  • 认证和授权;
  • 其他内置准入插件。

关键点是:ResourceQuota 判断的不是客户端最初提交的“意图”,而是经过相关默认化和变更准入后的对象形态。于是,一个没有写资源字段的 Pod 也可能因为 LimitRange 默认值而消耗配额。

ResourceQuota 的使用状态通常由配额控制器维护,配额对象中可以看到:

kubectl get resourcequota tenant-budget -n team-a -o yaml

典型输出结构包括:

status:
  hard:
    pods: "20"
    requests.cpu: "1"
  used:
    pods: "3"
    requests.cpu: 600m

status.used 是控制面观察和计算出的状态,不应被当作外部计费系统的精确、实时计量账本。发生高并发创建、控制器重试或对象快速删除时,状态显示可能存在短暂延迟;最终是否接受某个请求仍应以 API server 的准入结果为准。


8. LimitRange 和 ResourceQuota 的错误边界

8.1 只创建 LimitRange,不代表有租户预算

下面的 LimitRange

spec:
  limits:
  - type: Container
    max:
      cpu: "1"
      memory: "1Gi"

只能限制每个容器的最大资源,不能防止租户创建无限多个容器或 Pod。若希望限制 Namespace 总量,仍要创建 ResourceQuota

8.2 只创建 ResourceQuota,不代表请求一定合理

如果只设置:

hard:
  requests.cpu: "4"
  requests.memory: 8Gi

那么用户可以提交:

resources:
  requests:
    cpu: "4"
    memory: 8Gi

一个 Pod 就消耗掉整个 Namespace 的预算。也可能提交没有 requests 的 Pod,具体是否能够创建取决于配额是否包含适用资源、是否配置了 LimitRange,以及 Pod 的最终资源形态。

ResourceQuota 控制总量,不能替代单对象的 LimitRange

8.3 配置 max 不会自动设置 limit

下面的配置:

max:
  cpu: "1"

表示超过 1 CPU 时拒绝,不表示缺失的 CPU limit 会自动变成 1。要产生默认值,必须设置 default

8.4 requestslimits 不是同一个预算

如果配额同时配置:

hard:
  requests.cpu: "4"
  limits.cpu: "8"

则两个维度分别计算。一个容器请求 500m、限制 2,消耗:

  • requests.cpu500m
  • limits.cpu2

不能用 limit 去推导 request,也不能因为 request 配额有剩余,就认为 limit 配额一定有剩余。


9. 资源配额与 QoS、调度和运行时的关系

ResourceQuota 主要在 API 准入阶段工作;调度器随后使用 Pod 的资源请求选择节点;kubelet 和 Linux cgroup 在运行时执行限制。它们是三个不同阶段。

9.1 CPU

CPU request 主要用于调度和配额统计,CPU limit 主要用于运行时上限。CPU 超过 limit 时通常会被 throttling,而不是像内存那样直接 OOM。

因此:

  • requests.cpu 配得过小,可能让调度器过度装箱;
  • limits.cpu 配得过小,可能导致应用 CPU throttle;
  • ResourceQuota 只能限制租户声明的总量,不能保证租户实际获得固定 CPU 性能。

9.2 内存

内存 limit 由运行时通过 cgroup 等机制执行。容器超过限制时可能被 OOM kill;节点整体内存压力还可能触发 eviction。

因此:

  • limits.memory 是容器级运行时约束;
  • requests.memory 影响调度和配额;
  • ResourceQuota 不会防止节点内存压力,也不会替代 eviction 机制;
  • 一个租户未超过配额,仍可能因为节点不足而 Pending 或被驱逐。

9.3 QoS 分类

在常见 Kubernetes 规则下:

  • 所有容器都设置了 CPU 和内存的 request、limit,且对应值相等,Pod 倾向于属于 Guaranteed
  • 至少有一个容器设置了 request 或 limit,但不满足 Guaranteed 条件,通常属于 Burstable
  • 没有任何容器资源 request 和 limit,通常属于 BestEffort

LimitRange 的默认值会直接影响 QoS 分类。例如,只设置默认 limit,且系统把它同时作为 request 使用,可能使容器表现为 request 等于 limit;设置不同的 defaultRequestdefault 则通常会产生 Burstable Pod。

QoS 分类不等于配额范围。只有在 ResourceQuota 显式使用 BestEffortNotBestEffort scope 时,QoS 类别才会影响该配额的匹配。


10. PVC 配额、动态供给和真实存储容量

对于 PVC,常见配置如下:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: storage-budget
  namespace: team-a
spec:
  hard:
    requests.storage: 500Gi
    persistentvolumeclaims: "20"

假设当前 PVC 请求总量为 420Gi,再次创建一个请求 100Gi 的 PVC:

420Gi+100Gi=520Gi>500Gi420Gi + 100Gi = 520Gi > 500Gi

PVC 会在准入阶段被拒绝,动态供给器通常不会有机会创建对应的 PV。

还可以按 StorageClass 分别限制存储请求,形式类似:

hard:
  fast.storageclass.storage.k8s.io/requests.storage: 100Gi
  standard.storageclass.storage.k8s.io/requests.storage: 400Gi

这里的资源名称必须与集群实际支持的配额资源形式一致,应先使用当前版本文档或测试 Namespace 验证。

存储配额仍有边界:

  • 配额约束的是 PVC 声明的请求量;
  • 不代表底层云盘一定有相同的可用容量;
  • 不代表应用实际只写入了该容量;
  • StorageClass 的扩容、快照、克隆和厂商实现可能有额外行为;
  • 云厂商可能在 CSI、云 API 和计费层引入不同的限制。

因此,存储配额需要与存储后端容量、扩容策略和备份策略一起设计。


11. 一个可运行的端到端示例

下面的示例创建 Namespace、默认资源策略、资源配额和一个 Deployment。

11.1 创建 Namespace、LimitRange 和 ResourceQuota

apiVersion: v1
kind: Namespace
metadata:
  name: team-a
---
apiVersion: v1
kind: LimitRange
metadata:
  name: tenant-defaults
  namespace: team-a
spec:
  limits:
  - type: Container
    default:
      cpu: "500m"
      memory: "512Mi"
    defaultRequest:
      cpu: "100m"
      memory: "128Mi"
    min:
      cpu: "50m"
      memory: "64Mi"
    max:
      cpu: "1"
      memory: "1Gi"
---
apiVersion: v1
kind: ResourceQuota
metadata:
  name: tenant-budget
  namespace: team-a
spec:
  hard:
    requests.cpu: "1"
    requests.memory: 2Gi
    limits.cpu: "2"
    limits.memory: 4Gi
    pods: "3"
    services: "5"
    persistentvolumeclaims: "2"

应用:

kubectl apply -f tenant-policy.yaml
kubectl get limitrange,resourcequota -n team-a

检查具体规则:

kubectl describe limitrange tenant-defaults -n team-a
kubectl describe resourcequota tenant-budget -n team-a

11.2 创建一个不写资源字段的 Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: team-a
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: nginx
        image: nginx:1.27
        ports:
        - containerPort: 80

执行:

kubectl apply -f web.yaml
kubectl get pods -n team-a
kubectl get pod -n team-a -l app=web -o yaml
kubectl get resourcequota tenant-budget -n team-a -o yaml

每个 Pod 预计因 LimitRange 得到:

requests:
  cpu: 100m
  memory: 128Mi
limits:
  cpu: 500m
  memory: 512Mi

两个副本大约消耗:

  • CPU request:200m
  • 内存 request:256Mi
  • CPU limit:1
  • 内存 limit:1Gi
  • Pod 数量:2

如果把副本数改为 4,Pod 数量将超过 pods: "3"。Deployment 对象本身可能创建成功,但其中部分 Pod 创建会被拒绝,最终表现为 Deployment 不可用而不是 Deployment 创建命令本身必然失败。

11.3 验证单容器上限

创建一个请求 2 CPU 的 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: too-large
  namespace: team-a
spec:
  containers:
  - name: app
    image: nginx:1.27
    resources:
      requests:
        cpu: "2"
        memory: "256Mi"
      limits:
        cpu: "2"
        memory: "512Mi"

验证:

kubectl apply --dry-run=server -f too-large.yaml

--dry-run=server 会把请求发送到 API server,但不持久化对象,适合验证服务端默认化和准入规则。该对象应因 LimitRangemax.cpu: "1" 被拒绝。

如果改成 requests.cpu: "900m"limits.cpu: "2",则可能先满足 max,但违反:

20.92.22\frac{2}{0.9}\approx 2.22

这个值没有超过 CPU 比例上限 10;如果改成 requests.cpu: "100m"limits.cpu: "2",则:

20.1=20>10\frac{2}{0.1}=20>10

会因 maxLimitRequestRatio 被拒绝。


12. 诊断失败:先区分 LimitRange、Quota、调度和运行时

12.1 对象在 API 创建阶段被拒绝

优先检查:

kubectl apply --dry-run=server -f object.yaml
kubectl get limitrange -n team-a -o yaml
kubectl get resourcequota -n team-a -o yaml
kubectl explain limitrange.spec.limits

典型原因包括:

  • 容器 request 小于 min
  • 容器 limit 大于 max
  • limit/request 比例过大;
  • Namespace 配额中的 request、limit 或对象数量已耗尽;
  • PVC 超过单 PVC 上限或 Namespace 存储配额;
  • 对象数量配额使用了不适用于当前资源的名称。

12.2 控制器对象成功,但 Pod 没有全部出现

检查:

kubectl describe deployment web -n team-a
kubectl get rs -n team-a
kubectl get pods -n team-a
kubectl get events -n team-a --sort-by=.lastTimestamp

如果事件显示配额超限,通常是:

  • Deployment 副本数超过 Pod 配额;
  • 滚动更新期间新旧 ReplicaSet 临时共存;
  • LimitRange 默认值使每个 Pod 的请求高于预期;
  • Namespace 中有其他团队对象消耗了共享配额。

12.3 Pod 已创建但处于 Pending

这通常不是 ResourceQuota 准入错误。需要区分:

  • 调度器找不到满足 request 的节点;
  • 节点 taint、亲和性或拓扑约束不满足;
  • PVC 尚未绑定;
  • 节点资源不足。

检查:

kubectl describe pod <pod-name> -n team-a
kubectl get nodes
kubectl get pvc -n team-a

配额允许创建,只说明 API 对象通过了累计预算检查,不代表集群一定能调度和运行它。

12.4 Pod 运行后被限速、OOM 或驱逐

这属于运行时和节点压力路径,不是 ResourceQuota 直接执行的行为:

kubectl get pod <pod-name> -n team-a -o jsonpath='{.status.containerStatuses[*].lastState.terminated.reason}{"\n"}'
kubectl describe pod <pod-name> -n team-a
kubectl top pod -n team-a

需要结合:

  • CPU limit 是否过低导致 throttling;
  • 内存 limit 是否过低导致 OOM;
  • 节点是否存在内存或临时存储压力;
  • kubelet 是否触发 eviction;
  • 容器运行时和云平台监控。

13. 并发、删除和配额状态的实际边界

配额检查涉及对象的创建、更新和删除,控制器还要异步维护 status.used。在高并发场景下,不能把客户端读取到的 status.used 当作绝对实时值。

例如,两个控制器同时创建 Pod:

  1. 两个请求都读取到接近的配额状态;
  2. API server 的准入逻辑必须防止最终接受超过硬配额的结果;
  3. 配额控制器随后重新计算并更新 status.used
  4. 客户端可能在短时间内看到状态更新滞后或出现重试。

规范保证的是配额准入不能被正常请求流程随意绕过;实现细节和状态更新时延不应被用于构建精确计费账本。

删除对象也不是“立即释放所有预算”的同步操作。对象删除可能经历:

  • 删除请求被接受;
  • 对象进入 terminating 状态;
  • finalizer 阻止完全删除;
  • 控制器清理关联对象;
  • 配额控制器重新计算使用量。

因此,在自动扩缩容或高频 Job 场景中,应为配额释放延迟和控制器重试设计容错。


14. 多租户治理中的生产取舍

14.1 租户预算要覆盖“突发”还是“稳定容量”

如果把 requests.cpu 配得过高,租户可能很快耗尽配额,即使实际 CPU 使用率并不高;如果配得过低,调度器会低估资源需求,运行时可能频繁争抢资源。

常见预算模型是分别设置:

requests.cpu: "8"
limits.cpu: "16"
requests.memory: 16Gi
limits.memory: 24Gi

它表达的是:

  • 稳定调度承诺按 8 CPU、16 GiB 内存计算;
  • 允许声明的上限总和达到 16 CPU、24 GiB;
  • 实际是否能持续获得突发资源,仍取决于节点余量和调度结果。

这不是对租户的性能 SLA。若需要性能隔离,还要控制节点池、拓扑、优先级、抢占和底层平台容量。

14.2 对象数量配额要考虑控制器行为

限制 podsjobsconfigmapssecrets 可以防止错误脚本或租户程序无限创建对象,但过小的配额会影响:

  • Deployment 滚动更新;
  • Job 重试和历史保留;
  • Helm 或 Operator 产生的辅助对象;
  • 服务发现和配置发布;
  • 失败对象清理之前的短暂堆积。

对象配额应按“峰值并发对象数”估算,而不是只按稳定状态估算。

14.3 ResourceQuota 不是计费系统

配额适合做准入保护:

  • 防止一个 Namespace 消耗掉集群全部声明容量;
  • 为租户建立资源上限;
  • 在创建阶段快速失败。

它不适合单独承担:

  • 按时间计费;
  • 统计实际 CPU 使用;
  • 统计实际内存工作集;
  • 统计云盘实际占用;
  • 判断服务是否达到 SLO。

这些需求需要 Metrics Server、Prometheus、云平台账单或专门的成本系统。

14.4 版本和发行版差异

本文示例使用当前稳定的 Kubernetes 核心 API:

  • ResourceQuotaapiVersion: v1
  • LimitRangeapiVersion: v1
  • Deployment:apps/v1

但以下行为应按集群版本验证:

  • 新增的资源类型是否能使用 count/<resource> 配额;
  • scopeSelector 支持的匹配维度;
  • Pod 级资源字段和相关特性门控;
  • CSI 存储驱动对扩容、快照和克隆的实现;
  • 云厂商对节点、云盘和配额的额外限制。

可使用以下命令检查当前 API:

kubectl version
kubectl api-resources --namespaced=true
kubectl explain resourcequota
kubectl explain limitrange

不要因为某个云厂商控制台提供了“租户配额”功能,就假定它与 Kubernetes ResourceQuota 统计同一组资源;两者可能分别位于云 API、集群控制面和 Kubernetes Namespace 层。


15. 一套可验证的治理关系

可以把两者的职责归纳为四个连续问题:

  1. 这个容器的资源字段是否完整?
    LimitRange 的默认值和校验规则处理。

  2. 这个 Pod 或 PVC 是否超过单对象边界?
    LimitRangeContainerPodPersistentVolumeClaim 条目处理。

  3. 这个 Namespace 是否还有累计预算?
    ResourceQuotahard 和匹配范围处理。

  4. 集群是否真的有能力运行它?
    由调度器、节点资源、存储供给器、kubelet 和运行时共同决定。

一个完整的租户策略通常至少包含:

# 单对象边界和默认值
kind: LimitRange
spec:
  limits:
  - type: Container
    defaultRequest:
      cpu: 100m
      memory: 128Mi
    default:
      cpu: 500m
      memory: 512Mi
    max:
      cpu: "2"
      memory: 2Gi

# Namespace 累计预算
kind: ResourceQuota
spec:
  hard:
    requests.cpu: "4"
    requests.memory: 8Gi
    limits.cpu: "8"
    limits.memory: 16Gi
    pods: "20"
    requests.storage: 100Gi
    persistentvolumeclaims: "10"

其中 LimitRange 负责让单个工作负载的资源声明具有可预测形态,ResourceQuota 负责把所有工作负载的累计消耗控制在租户预算内。二者共同作用时,默认资源才会进入可计算的预算模型;但它们仍然不能替代 RBAC、NetworkPolicy、Pod 安全策略、调度隔离和运行时监控。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。