一次 Kubernetes 命名空间 ResourceQuota 配额耗尽导致 Pod 卡在 Pending 的排查记录

K8s 命名空间资源配额被副本扩容与 LimitRange 隐性注入吃满,新 Pod 卡在 Pending 无法调度。

一、问题背景

平台组在多租户的 Kubernetes 集群里给每个业务命名空间(Namespace)统一下发了 ResourceQuota,把 CPU、内存、Pod 数量等资源的"预算上限"钉死,用来做资源隔离和成本核算——谁超了谁自己负责。这套机制上线大半年一直相安无事,直到某天傍晚一次常规发布,线上突然冒出一批 Pending 状态的 Pod,发布流水线卡在"等待调度"迟迟不结束。

当时这个命名空间跑着一个在线推理服务(白天 2 副本,晚高峰弹性扩到 6 副本)外加几个后台批处理 Job。我们第一反应是节点资源不够,但监控里节点 CPU/内存使用率明明还有富余。问题显然不在"机器没油",而在"配额被卡了脖子"。

二、故障现象

故障最直观的表现是:新版本的 Deployment 滚动发布时,只有旧副本被逐批删掉,新副本却始终 Pendingkubectl get pods -n <ns> 看到状态长时间停在 ContainerCreating 之前、直接就是 Pending

describe 这些 Pending Pod,Events 区反复刷出一条:

1
2
3
4
5
6
7
Warning  FailedScheduling  0/20 nodes are available:
  20 node(s) had untolerated taint ... (无关)
  pod "infer-svc-7d9c-" is forbidden:
  exceeded quota: compute-resources,
  requested: cpu=500m, memory=512Mi,
  used: cpu=7800m, memory=14Gi,
  limited: cpu=8, memory=16Gi

也就是说:调度器根本没走到"找节点"那一步,就在准入阶段被 ResourceQuota 的准入控制器(QuotaAdmission)直接拦下了——used 已经摸到 limited 的天花板,requested 再多一点点就超。

更迷惑的是,同一时间还有两三个资源请求极小(yaml 里没写 resources.requests)的辅助 Pod 也卡在 Pending,看 describe 报的 requested: cpu=100m 这种微不足道的量,却同样 forbidden: exceeded quota。节点明明有空闲,配额数字也对不上"肉眼可见的用量",现场一度让人怀疑是集群 bug。

三、排查过程

第一步,确认是不是真超配额。 直接看配额实况:

1
2
kubectl get resourcequota -n <ns> -o yaml
kubectl describe quota compute-resources -n <ns>

输出里 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 累加:

1
2
3
kubectl get pods -n <ns> -o custom-columns=\
POD:.metadata.name,CPU_REQ:.spec.containers[0].resources.requests.cpu,\
MEM_REQ:.spec.containers[0].resources.requests.memory

一眼看清:推理服务晚高峰扩到 6 副本,每副本 cpu: 1.5 / mem: 2Gi,单这一个服务就吃掉 9 核里的 7.8 核——正是把 CPU 配额顶满的元凶。副本数一涨,request 总量线性叠加,瞬间撞线。

第三步,破解"小 Pod 也 Pending"的反常。 那几个没写 resources.requests 的辅助容器,为什么也要 100m?查该 ns 的 LimitRange

1
kubectl get limitrange -n <ns> -o yaml

里面有一条 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 的计量口径理解错误 + 默认注入的隐性放大

  1. ResourceQuota 同时约束 requestslimits 的总量,而不仅仅限制 limit;
  2. LimitRange 对未声明资源的容器会注入 defaultRequest/defaultLimit,这些值计入 ResourceQuota 统计,等于"看不见的请求"也在吃预算;
  3. 多团队共用一个 namespace 的单一配额,缺乏隔离,任一服务扩容都会挤占他人额度;
  4. 完全缺失 quota 使用率监控,配额在"温水煮青蛙"中耗尽,直到发布失败才暴露。

一句话:调度失败的根因不在节点,而在命名空间级准入控制把资源预算卡死,且这个预算被"副本扩容 + 默认注入"两股力量悄悄吃满。

六、预防措施

  • 配额隔离:按团队、按环境拆分命名空间,每个 ns 独立 ResourceQuota,互不挤占;重要业务单独配额。
  • LimitRange 收敛:把 defaultRequest/defaultLimit 设为贴近真实的小值,别让"默认值"变成隐性大户;同时用 defaultmax/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 确认是否有默认请求被隐性注入。把配额当预算管、加监控、拆隔离,就能让这类"发布到一半卡住"的事故从偶发变成可预警。

使用 Hugo 构建
主题 StackJimmy 设计