前一阵子帮一个客户排查线上事故,集群里几十个命名空间,某个算法团队的作业一上来,直接把整个生产环境的节点内存打满,其他业务Pod全部被驱逐。事后复盘时发现,这个命名空间从来没配置过任何资源约束,它就像一个无底洞,想申请多少就申请多少。这个问题在Kubernetes集群里太典型了——很多团队部署Kubernetes很熟练,但对资源配额(ResourceQuota)的精细化管控却停留在"知道有这个东西"的层面。这篇文章就把我在这类实战里沉淀下来的方案、坑和调优经验完整展开,聊聊怎么用资源配额策略真正把多团队共享集群的资源管住。
这篇文章适合正在维护多团队共享集群的运维和平台工程师,也适合已经入门Kubernetes、想深入理解资源治理机制的开发者。你会看到ResourceQuota与LimitRange、HPA的边界到底在哪,一份可落地的配额方案怎么从零设计,以及配额耗尽时最常见的几个"案发现场"和完整排障链路。
1. 先想明白一件事:资源配额防的是失控,不是性能
很多人对资源配额的第一个误解是"配额是用来提升性能的"。这句话只对了一半。ResourceQuota真正解决的问题,是多个团队或应用共享同一个集群时,某一个命名空间的资源消耗不会无限膨胀,挤压其他人的生存空间。性能优化是HPA和节点规格的事,配额是"资源分配规则"的事,两者不能混为一谈。
我见过一个非常典型的失控案例:一个测试环境的命名空间里,开发人员为了排查问题,一次提交创建了50个8C16G的Pod,整个集群总共才64C256G。结果就是其他所有团队的Pod瞬间Pending,甚至节点上已经在运行的Pod因为内存压力被反复驱逐。更有意思的是,这个操作并不是恶意的,只是某个部署脚本的副本数变量写错了。没有配额,一个100行不到的YAML就能让整个集群雪崩;有了配额,这个操作在API Server的准入阶段就会被拦截,业务方的反馈从"集群崩了"变成"创建Pod失败,资源配额超限"——这个差别是本质性的。
从这个角度切入,ResourceQuota在精细化管理里的角色其实像预算控制,而不是实时监控。它管的是"你承诺要用的量"(requests)和"你最多能用的量"(limits),这两个值是写死在Pod定义里的,API Server在创建Pod时就会做累加校验,而不是等到Pod真正跑起来吃满了CPU才发现问题。这就引出了一个很关键的特性:配额检查发生在调度之前,发生在API Server的准入控制阶段。
1.1 配额的三类管辖范围
Kubernetes原生ResourceQuota的能力可以分成三大块:
- 计算资源配额:管理CPU和内存的requests与limits总和,这是最常用也最核心的部分。
- 存储资源配额:管理所有PVC声明的存储总量,也可以按StorageClass分别设限。
- 对象数量配额:管理命名空间内Pod、Deployment、Service、PVC等对象的个数上限。
这个划分决定了配额策略的设计思路:不是拿一个YAML把所有东西都塞进去,而是按资源类型分层设计。CPU和内存关注的是"容量",对象数量关注的是"规模",存储关注的是"持久化成本",三者是不同维度的问题,自然要用不同的配额文件来管理。
还有一个容易被忽略的点:ResourceQuota只挂在命名空间级别。Kubernetes原生没有"全局配额"或者"跨命名空间配额"的概念。如果你希望做集群级别的总预算,比如整个集群最多只能跑200个Pod,那就得在每一个命名空间都落一份配额,或者借助外部策略引擎(比如Kyverno)在创建Namespace时自动注入配额模板。我后面会详细讲这个自动注入的思路。
2. ResourceQuota与LimitRange、HPA的职责边界:别再傻傻分不清
这一节专门写给那些把ResourceQuota、LimitRange、HPA三个概念搅在一起的人。这三兄弟看着都跟"资源"有关,但其实管的是完全不同的三个层面。
我用一句话概括它们的分工:
- ResourceQuota管的是"一个命名空间总共能用多少",作用于一组Pod的聚合。
- LimitRange管的是"单个Pod最多能要多少、最少要多少",作用于单个Pod的维度。
- HPA管的是"业务量变化时副本数自动扩缩多少",作用于工作负载的副本数。
资源管控的完整链条是:先用LimitRange限制每个Pod不能无限申请(比如单个Pod内存不允许超过4Gi),再用ResourceQuota限制整个命名空间的所有Pod加起来不能超过某个总量(比如内存总和不超过32Gi),最后用HPA在既定的总量范围内根据负载弹性伸缩。
| 维度 | ResourceQuota | LimitRange | HPA |
|---|---|---|---|
| 作用范围 | 命名空间 | Pod/Container | 工作负载 |
| 管什么 | 聚合用量上限 | 单容器资源上下限 | 副本数伸缩 |
| 触发阶段 | 创建/更新时准入校验 | 创建/更新时准入校验 | 周期监控指标 |
| 超出后的表现 | 请求被拒绝 | 请求被拒绝或自动补默认值 | 副本数调整 |
很多线上事故的根源就在于只配了LimitRange没配ResourceQuota,或者反过来。只配LimitRange的后果是:每个Pod看似都有上限,但没人限制Pod的总量,一个命名空间照样可以起几百个4C8G的Pod把整个集群打爆。只配ResourceQuota不配LimitRange的后果是:如果业务方在Pod里不写resources字段,配额根本不会拦截它(因为requests和limits都是0),等于形同虚设。
2.1 requests/limits语义:理解配额的底层逻辑
要真正用好ResourceQuota,必须先搞清楚requests和limits的语义,否则后面所有配置都是空中楼阁。
- requests是预留值:Pod被调度时,调度器会累加每个Pod的requests,确保节点上的可分配资源不小于所有Pod的requests之和。这个值代表"我至少需要这么多资源"。
- limits是上限值:容器实际运行时的资源使用量不能超过这个值。CPU超过limits会被限流,内存超过limits且触发OOM时会被杀掉。
ResourceQuota里面最常见的配置就是requests.cpu和limits.cpu分别设一个值。这两个值可以不一样,而且通常不一样。由于CPU是可压缩资源,你完全可以让所有Pod的limits之和大于requests之和,实现超卖;但内存不可压缩,超卖过头的代价就是OOM Killer在工作,所以生产环境里我通常建议limits.memory的总和不大于节点实际可分配内存。
这里需要补充一个底层机制:ResourceQuota这个准入控制器校验的是Pod声明里的requests和limits,不是Pod运行时的真实监控指标。也就是说,即使业务方写了一个requests.cpu: 100m但实际跑满了4个核,配额那边看到的仍然只有100m。配额解决的是"申请失控",真实用量失控得靠监控告警去兜底。这一点不在配额的能力范围内,但很多刚接触配额的人会误以为它是个用量监控工具,这里提前说清楚。
2.2 配额对存储和对象数量的管控方式
存储配额跟计算资源配额不一样,它统计的是PVC申请的总量。如果PVC指定了StorageClass,可以用<storage-class-name>.storageclass.storage.k8s.io/requests.storage来按存储类分别限流。比如集群里有fast-ssd和archive-hdd两个StorageClass,你可以规定fast-ssd类的PVC总量不超过100Gi,archive-hdd类的PVC总量不超过500Gi,这样不同性能等级的存储成本就能分账管理。
对象数量配额的字段名很容易记混。核心规则是:count/<resource>.<group>这种格式,比如count/pods、count/deployments.apps、count/services。对于pods这种核心资源,可以直接写count/pods;对于Deployment这种属于apps组的资源,必须写成count/deployments.apps。我见过有人在配额里写count/deployment导致完全不生效的,就是因为少了.apps这个组名后缀。
3. 从零设计一套配额方案:三份YAML与逐行拆解
说了这么多原理,下面给出一套可以直接落地的方案。假设场景是:一个12节点的生产集群(每节点16C64G),总共192C768G,承载三个业务团队共用的多个命名空间。目标是给每个命名空间设计一套合理的配额。
我习惯把配额拆成三份文件:compute-quota.yaml(计算资源)、storage-quota.yaml(存储)、object-count-quota.yaml(对象数量)。这样分开管理的好处是:不同资源类型的配额变更频率不一样,计算资源可能每周调一次,对象数量的配额可能半年都不用动,拆开以后方便独立审计和修改。
3.1 计算资源配额YAML
apiVersion: v1 kind: ResourceQuota metadata: name: compute-quota namespace: team-a spec: hard: requests.cpu: "24" requests.memory: "96Gi" limits.cpu: "48" limits.memory: "192Gi"这份配置的含义非常直白:team-a命名空间里所有Pod的requests.cpu总和不能超过24核,requests.memory总和不能超过96Gi;所有Pod的limits.cpu总和不能超过48核,limits.memory总和不能超过192Gi。
看到这里你可能会问:为什么requests和limits差距这么大?这其实是故意的。CPU是压缩型资源,超卖一倍甚至更多问题不大,只要节点有足够的空闲CPU,业务方可以短暂飙到很高的CPU使用率,Quota不会拦它。但内存不能这么干,因为内存一超就OOM。所以生产环境我一般建议requests.memory : limits.memory不超过1:2,而且limits.memory的总和要控制在节点的可分配内存总量以内,留出系统组件和DaemonSet的余量。
细心的读者可能还会问:requests.cpu: "24"这个数字是怎么定出来的?常用思路是:先看这个团队过去两周的监控数据,取namespace:container_cpu_usage_seconds_total的P95聚合值,再乘以1.5到2的安全系数。比如过去两周P95总CPU用量是10核,配额就定24核左右。这样既不会让业务方频繁触顶,又能防止资源被无意义地占用。内存类似,取P95内存用量乘以1.2到1.5。
3.2 存储资源配额YAML
apiVersion: v1 kind: ResourceQuota metadata: name: storage-quota namespace: team-a spec: hard: persistentvolumeclaims: "20" requests.storage: "500Gi" fast-ssd.storageclass.storage.k8s.io/requests.storage: "200Gi" archive-hdd.storageclass.storage.k8s.io/requests.storage: "300Gi"这里给了两层限制:PVC总数不超过20个,存储总量不超过500Gi,同时按StorageClass拆了细分限额。requests.storage统计的是所有PVC的容量申请之和,而fast-ssd...后缀的字段只统计指定StorageClass的PVC。
值得提醒的是,PVC对象在被删除后如果一直处于Terminating状态(通常是因为有PVC Finalizer没清干净),它的容量仍然会计入配额使用量,这一点非常容易踩坑。后面排障章节我会详细展开。
3.3 对象数量配额YAML
apiVersion: v1 kind: ResourceQuota metadata: name: object-count-quota namespace: team-a spec: hard: count/pods: "60" count/deployments.apps: "20" count/services: "30" count/persistentvolumeclaims: "20" count/configmaps: "50" count/secrets: "50"对象数量配额防的是啥?防的是某个团队把命名空间当成垃圾桶,什么对象都往里丢。我见过一个开发环境的命名空间里躺着几千个没人用的ConfigMap,每个几百字节,看起来不占资源,但etcd的压力上去了,kubectl get也变慢了。对象数量配额把这些看不见的隐患也管住了。
count/configmaps和count/secrets这两个字段很多团队会忽略,我建议至少给个几百的上限,防止CI/CD系统反复创建临时对象导致etcd里堆积垃圾数据。
3.4 部署和验证步骤
配额文件写好后,部署非常直接:
kubectl apply -f compute-quota.yaml kubectl apply -f storage-quota.yaml kubectl apply -f object-count-quota.yaml验证是否生效,先看配额概览:
kubectl get resourcequota -n team-a输出大致长这样:
NAME AGE REQUEST compute-quota 5m requests.cpu: 12/24, requests.memory: 52Gi/96Gi ...这个REQUEST列里的格式是used/hard,前者是当前已经占用的量,后者是配置的上限。还可以用kubectl describe resourcequota compute-quota -n team-a看更详细的分项。刚刚apply完时,used可能是0,也可能因为命名空间里已有的对象直接显示为某个非零值——这说明已有资源会计入配额,不存在"老Pod不受影响"的说法。
部署完成后建议立刻做一次负向测试:在一个超出配额的命名空间里创建一个超过配额的Deployment,确认它会报错。具体操作是创建Pod前预计一下当前used量,构造一个超量的请求。
4. 配额用尽的真实战场:排障过程与复现路径
配置配额只是第一步,真正考验人的是配额用尽之后的排障。这一章列几个我在实战中反复遇到的场景,每个场景都包含完整的排查链路,你可以直接照着复现。
4.1 场景一:Pod一直Pending,describe显示QuotaExceeded
现象:新创建的Deployment的Pod一直处于Pending状态,kubectl get pods看不到任何异常,Events里提示无法调度。
排查链路:
kubectl describe pod <pod-name> -n team-a观察Events输出。如果最后一行出现类似这样的内容:
0/12 nodes are available: 12 Insufficient cpu.那可能是节点资源真的不够。但如果出现的是:
Error creating: pods "xxx" is forbidden: exceeded quota: compute-quota, requested: requests.cpu=2, used: requests.cpu=24, limited: requests.cpu=24那就是配额用尽了。这两者的区别非常关键——一个是调度器在选节点时发现没有合适的节点,另一个是API Server在准入阶段直接拒绝了请求。两者的处理方式完全不同,前者加节点或者调调度策略,后者只能等命名空间里其他Pod释放资源,或者调大配额。
看到了配额用尽,下一步要回答的问题是"谁把配额吃掉了?"我最常用的命令是:
kubectl get pods -n team-a --sort-by=.status.phase先看有没有状态异常的Pod,再看有没有副本数异常膨胀的Deployment。如果发现几十个Evicted的Pod躺在命名空间里,这些Pod虽然不运行了,但它们仍然会计入配额,这一点很多人不知道。Evicted的Pod不会自动被清理干净(除非有垃圾回收控制器兜底),它们像幽灵一样占据着配额数字。处理方法很简单:
kubectl delete pods -n team-a --field-selector=status.phase=Failed删完以后再看kubectl get resourcequota -n team-a,used值就会降下来。
4.2 场景二:Deployment滚动更新卡死
这个场景非常有代表性。现象是:某团队发版时改了Deployment里的镜像,但新版本就是起不来,滚动更新一直处于等待状态。
根因分析:Deployment滚动更新的逻辑是先创建一个新ReplicaSet,新Pod起来了再缩掉旧的。问题在于,新Pod的创建请求也受ResourceQuota限制。如果当前命名空间的配额已经用尽,新Pod创建失败,Deployment就会一直卡在"等待旧副本缩容"或者"等待新副本就绪"的状态。
我看过最典型的案例:team-a的requests.cpu配额是24核,当前已经用了23.5核,新版本Deployment一上来要申请1核,直接超限,整个发布卡死。而旧的Pod占了7核,如果rollingUpdate的maxUnavailable又设成了0,新Pod起不来旧Pod就不会缩容,死锁就产生了。
解决思路有几个方向:
- 手动把旧ReplicaSet缩容,给新Pod腾出配额空间。
- 把
maxUnavailable调成1,让滚动更新可以先把旧的缩掉再起新的。 - 发布前检查配额余量,确定够再触发更新。
这个坑最阴险的地方在于它不会直接报"配额不足"给你看,现象上更像是"发布卡住"。排查时记得先看ReplicaSet的状态,再顺着Events往上看有没有exceeded quota字样。
4.3 场景三:更新Pod的resources字段失败
这个场景比较隐蔽。现象是:直接对一个正在运行的Pod执行kubectl edit pod,修改它的resources字段,保存时报错配额超限。为什么会这样?因为Pod的资源定义变了以后,API Server要重新做一次准入校验,重新累加这个命名空间所有Pod的requests和limits总和,如果新的总和超过配额,更新就会被拒绝。
这个限制对StatefulSet这类带持久化状态的工作负载尤其头疼——如果你想把某个有状态服务的requests调大,而配额又没有余量,只能先缩容或者删掉几个Pod释放额度,不能直接原地改。
我在这里养成了一个习惯:调大一个Pod的requests之前,先看一眼命名空间的配额余量。比如用kubectl describe resourcequota compute-quota -n team-a,看requests.cpu的used/hard差距,确保改动后不会超限,再执行操作。
4.4 场景四:PVC删不掉导致存储配额不释放
这个坑在存储配额里特别常见。现象:PVC处于Terminating状态好几天了,kubectl get pvc能看到名字,但状态一直是Terminating,配额里的requests.storage也没有减少。原因通常是PVC上还挂着一个Finalizer,比如kubernetes.io/pvc-protection,这个Finalizer要等PVC绑定的Pod和PV的引用都清理干净才会被移除。
排查链路:
kubectl get pvc <pvc-name> -n team-a -o yaml | grep finalizers如果Finalizer卡住了,最常见的处理方法是查看有没有还在使用这个PVC的Pod,如果有就删掉;如果没有,确认是控制器的问题,再手动移除Finalizer:
kubectl patch pvc <pvc-name> -n team-a -p '{"metadata":{"finalizers":null}}' --type=merge这个操作属于"最后一招",动手前务必确认PVC里没有还没备份的数据。Finalizer清掉之后,PVC被真正删除,配额才会释放。
4.5 场景五:LimitRange默认值导致意外拒绝
这个场景比较反直觉。现象:命名空间里配置了ResourceQuota之后,一个完全没写resources字段的Pod反而被拒绝了,报错内容还是配额超限。
原因在于,如果你同时给这个命名空间配了一个带默认值的LimitRange,API Server在准入阶段会先走LimitRange控制器,给没有显式声明resources的Pod自动填充默认的requests和limits,然后再走ResourceQuota校验。也就是说,LimitRange补的默认值也算进了配额累加里。结果就是业务方觉得自己"啥都没要",系统却按默认值算了他的配额消耗。
解决思路是:LimitRange的默认值要设计得合理,而且要跟配额配合着看。比如LimitRange默认requests.memory: 512Mi,那这个命名空间如果有20个不写resources字段的Pod在跑,光这些Pod就吃掉了10Gi内存配额。如果这个数字远超预期,业务方的无效默认值就会悄悄吃掉配额空间。
5. 多团队场景下的配额治理:引入配额模板和动态调优
最后聊一聊规模化的做法。如果你维护的集群里有几十个命名空间,挨个手写ResourceQuota YAML是不现实的。我的经验是分三步走。
5.1 用GitOps和工具链做配额模板化
在生产环境,我会把ResourceQuota纳入GitOps工作流,用Kustomize或Helm管理。比如用Kustomize维护一个base目录,里面的ResourceQuota是默认规格,然后每个团队一个overlay目录,通过patches调整具体数值:
base/ kustomization.yaml compute-quota.yaml team-a/ kustomization.yaml compute-quota-patch.yamlpatch文件里只写增量变更:
apiVersion: v1 kind: ResourceQuota metadata: name: compute-quota spec: hard: requests.cpu: "32" requests.memory: "128Gi"这样每个团队的配额差异一目了然,审计时有据可查。配合ArgoCD或Flux,谁的配额被改了、改了什么值,在提交记录里都能看到,省去大量扯皮时间。
5.2 把配额和LimitRange一起模板化
配额模板化的时候,我还会顺手把LimitRange一起加进去。一个命名空间最少要有这两样东西:
- 一个LimitRange,给所有Pod提供合理的默认requests/limits,并限制单Pod最大规格。
- 一个ResourceQuota,限制整个命名空间的聚合用量。
新建Namespace时自动注入这两个模板,最简单的方式是写一个极小的准入Webhook,在Namespace创建时自动打上这两份配置。也可以直接用Kyverno这类策略引擎的generate规则来做,代码量更少。这样从源头保证任何新团队进入集群时,资源治理规则是默认为开启的,而不是靠口头提醒。
5.3 配额用量的监控告警
配额配好不等于不管了,Quota本身不会在用量接近上限时自动告警。我的监控方案是用kube-state-metrics暴露的kube_resourcequota指标,配一组Prometheus告警规则:
groups: - name: quota-alerts rules: - alert: ResourceQuotaUsageHigh annotations: summary: "Namespace {{ $labels.namespace }} quota usage high" expr: | kube_resourcequota{type="used"} / kube_resourcequota{type="hard"} > 0.85 for: 15m labels: severity: warning告警阈值通常设85%。低于这个值说明余量充足,超过这个值就要留意了。如果某个命名空间的配额用量频繁在90%以上徘徊,说明要么配额定得太紧,要么业务增长太快,该做一次正式的配额扩容评审了。
5.4 关于动态调整,我的个人体会
最后说点偏经验的东西。配额不是设置完就一劳永逸的,它更像团队之间的契约。我见过不少运维同学把配额卡得死死的,业务方一超就拒绝,结果双方关系搞得很僵;也见过完全不管配额,集群三天两头出事故的,都不健康。
比较稳妥的节奏是:每季度做一次全集群配额审视,结合监控数据和业务方的实际使用情况,该松的松、该紧的紧。比如开发环境可以宽松一点,超卖比例高一些,毕竟偶发失败代价低;生产环境则要严格一些,宁可多留一些buffer。
我自己的想法是:配额真正的价值不是限制,而是给每个团队一个确定性——你在预算内怎么折腾都行,但出了预算要提前打报告,而不是等到把集群搞挂了再被动的救火。这套机制跑顺以后,平台团队和业务团队之间的关系会健康很多。