ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Kubernetes资源配额实战:多团队集群资源管控与排障指南

2026/9/13 14:40:16 拓冰建站 浏览量
Kubernetes资源配额实战:多团队集群资源管控与排障指南 前一阵子帮一个客户排查线上事故集群里几十个命名空间某个算法团队的作业一上来直接把整个生产环境的节点内存打满其他业务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在既定的总量范围内根据负载弹性伸缩。维度ResourceQuotaLimitRangeHPA作用范围命名空间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总量不超过100Giarchive-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 计算资源配额YAMLapiVersion: 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 存储资源配额YAMLapiVersion: 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 对象数量配额YAMLapiVersion: 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一直Pendingdescribe显示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.cpu2, used: requests.cpu24, limited: requests.cpu24那就是配额用尽了。这两者的区别非常关键——一个是调度器在选节点时发现没有合适的节点另一个是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-selectorstatus.phaseFailed删完以后再看kubectl get resourcequota -n team-aused值就会降下来。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如果有就删掉如果没有确认是控制器的问题再手动移除Finalizerkubectl patch pvc pvc-name -n team-a -p {metadata:{finalizers:null}} --typemerge这个操作属于最后一招动手前务必确认PVC里没有还没备份的数据。Finalizer清掉之后PVC被真正删除配额才会释放。4.5 场景五LimitRange默认值导致意外拒绝这个场景比较反直觉。现象命名空间里配置了ResourceQuota之后一个完全没写resources字段的Pod反而被拒绝了报错内容还是配额超限。原因在于如果你同时给这个命名空间配了一个带默认值的LimitRangeAPI 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{typeused} / kube_resourcequota{typehard} 0.85 for: 15m labels: severity: warning告警阈值通常设85%。低于这个值说明余量充足超过这个值就要留意了。如果某个命名空间的配额用量频繁在90%以上徘徊说明要么配额定得太紧要么业务增长太快该做一次正式的配额扩容评审了。5.4 关于动态调整我的个人体会最后说点偏经验的东西。配额不是设置完就一劳永逸的它更像团队之间的契约。我见过不少运维同学把配额卡得死死的业务方一超就拒绝结果双方关系搞得很僵也见过完全不管配额集群三天两头出事故的都不健康。比较稳妥的节奏是每季度做一次全集群配额审视结合监控数据和业务方的实际使用情况该松的松、该紧的紧。比如开发环境可以宽松一点超卖比例高一些毕竟偶发失败代价低生产环境则要严格一些宁可多留一些buffer。我自己的想法是配额真正的价值不是限制而是给每个团队一个确定性——你在预算内怎么折腾都行但出了预算要提前打报告而不是等到把集群搞挂了再被动的救火。这套机制跑顺以后平台团队和业务团队之间的关系会健康很多。