一、问题背景
平台组在多租户的 Kubernetes 集群里给每个业务命名空间(Namespace)统一下发了 ResourceQuota,把 CPU、内存、Pod 数量等资源的"预算上限"钉死,用来做资源隔离和成本核算——谁超了谁自己负责。这套机制上线大半年一直相安无事,直到某天傍晚一次常规发布,线上突然冒出一批 Pending 状态的 Pod,发布流水线卡在"等待调度"迟迟不结束。
当时这个命名空间跑着一个在线推理服务(白天 2 副本,晚高峰弹性扩到 6 副本)外加几个后台批处理 Job。我们第一反应是节点资源不够,但监控里节点 CPU/内存使用率明明还有富余。问题显然不在"机器没油",而在"配额被卡了脖子"。
二、故障现象
故障最直观的表现是:新版本的 Deployment 滚动发布时,只有旧副本被逐批删掉,新副本却始终 Pending,kubectl get pods -n <ns> 看到状态长时间停在 ContainerCreating 之前、直接就是 Pending。
describe 这些 Pending Pod,Events 区反复刷出一条:
|
|
也就是说:调度器根本没走到"找节点"那一步,就在准入阶段被 ResourceQuota 的准入控制器(QuotaAdmission)直接拦下了——used 已经摸到 limited 的天花板,requested 再多一点点就超。
更迷惑的是,同一时间还有两三个资源请求极小(yaml 里没写 resources.requests)的辅助 Pod 也卡在 Pending,看 describe 报的 requested: cpu=100m 这种微不足道的量,却同样 forbidden: exceeded quota。节点明明有空闲,配额数字也对不上"肉眼可见的用量",现场一度让人怀疑是集群 bug。
三、排查过程
第一步,确认是不是真超配额。 直接看配额实况:
|
|
输出里 Status.Hard 是上限(cpu: 8 / memory: 16Gi / pods: 30),Status.Used 是已用(cpu: 7800m / memory: 14Gi / pods: 28)。CPU 已用 7.8 核逼近 8 核上限,新 Pod 再要 500m 必然超。这一步坐实了:不是节点不够,是命名空间配额见顶,调度器在准入阶段就拒了。
第二步,搞清楚"谁把配额吃没了"。 列出该 ns 下所有 Pod 的 requests 累加:
|
|
一眼看清:推理服务晚高峰扩到 6 副本,每副本 cpu: 1.5 / mem: 2Gi,单这一个服务就吃掉 9 核里的 7.8 核——正是把 CPU 配额顶满的元凶。副本数一涨,request 总量线性叠加,瞬间撞线。
第三步,破解"小 Pod 也 Pending"的反常。 那几个没写 resources.requests 的辅助容器,为什么也要 100m?查该 ns 的 LimitRange:
|
|
里面有一条 defaultRequest: cpu: 100m, memory: 128Mi。关键点来了:当 Pod 不声明 requests 时,LimitRange 会自动注入默认 request;而 ResourceQuota 统计的是"实际生效的 request"(含被注入的默认值),不是 yaml 里写的。于是这些"看似零请求"的 Pod,每个悄悄占 100m,十几个叠起来又吃掉近 1.5 核,和推理服务的用量凑在一起,把本就不宽裕的配额彻底顶爆。节点有空闲毫无意义——quota 是先于节点调度的硬门槛。
第四步,定位"为什么没人早发现"。 翻历史:配额是半年前一次性设的,之后业务多次扩容、加新服务,但没有任何 quota 使用率的监控和告警。团队下意识以为 ResourceQuota “只限制 limit、不限制 request”,于是不少 yaml 只写了 limits 没写 requests,结果 request 被 LimitRange 注入默认值的规则反噬,谁也说不清真实占用。
四、解决方案
止血(分钟级):
- 紧急把推理服务副本从 6 缩回 3(
kubectl scale deploy infer-svc --replicas=3 -n <ns>),立刻释放约 4.5 核,Pending Pod 随之被调度起来,发布流水线恢复。 - 同步给真正重要的几个小服务显式补上合理的 requests(去掉对 LimitRange 默认值的依赖),让配额占用透明可控。
- 临时上调 ResourceQuota 天花板(
kubectl edit resourcequota compute-resources -n <ns>,cpu 8→12),给当晚高峰留缓冲——这是临时措施,不是根治。
根治(周内):
- 给所有 Deployment 补全
resources.requests/limits,消除"裸 Pod 靠 LimitRange 兜底"的盲区。 - 把 LimitRange 的
defaultRequest调小到与真实负载匹配,避免默认值虚高吃掉配额。 - 按"团队/环境"拆分命名空间,各自独立配额,不再多个团队挤一个 ns 共享预算。
五、根因分析
根因是对 ResourceQuota 的计量口径理解错误 + 默认注入的隐性放大:
ResourceQuota同时约束requests和limits的总量,而不仅仅限制 limit;LimitRange对未声明资源的容器会注入defaultRequest/defaultLimit,这些值计入 ResourceQuota 统计,等于"看不见的请求"也在吃预算;- 多团队共用一个 namespace 的单一配额,缺乏隔离,任一服务扩容都会挤占他人额度;
- 完全缺失 quota 使用率监控,配额在"温水煮青蛙"中耗尽,直到发布失败才暴露。
一句话:调度失败的根因不在节点,而在命名空间级准入控制把资源预算卡死,且这个预算被"副本扩容 + 默认注入"两股力量悄悄吃满。
六、预防措施
- 配额隔离:按团队、按环境拆分命名空间,每个 ns 独立 ResourceQuota,互不挤占;重要业务单独配额。
- LimitRange 收敛:把
defaultRequest/defaultLimit设为贴近真实的小值,别让"默认值"变成隐性大户;同时用default与max/min约束单容器资源上下限。 - 强制声明资源:通过准入控制(如 Kyverno/Gatekeeper 策略)禁止无
requests/limits的 Pod 入库,杜绝 LimitRange 兜底带来的不透明。 - 配额监控告警:用 Prometheus 采集
kube_resourcequota系列指标,对used/hard超 80% 的命名空间提前告警,把"发布失败"前移成"容量预警"。 - 发布前 dry-run:CI 里用
kubectl apply --dry-run=server配合配额校验,扩容类变更先评估 request 增量是否越线。
七、总结
这次故障的教训是:Kubernetes 的资源约束是多层叠加的——ResourceQuota(命名空间预算)、LimitRange(默认值注入)、节点可分配资源(真实算力)各管一段,而调度失败往往出在最上游那道"配额门"上。节点有空闲只是必要条件,命名空间配额见顶照样让 Pod 永远 Pending。
排查的核心动作就是三板斧:kubectl describe quota 看 used vs hard 是否顶满 → kubectl get pods 累加 requests 找"吃配额的大户" → kubectl get limitrange 确认是否有默认请求被隐性注入。把配额当预算管、加监控、拆隔离,就能让这类"发布到一半卡住"的事故从偶发变成可预警。