Pod 一直 Pending:从调度失败到资源碎片的排查路径
## 一、Pending 不是一个问题,是六种问题
`kubectl get pod` 看到 `Pending`,很多人第一反应是「节点不够了,加机器」。但在实际排查中,Pending 至少有六个互不相关的原因,加机器只能解决其中两个:
1. **资源不够**:所有节点的可分配资源都小于 Pod 的 requests
2. **资源碎片**:总量够,但没有单个节点塞得下(最容易被误判成「资源不够」)
3. **亲和性冲突**:nodeSelector / nodeAffinity / podAntiAffinity 约束无解
4. **污点未容忍**:节点带 taint,Pod 没配对应的 toleration
5. **存储未就绪**:PVC 处于 Pending,Pod 卡在 VolumeBinding
6. **镜像或配额**:ResourceQuota 拦截,或镜像拉不下来(后者其实是 ImagePullBackOff,但初次创建时也会短暂 Pending)
判断顺序很关键。有一句话能省掉大半时间:**Pending 的 Pod 一定有调度事件,而且事件里写明了失败原因,只是默认没显示出来**。
## 二、第一步永远是 describe,但要看 Events 的最后几行
```
kubectl describe pod <pod> -n <ns>
```
重点看两处:
**Conditions** 里的 `PodScheduled`:
```
Conditions:
Type Status
PodScheduled False
```
`PodScheduled=False` 说明问题在调度阶段,后面五类原因都可能。如果 `PodScheduled=True` 而 Pod 仍然 Pending,那问题在拉镜像或挂载卷,不在调度。
**Events** 的最后几行,会直接给出原因,常见几种:
```
0/12 nodes are available: 12 Insufficient cpu.
0/12 nodes are available: 12 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn't tolerate.
0/12 nodes are available: 3 node(s) didn't match Pod's node affinity/selector, 9 Insufficient memory.
0/12 nodes are available: 12 node(s) didn't find available persistent volumes to bind.
0/12 nodes are available: 12 node(s) exceed max volume count.
```
这行文字已经把原因说清楚了。如果 Events 是空的,说明调度器还没处理到这个 Pod,等 10 秒再看,或者检查 `kube-scheduler` 是否健康。
再看调度器自己记录的原因:
```
kubectl get events -n <ns> --field-selector involvedObject.name=<pod> --sort-by=.lastTimestamp
```
`--sort-by` 很重要,因为 `describe` 只显示最后几条,早期的事件可能被挤掉。
## 三、资源不够 vs 资源碎片:最容易误判的一对
这两者的表现完全一样(`Insufficient cpu` / `Insufficient memory`),但处理方式相反。区分方法是对比两个数字。
**看节点可分配量:**
```
kubectl describe node <node> | grep -A 8 "Allocated resources"
```
输出:
```
Allocated resources:
Resource Requests Limits
-------- -------- ------
cpu 7800m (97%) 12000m
memory 28Gi (87%) 40Gi
```
**再看 Pod 自己的 requests:**
```
kubectl get pod <pod> -o jsonpath='{.spec.containers[*].resources.requests}'
```
如果节点显示 CPU 97%、剩 240m,而 Pod 要 500m,那就是**碎片**——节点有剩余,但不够这一个 Pod。如果所有节点剩余都小于 500m,才是真的**总量不够**。
处理方式完全不同:
- **碎片**:调整 Pod 的 requests(如果实际用量远小于 requests,说明申请值虚高)、打散已有负载、或者用 `descheduler` 做重平衡。加机器也能解决,但成本最高。
- **总量不够**:扩容节点,或者降低整体 requests。
**一个高频问题:requests 虚高。** 很多团队建 Pod 时直接抄 `requests: cpu 2`,实际用量 200m。结果集群 CPU 利用率只有 20%,却整天报 `Insufficient cpu`。查实际用量:
```
kubectl top pod -n <ns> --containers
```
`requests` 应该贴近 P95 实际用量,而不是「怕不够就往上写」。这也是为什么生产上应该配 VPA(垂直自动伸缩)或至少定期 review。
## 四、污点与容忍:节点明明空着却调度不上去
现象:节点 CPU 5%、内存 10%,Pod 就是上不去。
```
kubectl describe node <node> | grep -i taint
```
输出:
```
Taints: node-role.kubernetes.io/control-plane:NoSchedule
```
节点上打了污点,Pod 没有对应的 toleration。三种处理:
1. 给 Pod 加 toleration(适合确实要跑在专用节点上的场景)
2. 去掉节点污点(仅适合自建集群,托管集群的 master 污点不要动)
3. 用 nodeSelector 避开这些节点(治标)
新集群最容易踩的一类:**云厂商的托管节点池默认带污点**,比如 GPU 节点带 `nvidia.com/gpu=present:NoSchedule`,普通 Pod 上不去,而 GPU 业务 Pod 因为忘了加 toleration 也上不去,两边都卡着。
## 五、亲和性:约束写错时最难查
三种约束的作用域不同,容易混:
| 约束 | 作用对象 | 典型误用 |
|---|---|---|
| `nodeSelector` | 节点标签,精确匹配 | 标签键写错,永远不匹配 |
| `nodeAffinity` | 节点标签,支持 In/NotIn/Exists | `requiredDuringScheduling` 写太死 |
| `podAffinity` / `podAntiAffinity` | 其他 Pod 的拓扑分布 | `topologyKey` 写错导致无解 |
排查手段是直接查节点标签是否真的存在:
```
kubectl get nodes --show-labels | grep <你的标签键>
```
很多人以为 `kubernetes.io/hostname` 之类的标签一定有,或者以为云厂商的 `node.kubernetes.io/instance-type` 是 `ecs.c6.xlarge` 而实际是别的值。**标签值必须逐字核对**。
另一个高频坑是 `podAntiAffinity` 配 `requiredDuringSchedulingIgnoredDuringExecution` + `topologyKey: kubernetes.io/hostname`,同时副本数大于节点数。这种配置在节点数不足时必然有 Pod Pending,而且事件里只会说 `node(s) didn't match pod anti-affinity rules`,不会告诉你是「副本数超过了节点数」。这种情况下应该改用 `preferredDuringScheduling`,或者先把副本数压到节点数以内。
## 六、PVC 未绑定:Pod 卡在 VolumeBinding
事件里出现这句就该查存储:
```
pod has unbound immediate PersistentVolumeClaims
```
或者:
```
node(s) didn't find available persistent volumes to bind
```
三个方向:
**1. StorageClass 是否存在、是否可动态供给**
```
kubectl get sc
kubectl get pvc -n <ns>
```
PVC 的 `STATUS` 如果是 `Pending`,说明没有 PV 能匹配。看它的 Events:
```
kubectl describe pvc <pvc> -n <ns>
```
**2. `accessModes` 不匹配**
要 `ReadWriteMany` 但 StorageClass 只支持 `ReadWriteOnce`,就会永远 Pending。云厂商的块存储通常只支持 RWO,要 RWX 得用文件存储(NFS/CephFS 类)。
**3. 节点可用区约束**
云上 PV 是绑定可用区的。如果 Pod 被 nodeAffinity 限定在 A 区,而 PV 在 B 区,就会出现「PV 有、节点有,但绑不上」。事件里会写 `node(s) had volume node affinity conflict`,这一句很容易被忽略掉,因为它看起来像节点亲和性问题,实际是存储的拓扑约束。
## 七、批量排查:一次看清全部 Pending
单个 Pod 看 describe 还行,一次几十个就得用聚合手段。
**按原因分组:**
```bash
kubectl get pods -A --field-selector=status.phase=Pending -o wide
```
**直接读调度器事件,按消息归类:**
```bash
kubectl get events -A \
--field-selector reason=FailedScheduling \
--sort-by=.lastTimestamp \
-o jsonpath='{range .items[*]}{.message}{"\n"}{end}' \
| sed 's/[0-9]\+ node(s)/N node(s)/' \
| sort | uniq -c | sort -rn
```
这条命令的价值在于:把「12 nodes are available: 12 Insufficient cpu」这类消息归一化后统计,一眼就能看出是资源问题占多数,还是亲和性问题占多数。比逐个 describe 快一个量级。
**看整体资源缺口:**
```bash
kubectl describe nodes | grep -E "^Name:|cpu |memory " | head -40
```
或者用 `kubectl-top` 之类的工具看集群维度的 requests 总和与 allocatable 的比值。这个比值超过 85% 时,碎片导致的 Pending 会开始频繁出现。
## 八、requests 与 limits 设错的连锁反应
调度只看 `requests`,不看 `limits`,也不看实际用量。这个设计导致一个常见矛盾:
- `requests` 设小 → 调度容易通过 → 但运行时被限流(CPU throttling)或 OOMKilled
- `requests` 设大 → 运行稳定 → 但集群「看起来很满」,实际利用率很低
正确的做法是分开设:CPU 的 requests 按 P95 用量(可压缩资源,超了只是变慢),memory 的 requests 按接近 limits(不可压缩资源,超了就被杀)。
```
resources:
requests:
cpu: 200m
memory: 1Gi
limits:
cpu: 1000m
memory: 1Gi
```
CPU 的 limit 给到 requests 的 3~5 倍是合理的(应对突发),memory 的 limit 等于 requests 最稳(保证不被 OOM 杀)。
这样配置的好处很直接:调度按 200m 算,一台 4 核节点能放 20 个 Pod;运行时它们可以各自突发到 1 核。集群利用率上去了,Pending 也少了。
## 九、一份可直接抄的排查清单
```bash
# 1. 是不是调度阶段
kubectl describe pod <pod> -n <ns> | grep -A 3 "Conditions:"
# 2. 直接看失败原因
kubectl get events -n <ns> --field-selector involvedObject.name=<pod> --sort-by=.lastTimestamp
# 3. 节点可分配余量
kubectl describe node <node> | grep -A 8 "Allocated resources"
# 4. Pod 要多少
kubectl get pod <pod> -o jsonpath='{.spec.containers[*].resources.requests}'
# 5. 节点污点
kubectl describe node <node> | grep -i taint
# 6. 节点标签(核对亲和性)
kubectl get nodes --show-labels
# 7. PVC 状态
kubectl get pvc -n <ns>
# 8. 全部 Pending 按原因聚合
kubectl get events -A --field-selector reason=FailedScheduling \
-o jsonpath='{range .items[*]}{.message}{"\n"}{end}' | sort | uniq -c | sort -rn
# 9. 实际用量(判断 requests 是否虚高)
kubectl top pod -n <ns> --containers
```
第 2 步和第 8 步是最省时间的两个:一个看单个,一个看全局。
## 十、决策表
| 事件消息 | 判断 | 处理 |
|---|---|---|
| `Insufficient cpu/memory`,节点余量 < 需求 | 碎片 | 降 requests、重平衡,或扩节点 |
| `Insufficient cpu/memory`,所有节点余量都很小 | 总量不够 | 扩容 |
| `had taint ... that the pod didn't tolerate` | 污点 | 加 toleration 或去污点 |
| `didn't match Pod's node affinity/selector` | 标签不匹配 | 逐字核对节点标签 |
| `didn't match pod anti-affinity rules` | 反亲和无解 | 改 preferred,或减副本 |
| `unbound immediate PersistentVolumeClaims` | PVC 未绑定 | 查 SC / accessModes |
| `didn't find available persistent volumes` | 无可用 PV | 查 SC 是否动态供给 |
| `had volume node affinity conflict` | PV 与节点跨可用区 | 让 PV 与节点同区 |
| `exceed max volume count` | 节点挂盘数超限 | 换节点或减少 PVC |
| `PodScheduled=True` 但仍 Pending | 不在调度阶段 | 查镜像拉取与卷挂载 |
| 长期 pending 且事件无变化 | 调度器异常 | 查 `kube-scheduler` 日志与健康 |
## 收尾
Pending 的排查有个很朴素的判断顺序:**先确认是不是调度问题(看 PodScheduled),再看事件里的那句话,然后对照上面那张表**。
真正花时间的不是定位,而是定位之后的取舍:requests 该不该降、反亲和该不该改、是加节点还是加密度。这些决定没有标准答案,但只要 requests 贴近实际用量、CPU 与 memory 分开策略,绝大多数 Pending 根本不会出现。
一句话总结:**Pending 很少是「容量不够」,多半是「账算错了」。**
- 点赞
- 收藏
- 关注作者
评论(0)