上周帮一个开发团队排查Pod启动失败,Event列表里刷出来一连串forbidden,错误信息写着:minimum memory usage per Container is 200Mi, but request is 0。群里的第一反应是“节点资源不够了”,但我去看节点水位,CPU和内存都富余得很。真正的原因,是命名空间里躺着一个大家早就忘记了的LimitRange。
这个场景估计不少人都遇到过。LimitRange是Kubernetes里一个轻量但非常“霸道”的准入控制机制,它不抢资源、不调度、不监控,只在你创建Pod的那一刻决定一件事:这个Pod的资源声明,够不够格、合不合法、要不要替你补上。如果你没搞清楚它的脾气,它就能在你上线前十分钟给你上一课。
这篇文章我不打算写文档翻译,就按实际配置经验和踩坑记录来讲:LimitRange到底管什么、配置项怎么拆、默认值机制有什么坑、配完怎么验证、出了问题怎么排查。
1. 为什么需要LimitRange:失控的资源声明才是常态
1.1 一个不算罕见的失控现场
先还原一下那个团队的现场。他们有个开发命名空间,几十个微服务在里面共跑,之前一直是各团队自己写Deployment,谁也没在意资源声明。后来有个新来的同学部署一个新服务,YAML里resources字段没写,Pod直接创建失败。他查了Event,以为是节点资源不足,还去扩容了一个节点——结果没用。
问题不在节点,而在准入阶段就被“劝返”了。LimitRange是一个内置准入控制器(Admission Controller),请求打到API Server之后、etcd持久化之前,会先经过它。它看到你的Pod资源声明不合法,直接拒绝写入。
如果没有LimitRange,这个Pod其实能创建成功,只是永远Pending,因为调度器发现节点没那么多资源可分。有了LimitRange,错误多了一道前置拦截,表面上看“更严格”,但因为大家不知道这条规则存在,反而更难排查。
所以第一件事要建立认知:LimitRange不是在帮你分配资源,是在帮你强制声明资源。它不关心你的节点有没有资源,只检查你的YAML合不合规。
1.2 LimitRange和ResourceQuota的分工
很多人分不清这两个概念,我也被问过很多次。一句话总结:
- ResourceQuota:管命名空间的总量,比如“整个dev命名空间CPU请求总和不能超过20核”。
- LimitRange:管单个Pod或容器的最小、最大、默认资源声明,比如“单个容器CPU不能超过2核,至少请求100m”。
两者可以独立使用,也可以组合。组合时有一个特别容易踩的坑:如果ResourceQuota要求命名空间里所有Pod都有requests.cpu,但LimitRange只设了default没设defaultRequest,那Pod的requests仍然是0,照样会被Quota拦下。所以做平台规范时,我通常建议两个一起设计,LimitRange负责单Pod边界,ResourceQuota负责总量边界,并且记得把请求和限制两个维度都覆盖到。
1.3 LimitRange的四种能力,一次讲清
LimitRange能做的,归纳起来是四件事:
- 默认值(default / defaultRequest):Pod没声明
limits或requests时,自动帮你填上。这是对开发体验最友好的能力。 - 上限(max):声明的
limits不能超过这个值。 - 下限(min):声明的
requests不能低于这个值,通常也用来配合比例限制。 - 比例(maxLimitRequestRatio):
limits和requests的比值不能超过某个倍数,防止“请求100m但限制8核”这种极端浪费。
它们的作用对象可以是容器(type: Container),也可以是Pod(type: Pod)。区别在于:Container类型作用于每个容器,Pod类型作用于Pod内所有容器的资源之和。default、defaultRequest、maxLimitRequestRatio这三个只能作用于容器,max和min则两种都支持。这个细节后面配置的时候还会再碰见。
2. 一份能直接落地的LimitRange配置:逐字段拆解
2.1 一个完整示例
下面这份是我在实际开发环境里用过的模板,包含了常规需要的所有维度:
apiVersion: v1 kind: LimitRange metadata: name: dev-pods-range namespace: dev spec: limits: - max: cpu: "2" memory: 2Gi min: cpu: 100m memory: 200Mi default: cpu: "1" memory: 1Gi defaultRequest: cpu: 500m memory: 512Mi maxLimitRequestRatio: cpu: "3" type: Container这份配置表达的含义是:在dev命名空间中,每个容器的CPU请求至少100m、最多2核,内存请求至少200Mi、最多2Gi;如果Pod没写资源声明,自动填入请求500m/512Mi、上限1核/1Gi;并且CPU的limits不能超过requests的3倍。
使用前先创建命名空间并应用配置:
kubectl create namespace dev kubectl apply -f limitrange.yaml应用后可以这样查看生效情况:
kubectl get limitrange -n dev kubectl describe limitrange dev-pods-range -n dev注意LimitRange是命名空间级别的资源,跨命名空间不生效。你配置在dev,生产环境prod什么也不会有。很多团队只在某个namespace配了,别的namespace出同样问题,原因是没配,不是“规则漏了”。
2.2 参数对照表与生效对象
为了配置时不迷糊,我把参数、作用、支持类型整理成一张表,建议收藏:
| 参数 | 作用 | 生效类型 |
|---|---|---|
max | 限制最大limits,配额合法性校验 | Container / Pod |
min | 限制最小requests,低于即拒绝 | Container / Pod |
default | 未声明limits时自动填充 | 仅 Container |
defaultRequest | 未声明requests时自动填充 | 仅 Container |
maxLimitRequestRatio | limits / requests最大比值 | 仅 Container |
type | 指定作用于容器还是Pod聚合 | 必填 |
假设你配置了type: Pod的max,那么它是把Pod内所有容器的同类资源相加再与上限比较。比如两个容器各声明500m CPU请求,Pod级别的max就不能低于1000m;但如果type是Container,则每个容器各自受500m上限约束,两个加起来可以到1000m。这个区别在微服务多容器Pod场景下非常关键,别搞混。
2.3 单位、语义与几个“非法组合”
资源单位的坑也值得单独说。CPU的单位默认是“核”,支持小数和毫核两种写法:0.5和500m等价,写成整数1表示1核。内存的单位要特别注意二进制和十进制的区别:128Mi是128 \* 1024 \* 1024,128M是128 \* 1000 \* 1000,两者差了约34MiB。Kubernetes里建议优先用Ki/Mi/Gi这套二进制单位,语义更精确,避免换算偏差。
还有一个“非法组合”容易踩:假如你设置了maxLimitRequestRatio: cpu: 3,同时又设置了default: cpu: 2和defaultRequest: cpu: 500m,那么不带资源声明的Pod会先被自动填充成limits=2、requests=500m,比值是4,超过3,直接创建失败。所以配置里这几个参数不是孤立的,它们之间会互相制约。后面我会专门讲这个联动问题。
3. 三种创建路径与默认值机制的记忆陷阱
3.1 不想写YAML?一条命令搞定
如果只是想快速起一个策略,kubectl create limitrange可以一行生成:
kubectl create limitrange dev-pods-range \ --namespace=dev \ --max=cpu=2,memory=2Gi \ --min=cpu=100m,memory=200Mi \ --default=cpu=1,memory=1Gi \ --default-request=cpu=500m,memory=512Mi \ --max-limit-request-ratio=cpu=3命令参数和YAML字段基本一一对应:--max、--min、--default、--default-request、--max-limit-request-ratio。
但这里有个我特别推荐的小习惯:先用--dry-run=client -o yaml把命令转成YAML,再决定是apply还是继续用命令创建:
kubectl create limitrange dev-pods-range \ --namespace=dev \ --max=cpu=2,memory=2Gi \ --dry-run=client -o yaml这样可以先把声明做成文件留痕,团队评审和后续GitOps都方便。直接敲命令创建也没问题,就是不好追溯。极限层面,kubectl create limitrange默认会以Container类型创建所有限制,如果你需要Pod级别的max/min,它是不支持的,只能走YAML。
3.2 default 和 defaultRequest:最容易记反的一对
这两个字段是LimitRange默认值机制的核心,也是我见过被记反最多的。
default:给limits设默认值。defaultRequest:给requests设默认值。
从名字逻辑上,default是“兜底的限制”,defaultRequest是“兜底的请求”,不是“默认值”和“默认请求值”这种模糊对应。记住一个侧面:defaultRequest存在,是因为requests单独需要默认值。Kubernetes里默认情况下,如果你不设置Pod资源,requests和limits都会是0,这会带来两个问题:
- 调度时requests=0,调度器认为它不占资源,实际运行却占用节点资源,形成超卖。
- 运行时CPU不设limits,Pod可以无限抢占CPU时间片;内存不设limits,Pod可以一直申请到触达节点上限乃至被OOM Kill。
设置默认值的意义,本质上是在团队没有养成声明习惯时,由平台侧接管兜底。
如果只设置default不设置defaultRequest,那么Pod没有声明requests时,requests不会被自动填充,仍然为0。这在有ResourceQuota要求请求值时会导致创建被拒。所以我的经验是:默认值通常成对设置,不要只填一半,只填一半时行为不是你预期的“另一半沿用某规则”,而是零值。
还有个容易忽略的点:default和defaultRequest只对新建的Pod生效,对已经存在的Pod没有回溯能力。也就是说后配置LimitRange,不会把集群里已经跑着的Pod“补”上默认值。你要么等它们重建,要么手动滚动重启。
3.3 max/min + default 的优先级关系
当Pod显式声明了资源时,default和defaultRequest不生效,只校验min和max。当Pod没声明时,先经过默认值填充,再走同一套校验。
这里有个隐藏优先级:如果default的值超过了max,创建必失败。Kubernetes不会帮你把default压到max以内,而是直接抛错。配置时要自查:default的limits <= max的limits,defaultRequest的requests >= min的requests。同理,default和defaultRequest的比例也得满足maxLimitRequestRatio。
我自己配置时,会先把default/defaultRequest设置成“比max/min更保守”的中间值,保证四种排列组合下都不冲突。如果团队要的是极严格治理,那直接把default和defaultRequest去掉,强制所有人显式声明,不声明就直接拒绝。
4. 配置之后的前后行为对比:三类场景实测验证
4.1 场景一:Pod完全没写resources
如果没有配LimitRange,这种行为是“允许创建,所有资源值默认0”。配了LimitRange之后,分两种情况:
情况A:配了default/defaultRequestPod创建时会自动补充,比如下面的Deployment没写resources:
apiVersion: apps/v1 kind: Deployment metadata: name: web namespace: dev spec: replicas: 1 selector: matchLabels: app: web template: metadata: labels: app: web spec: containers: - name: web image: nginx:1.23实际生效的Pod资源会被填充为requests: cpu=500m, memory=512Mi,limits: cpu=1, memory=1Gi。用下面命令能查到:
kubectl get pod -n dev -o jsonpath='{.items[0].spec.containers[0].resources}'情况B:只配了min没有配default这时候创建直接失败。API Server的拒绝消息类似:
pods "web-xxx" is forbidden: minimum memory usage per Container is 200Mi, but request is 0对平台规范来说,这其实是好事,强制团队写资源声明。但对终端开发者来说,这个报错如果不提示LimitRange存在,很容易被误判成节点不足,所以建议在团队内部发一份文档说明“dev命名空间启用了资源声明校验”。
4.2 场景二:显式声明了resources的Pod
这类Pod会跳过默认值填充,直接校验min/max。我们试几个case:
containers: - name: web image: nginx:1.23 resources: requests: cpu: 100m memory: 200Mi limits: cpu: 500m memory: 1Gi这个能创建成功,因为它满足min和max。但如果把requests调成50m:
requests: cpu: 50m memory: 200Mi创建就会被拒,理由是最小CPU请求是100m。再换一个,声明requests=100m但limits=2:
requests: cpu: 100m memory: 200Mi limits: cpu: 2 memory: 2Gi这个在CPU max上正好擦边,2核没超,但算一下ratio:2000m / 100m = 20,远超我们配置的maxLimitRequestRatio: 3,所以同样会被拒。这种case最容易让开发不理解:明明没超max,为什么不让创建?因为你的请求和限制之间差距过大,随时可能抢占大量CPU,平台不希望出现这种资源黑洞。
4.3 场景三:静态Pod、InitContainer与Pod级别限制
这个场景容易出奇效,也容易出错。
先说静态Pod。LimitRange是API Server准入层面的限制,而静态Pod是kubelet直接根据manifest文件创建的,不经过API Server,所以LimitRange管不到,也不会被填充默认值。这个很多人不知道,如果你在用静态Pod,它就能“绕过”这套规则。
再说InitContainer。LimitRange的默认值也会作用到init容器上,而校验整个Pod是否合法时,Kubernetes会把init容器和普通容器合并计算。具体规则是:计算Pod资源总量时,普通容器各自的requests/limits相加,init容器则取所有init容器中的最大值,最后两项取较大值。之所以这么算,是因为init容器是串行执行的,不会同时运行,取最大值更接近真实峰值。
举个实际例子:Pod里有一个init容器声明了2Gi内存请求,两个普通容器各声明1Gi,那么针对这个Pod的内存请求总量就是3Gi(因为init容器的2Gi小于两个普通容器的之和2Gi,这里碰巧相等;如果init容器声明4Gi,那总量就是4Gi)。配置Pod级别min/max时,要把这种计算逻辑考虑进去,否则会出现“每个容器单独看都合规,但合并后超过Pod级限制”的情况。
4.4 更新和删除LimitRange的前后行为
修改LimitRange可以用kubectl edit或重新apply新版YAML:
kubectl edit limitrange dev-pods-range -n dev它只影响之后创建的Pod,已存在的Pod即使超过新限制也不会被打断。很多团队调整完限制后问“为什么存量Pod没变化”,我的回答一般是:符合预期,Kubernetes不会主动驱逐或重建存量Pod,你需要自己滚动重启,或者等Pod自然重建。
删除LimitRange同理:
kubectl delete limitrange dev-pods-range -n dev删除后不再有默认值注入,也不再校验min/max,但存量Pod维持当时的实际资源值不变,不会被“回收”。这里要提醒业务团队:删除LimitRange不等于给Pod“解除限制”,想要彻底改资源,还是得改工作负载的声明并滚动。
5. 排障链路与团队落地建议
5.1 一个标准的Pod创建失败排查流程
当Pod创建失败并且Event里报出forbidden字样时,我建议按下面这个链路走,避免无头苍蝇:
第一步,看Event:
kubectl get events -n dev --sort-by=.metadata.creationTimestamp | grep -i forbidden第二步,定位拒绝主体。如果是LimitRange拒绝,错误里通常会带资源值,比如:
minimum cpu usage per Container is 100m, but request is 0这个信息足够反推出是哪个LimitRange的问题,先看当前命名空间的LimitRange:kubectl get limitrange -n dev,再kubectl describe limitrange -n dev看具体规则。
第三步,对照Pod声明逐项比对:请求是否低于min,限制是否高于max,比例是否超过ratio,默认值是否与max冲突。把每一步列出来,基本能在几分钟内定位。
第四步,如果是改了LimitRange之后出问题,去看改前改后的差异,并确认当前待创建Pod是“新创建”还是“存量重建”。存量重建也会走校验,所以同一条规则会导致重建时失败,这种情况在滚动更新时特别常见。
5.2 几个最容易误判的边界情况
我见过不少平台组的同学也会栽在这几个点:
第一个误判是把min理解成“预留资源”。它不是。LimitRange只做准入校验,不参与调度,不做资源预留。即使Pod满足了min,调度时仍要看节点有没有足够剩余资源。
第二个误判是认为给LimitRange设置了default,所有Pod就自动带上资源。实际上已经运行中的Pod不会自动补上,默认值只在创建Pod时注入。如果你用kubectl get pods显示Running就以为策略生效了,记得先确认Pod的创建时间是否晚于LimitRange。
第三个误判是认为Pod级别的max/min和Container级别的max/min可以混着不设type。不写type时默认是Container,如果你在YAML里只写了max没写type,那它作用于单个容器,不是Pod总量。想表达Pod级别限制,必须显式写type: Pod。
第四个误判是修改LimitRange只调大max就能让新Pod的limits随便写。对,但只对新建Pod有效,且如果同时有ResourceQuota在管总量,单Pod调大只是“单点合法”而已,总量可能会卡在Quota。此时Event里会提示配额不足,别只盯着LimitRange看。
5.3 面向不同环境的落地策略
配置LimitRange没有银弹,我按环境类型给你一套建议,可以直接抄:
- 开发环境:核心目标是“大家省心”。设置
default和defaultRequest,让不写resources的Pod也能跑起来,同时控制上限不超过2核/2Gi,避免某人一口气把全namespace的CPU请求都塞进来。开发环境被逼着写资源声明意义不大,自动兜底更实际。 - 预发/生产环境:核心目标是“强制规范”。设置严格的
min和max,但不设置default和defaultRequest,谁不写资源直接拒绝创建。这不是为了整人,而是为了确保线上每个工作负载都有清晰可见的资源画像,出问题时能快速定位。 - 对外平台(多租户):组合使用LimitRange+ResourceQuota。LimitRange限制单租户单Pod边界,ResourceQuota限制单租户总量,且
default和defaultRequest都要配齐,因为租户的YAML质量不可控,平台侧必须先兜底再拦截。
我个人习惯在创建LimitRange之后立刻做一个冒烟测试:先apply一个不带resources的Deployment,看它是否被拒绝或被填充默认值;再apply一个超出max的Deployment,确认拒绝信息是否清晰。这两步走完,再放开给业务用。后面规划化Radar改造几乎撑占了配置表一半。生产别玩。
5.4 团队协作时的一个加分习惯
配置LimitRange不写注释,几个月后就是天书。我建议在每个LimitRange的metadata里加注释,写明“为什么限制是这个值”:
apiVersion: v1 kind: LimitRange metadata: name: dev-pods-range namespace: dev annotations: description: "dev命名空间资源边界,CPU限制2核、内存2Gi,默认填充500m/512Mi,防止单个容器超卖抢占" spec: limits: - max: cpu: "2" memory: 2Gi min: cpu: 100m memory: 200Mi default: cpu: "1" memory: 1Gi defaultRequest: cpu: 500m memory: 512Mi maxLimitRequestRatio: cpu: "3" type: Container这个annotations字段不影响任何功能逻辑,但在半年后别人排查问题时,能少浪费大量时间。同时,把LimitRange纳入Git版本管理并走评审,比直接在集群里敲命令更容易保持一致性。
反复被坑之后我养成一个习惯:每次为命名空间加LimitRange,我都会先用kubectl create limitrange ... --dry-run=client -o yaml生成YAML,加上说明注释,再apply,然后立刻用两个测试Deployment验证“无声明被兜底”和“超限被拒绝”两条路径。这套流程看着笨,但能省掉后面无数个深夜救火的半小时。