一、问题背景
Pod 是 Kubernetes 中最小的调度单元。当你执行 kubectl apply -f pod.yaml 之后,Pod 会经历以下几个主要阶段:
Pending → Running → Succeeded / Failed
其中 Pending 是一个过渡状态,表示 Pod 已经被 API Server 接受,但还没有在任何一个节点上成功运行起来。正常情况下一两秒就能过去,但如果卡在 Pending 超过几十秒甚至几分钟,那就说明调度或者容器创建环节出了阻塞。
理解这个状态的关键在于搞清楚 Kubernetes 的调度链路:
用户创建 Pod → API Server 写入 etcd
Scheduler 监听到未绑定的 Pod → 执行调度算法,过滤出一组合适的节点,然后打分选出最优节点
Scheduler 把选中的节点名写回 Pod 的 spec.nodeName 字段
目标节点上的 Kubelet 监听到分配给自己的 Pod → 拉镜像、创建容器、启动
Pod 卡在 Pending,问题一定出在第 2 步或者第 4 步之前——要么 Scheduler 找不到合适的节点,要么 Scheduler 完成绑定后 Kubelet 在拉镜像/挂载存储阶段卡住。
这篇文章聚焦在 7 个最常见的排查方向,每个方向都提供可执行的命令、可观察的指标、可操作的修复步骤。读完你可以把它当作故障排查速查手册来用。
二、适用场景
以下场景最容易遇到 Pod Pending 问题:
新业务首次部署到 K8s 集群,对集群资源容量不熟悉
应用做了大版本升级,镜像体积或者资源配置发生了显著变化
节点被下线维护后,大量 Pod 重调度导致资源紧张
集群中引入了污点、亲和性等调度策略后,老旧 Pod 配置没有跟着更新
集群管理员调整了 ResourceQuota 或 LimitRange,部分 Namespace 配额耗尽
使用 StatefulSet 时 PVC 配置了错误的 StorageClass 或访问模式
三、排查前的准备:核心命令速查
在开始排查之前,先掌握这几条命令的用法,它们是你定位问题的第一组工具:
3.1 查看 Pod 详细状态
kubectl describe pod-n
这是排障时最有用的命令。直接拉到输出的最底部,看 Events 段落。Scheduler 产生的所有调度失败原因都会以 Warning 事件的形式记录在这里。举个例子,如果节点资源不足,你会看到类似:
Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 2m default-scheduler 0/5 nodes are available: 3 node(s) didn't match node selector, 2 Insufficient memory.
这个 Events 输出是你排查 Pending 问题的第一线索。
3.2 查看集群事件
kubectl get events -n--sort-by='.lastTimestamp'
如果你不确定是哪个 Pod 出了问题,或者想按时间线梳理整个 Namespace 的事件,这条命令比 describe pod 更全局。
3.3 查看节点资源使用
kubectl top nodes kubectl top pods -n
这两条命令依赖 Metrics Server 组件。如果你的集群没有装 Metrics Server(很多私有部署集群默认不带),这两条命令会报 error: Metrics API not available。这种情况下需要先部署 Metrics Server:
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
注意:Metrics Server 默认需要 kubelet 证书验证,如果你用的是自签证书的集群,需要在 Metrics Server Deployment 里加上 --kubelet-insecure-tls 参数。
3.4 查看节点详细信息
kubectl get nodes -o wide kubectl describe node
describe node 输出里的 Taints、Conditions、Allocated resources 这三段是排查调度的关键信息。
3.5 查看 Pod YAML
kubectl get pod-n -o yaml
在 Pod 卡 Pending 时,重点看以下字段:
spec.nodeName:如果为空,说明 Scheduler 还没完成调度
spec.nodeSelector:看 Pod 对节点标签的要求
spec.affinity:看节点亲和性和 Pod 反亲和性配置
spec.tolerations:看 Pod 对污点的容忍配置
spec.containers[].resources:看 Pod 的 requests 和 limits
status.conditions:看 Pod 当前各阶段的状态
四、七个排查方向
方向一:节点资源不足
现象
kubectl get pod 看到 Pod 状态是 Pending,kubectl describe pod 的 Events 中明确出现 Insufficient cpu、Insufficient memory、Insufficient ephemeral-storage 等字样。
原理
Scheduler 的调度算法有一个「预选」阶段(Filtering)。在这个阶段,Scheduler 会逐个检查每个节点是否满足 Pod 的资源需求。具体来说,Scheduler 会计算每个节点上已经分配给 Pod 的 requests 总量,然后判断该节点的剩余可分配资源是否大于等于新 Pod 的 requests。
这里有一个容易踩的坑:Scheduler 看的是 requests 值,不是 limits。即便你的节点上实际使用率只有 10%,但只要 requests 的总和满了,Scheduler 就不会把新 Pod 调度上去。这是很多人第一次排查时的困惑点——看监控面板节点负载很低,但 Pod 就是调度不上去,就是因为 requests 设置的太高。
检查步骤
第一步:确认线索
kubectl describe pod-n | tail -20
看到类似输出:
Warning FailedScheduling 3m default-scheduler 0/5 nodes are available:
2 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn't tolerate,
3 Insufficient memory.
翻译:5 个节点可用,2 个 master 节点有污点不能调度普通 Pod,剩 3 个 worker 节点,但内存 requests 不够。
第二步:检查 Pod 的 requests 设置
kubectl get pod-n -o jsonpath='{.spec.containers[*].resources.requests}'
如果输出为空或者某个值设得很大(比如 memory: 16Gi 但节点总共就 8G 内存),问题就清楚了。
如果 Pod 没有显式声明 requests,但 Namespace 下配置了 LimitRange 设置了默认 requests,则以 LimitRange 的值为准。检查方式:
kubectl get limitrange -nkubectl describe limitrange -n
第三步:检查各节点当前资源分配情况
kubectl describe nodes | grep -A 5 "Allocated resources"
输出类似:
Allocated resources: (Total limits may be over 100 percent, i.e., overcommitted.) Resource Requests Limits -------- -------- ------ cpu 3500m (87%) 6000m (150%) memory 28Gi (93%) 32Gi (106%)
这里 Memory Requests 占比 93%,如果新 Pod 的 memory requests 超过剩余的 7%(约 2Gi),那就调度不上去了。
也可以用下面更直观的方式一个一个节点看:
kubectl describe node| grep -E "Name:| cpu | memory "
第四步:检查节点实际使用(和 requests 对比)
kubectl top nodes
如果实际使用率远低于 requests(比如实际 30%,requests 占了 90%),说明 Pod 的 requests 设置过大,资源被「预占」但并未真正使用——这是运维常说的「资源超卖不足」问题,解决方向是调低 requests 或者利用 VPA(Vertical Pod Autoscaler)做自动调整。
修复方案
按优先级从高到低排列:
方案 A:调低 Pod 的 requests 值(最快、无风险)
编辑 Deployment/StatefulSet,把 resources.requests 降到合理水平。需要根据应用的实际情况评估——用 Prometheus 拉一段时间内的实际内存 CPU 用量,结合监控数据做决策。
resources: requests: cpu: "200m" memory: "256Mi" limits: cpu: "500m" memory: "512Mi"
这个修改会触发 Pod 滚动重建,建议在低峰期操作。
方案 B:扩容节点(稳妥、有成本)
如果是云环境,调整节点池的节点数量或者规格:
# GKE gcloud container clusters resize--node-pool --num-nodes 5 # EKS eksctl scale nodegroup --cluster= --name= --nodes=5 # AKS az aks scale --resource-group --name --node-count 5
如果是自建集群,加一台机器并注册到集群:
# 新机器上安装 kubelet 和 kubeadm,然后 kubeadm join:6443 --token --discovery-token-ca-cert-hash sha256:
方案 C:驱逐不需要的低优先级 Pod
通过以下命令找出低优先级 Pod(未设置 PriorityClass 的 Pod 默认 priority 为 0):
kubectl get pods --all-namespaces --sort-by='.spec.priority'
和业务方确认后驱逐或缩容。
# 危险操作:驱逐 Pod kubectl drain--ignore-daemonsets --delete-emptydir-data
这条命令风险较高,会驱逐节点上所有 Pod。执行前确认目标节点上还有哪些 Pod,确认是否有单点故障:
kubectl get pods --all-namespaces -o wide --field-selector spec.nodeName=
验证
修复后观察 Pod 状态变化:
kubectl get pod-n -w
看到 STATUS 从 Pending 变成 ContainerCreating 再变成 Running,说明问题解决。
kubectl describe pod-n | grep -A 5 "Events"
Events 中不再出现 FailedScheduling,而是出现 Scheduled 和 Pulling、Started 事件,确认调度成功。
方向二:污点与容忍(Taints / Tolerations)
现象
kubectl describe pod 的 Events 中看到:
0/5 nodes are available: 1 node(s) had taint {key: value}, that the pod didn't tolerate, 4 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn't tolerate.
或者 kubectl get pod 看到 Pod 一直是 Pending,但 Events 里说节点都有污点。
原理
Taint 是节点对 Pod 的排斥机制,Toleration 是 Pod 对 Taint 的豁免声明。简单想就是:节点说「我不接待普通人」,那只有持有「豁免证」(Toleration)的 Pod 才能调度上来。
常见污点场景:
Master 节点默认污点:node-role.kubernetes.io/master:NoSchedule,防止普通应用 Pod 调度到主控节点
专用节点污点:比如 GPU 节点打上 gpu=true:NoSchedule,只有明确需要 GPU 的 Pod 才能调度
节点异常污点:Kubelet 检测到节点磁盘满、内存不足等情况,会自动给节点打上污点,防止新 Pod 调度过来
自定义隔离污点:运维人员手动给节点打污点进行维护,比如 node.kubernetes.io/unschedulable:NoSchedule
检查步骤
第一步:看节点上有什么污点
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{" "}{.spec.taints}{"
"}{end}'
或者更可读的方式:
kubectl describe node| grep -A 5 "Taints"
第二步:看 Pod 有没有对应的 Toleration
kubectl get pod-n -o jsonpath='{.spec.tolerations}' | python3 -m json.tool
对比节点污点和 Pod 的 Toleration,看看是不是不匹配。
第三步:检查节点 Conditions
节点因异常自动添加的污点通常会伴随对应的 Condition。查看方式:
kubectl describe node| grep -A 3 "Conditions:"
重点关注以下几种:
MemoryPressure → node.kubernetes.io/memory-pressure:NoSchedule
DiskPressure → node.kubernetes.io/disk-pressure:NoSchedule
PIDPressure → node.kubernetes.io/pid-pressure:NoSchedule
NetworkUnavailable → node.kubernetes.io/network-unavailable:NoSchedule
这些是 Kubelet 检测到节点异常后自动添加的污点,对应的 Condition 为 True 时说明节点确实有问题。
修复方案
场景一:Pod 需要调度到有特殊污点的节点
在 Pod 或 Deployment 中添加对应的 Toleration:
spec: tolerations: - key: "node-role.kubernetes.io/master" operator: "Exists" effect: "NoSchedule"
或者在 Deployment 中:
spec: template: spec: tolerations: - key: "gpu" operator: "Equal" value: "true" effect: "NoSchedule"
场景二:节点因异常产生污点,需要修复节点
首先登录到节点上排查具体问题:
# 检查磁盘 df -h # 检查内存 free -h # 检查 PID 数量 cat /proc/sys/kernel/pid_max ps aux | wc -l
修复节点问题后,Condition 通常会自动恢复,对应的污点也会自动移除。如果污点没有自动移除,可以手动去掉:
kubectl taint nodesnode.kubernetes.io/disk-pressure:NoSchedule-
注意末尾有个 - 号,表示移除污点。
场景三:节点维护后恢复调度
如果运维手动给节点打过 unschedulable 污点,维护完成后需要手动移除:
# 先看现在有哪些污点 kubectl describe node| grep Taints # 移除维护污点(如果有的话) kubectl taint nodes dedicated=maintenance:NoSchedule- # 如果用了 cordon kubectl uncordon
cordon 的本质也是给节点加上 node.kubernetes.io/unschedulable:NoSchedule 污点。
验证
kubectl describe pod-n | grep -A 5 "Events"
确认 Events 中出现了 Scheduled 事件,Pod 成功调度到目标节点。
kubectl get pod-n -o wide
确认 NODE 字段填上了预期的节点名。
风险提醒
给节点打或移污点会立即影响调度决策,可能触发已调度 Pod 被驱逐(如果 effect 是 NoExecute)
NoExecute 类型的污点不仅阻止新 Pod 调度,还会驱逐已经在该节点上运行的、不匹配 Toleration 的 Pod
在维护之前,务必先 kubectl cordon 阻止新 Pod 调度,再 kubectl drain 驱逐已有 Pod
不要在流量高峰期操作
方向三:节点选择器与亲和性(NodeSelector / Affinity)
现象
Events 中看到:
0/5 nodes are available: 5 node(s) didn't match node selector.
或者:
0/5 nodes are available: 5 node(s) didn't match pod affinity/anti-affinity rules.
也可能更细致:
3 node(s) didn't match node selector, 2 node(s) didn't match pod anti-affinity rules.
原理
NodeSelector 是最简单的节点筛选机制:Pod 声明自己需要节点上有某些标签,Scheduler 只在带对应标签的节点上调度。
NodeAffinity 是 NodeSelector 的升级版,支持更丰富的匹配规则:
requiredDuringSchedulingIgnoredDuringExecution:硬性要求,满足才调度,调度后标签变了不驱逐
preferredDuringSchedulingIgnoredDuringExecution:软性偏好,优先满足但不强制
PodAffinity 和 PodAntiAffinity 控制 Pod 之间的拓扑关系:
PodAffinity:让 Pod 集中部署(和带某标签的 Pod 放在一起)
PodAntiAffinity:让 Pod 分散部署(不和带某标签的 Pod 放在一起)
检查步骤
第一步:看 Pod 的 nodeSelector 和 affinity 配置
kubectl get pod-n -o yaml | grep -A 10 "nodeSelector|affinity"
记录下 Pod 要求了哪些标签。
第二步:看集群节点上有哪些标签
kubectl get nodes --show-labels
或者只看具体节点的标签:
kubectl describe node| grep -A 2 "Labels:"
第三步:交叉比对
把 Pod 要求的标签和节点实际的标签做对比。比如 Pod 要求 nodeSelector: disktype: ssd,但集群里所有节点的 disktype 标签都是 hdd 或者根本没这个标签,那就是匹配不上。
第四步:Pod 反亲和性排查
如果 Pod 有 PodAntiAffinity 规则,要看拓扑域内是否已经有匹配的 Pod。最常见的情况是 Deployment 配置了 requiredDuringScheduling 级别的反亲和性,而节点数少于副本数,导致后面的副本无处可调度。
比如配置了:
affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app: my-app topologyKey: "kubernetes.io/hostname"
这个规则要求同一个 hostname(也就是同一台机器)上不能有两个带 app: my-app 标签的 Pod。如果你有 3 个副本但只有 2 个节点,第三个 Pod 就会一直 Pending。
修复方案
场景一:NodeSelector 的标签节点上没有
给节点补上标签:
kubectl label nodedisktype=ssd
或者修改 Pod/Deployment,去掉或放宽 nodeSelector 的要求。
场景二:NodeAffinity 条件太严格
检查是不是可以把 requiredDuringScheduling 改成 preferredDuringScheduling:
affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: -weight:1 preference: matchExpressions: -key:disktype operator:In values: -ssd
场景三:PodAntiAffinity 限制了副本分布
考虑以下调整优先级:
增加节点数量
把 requiredDuringScheduling 改成 preferredDuringScheduling
调整 topologyKey,从 kubernetes.io/hostname(每节点一个)改成 topology.kubernetes.io/zone(每可用区一个)
验证
修改后观察 Pod 调度:
kubectl get pod -n-l app= -o wide
检查同一节点上是否真的没有重复 Pod,以及所有 Pod 是否都变成 Running。
方向四:PersistentVolumeClaim 绑定问题
现象
Pod 依赖 PVC 挂载持久卷,但 kubectl get pvc 看到 PVC 状态是 Pending。这种情况下,Pod 的 Events 可能不会直接显示 PVC 相关错误(取决于 Kubernetes 版本和 Pod spec 写法),需要主动去检查 PVC 状态。
强烈建议:每次排查 Pending Pod 都顺手看一下 kubectl get pvc -n
原理
Pod 引用 PVC,PVC 向 StorageClass 请求动态创建 PV,或者从已有 PV 池中绑定一个匹配的 PV。以下环节都可能卡住:
PVC 指定的 StorageClass 不存在
StorageClass 配置的 Provisioner(比如 AWS EBS CSI Driver、NFS Provisioner)没有运行或出了问题
静态 PV 的容量、访问模式、StorageClass 与 PVC 不匹配
PV 池中容量不足,没有满足 PVC 要求的 PV
PVC 的 AccessMode(RWO/ROX/RWX)与可用 PV 不匹配
检查步骤
第一步:检查 PVC 状态
kubectl get pvc -n
如果 STATUS 是 Pending,进一步 describe:
kubectl describe pvc-n
重点看 Events 部分的错误信息。
常见输出:
Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning ProvisioningFailed 2m persistentvolume-controller storageclass.storage.k8s.io "fast-ssd" not found
这说明 PVC 引用的 StorageClass 不存在。
第二步:检查 StorageClass 是否存在
kubectl get storageclass kubectl describe storageclass
如果 kubectl get storageclass 是空的,说明要么没有创建 StorageClass,要么集群没有安装存储插件。
第三步:检查 PV 列表
对于用静态 PV 的场景:
kubectl get pv
看 PV 的 STATUS(Available / Bound / Released)和 CAPACITY 是否满足 PVC 的请求。重点关注:
STATUS 是 Available 的 PV 有多少
容量(CAPACITY)够不够 PVC 的 storage request
AccessModes 是否匹配(RWO 的 PV 不能绑 ROX 的 PVC)
PV 的 storageClassName 和 PVC 的是否一致
第四步:检查存储插件组件状态
如果 StorageClass 的 Provisioner 是 kubernetes.io/aws-ebs、ebs.csi.aws.com 等外部插件,需要检查对应的 Controller Pod 是否在运行:
kubectl get pods -n kube-system | grep -E "ebs|csi|nfs|provisioner"
修复方案
场景一:StorageClass 不存在
创建对应的 StorageClass。例如 AWS EBS gp2 类型:
apiVersion: storage.k8s.io/v1 kind:StorageClass metadata: name:fast-ssd provisioner:kubernetes.io/aws-ebs parameters: type:gp2 fsType:ext4 reclaimPolicy:Delete volumeBindingMode:WaitForFirstConsumer
注意 volumeBindingMode: WaitForFirstConsumer 会让 PV 等到有 Pod 引用 PVC 时才创建,如果设成 Immediate,PV 会在 PVC 创建时立即被 Provisioner 创建。
场景二:静态 PV 规格不匹配
删除不匹配的 PVC(确认没有数据需要保留),用正确的规格重建:
# 先确认 PVC 关联的 Pod 及相关资源 kubectl get pod -n-o wide | grep -E "pod-name|volumes" # 备份当前的 PVC YAML kubectl get pvc -n -o yaml > pvc-backup.yaml # 删除 PVC(风险操作,确认没有关联的 Pod 在使用) kubectl delete pvc -n
然后调整 YAML 中的 spec.resources.requests.storage 或 spec.storageClassName 重新 apply。
场景三:CSI 驱动 Pod 异常
# 查找 CSI 相关 Pod kubectl get pods --all-namespaces | grep csi # 查看异常的 CSI Pod 日志 kubectl logs -n kube-system
根据日志定位具体问题,常见的是云厂商 API 权限问题(IAM 角色/服务账号没有创建卷的权限)、API 限流、配额耗尽等。
验证
kubectl get pvc -n# 预期 STATUS 变成 Bound
然后确认 Pod 状态:
kubectl get pod-n # 预期变成 Running
风险提醒
删除 PVC 可能导致数据丢失,取决于 PV 的 reclaimPolicy
reclaimPolicy: Delete:删除 PVC 时 PV 和底层存储会被删除,数据永久丢失
reclaimPolicy: Retain:删除 PVC 后 PV 变为 Released 状态,底层存储还在但需要手动清理和重新绑定
在删除 PVC 之前,务必确认 reclaimPolicy 和数据备份状态
方向五:镜像拉取问题
现象
严格来说,镜像拉取失败或拉取缓慢时,Pod 的状态是 ContainerCreating 而不是 Pending。但在某些版本的 Kubernetes 中,Scheduler 绑定节点后到 Kubelet 报告容器状态之间有一个短暂的过渡期,Pod 对外显示 Pending 但实际已经开始拉镜像了。
有两种情况值得在 Pending 排查中加入这个方向:
Pod 从 Pending 变成 ContainerCreating 的速度极快(小于 1 秒),你看到的时候刚好是 Pending,但其实是在拉镜像
私有镜像仓库网络不通,导致 Kubelet 拉镜像超时,这种情况下 Pod 可能会在 Pending/ContainerCreating 之间卡住
检查步骤
第一步:确认 Pod 当前阶段
kubectl describe pod-n | grep -A 20 "Conditions:"
看 Type: Ready 的状态和 Reason。
第二步:检查 Events 中的镜像拉取事件
kubectl describe pod-n | grep -A 5 -E "Pull|image|Image"
如果看到:
Failed to pull image ——镜像仓库地址错误、认证失败,或者镜像不存在
ErrImagePull ——通常是镜像标签写错或认证有问题
ImagePullBackOff ——Kubelet 已经重试多次但都失败了,进入了退避期
第三步:检查 imagePullSecrets 配置
如果镜像是私有仓库的,需要确认 Pod 或 ServiceAccount 绑定了正确的 imagePullSecrets:
kubectl get pod-n -o jsonpath='{.spec.imagePullSecrets}'
如果输出为空,而镜像来自私有仓库,那 Kubelet 没有仓库的认证凭据,自然拉不下来。
检查 Secret 是否存在:
kubectl get secret-n
第四步:在节点上手动拉镜像
SSH 登录到 Pod 被调度到的节点,手动用 docker 或 containerd 拉取镜像:
# docker 环境 docker pull# containerd 环境 crictl pull
如果手动也拉不下来,问题就在镜像仓库连通性或者认证上。
# 检查网络连通性 curl -v https:///v2/ # 检查 DNS 解析 nslookup
修复方案
场景一:镜像标签不存在
确认镜像标签是否真的存在于仓库中。如果镜像标签拼写错误(比如 latest 写成 latests、版本号写错),修正后重新 apply。
场景二:私有仓库认证问题
创建 Docker Registry Secret:
kubectl create secret docker-registry regcred --docker-server=--docker-username= --docker-password= --docker-email= -n
然后在 Pod 或 ServiceAccount 中引用这个 Secret。推荐绑定到 ServiceAccount 上,这样该 SA 下的所有 Pod 自动获得镜像拉取权限:
kubectl patch serviceaccount default -n-p '{"imagePullSecrets": [{"name": "regcred"}]}'
场景三:镜像仓库网络不通
检查集群节点到镜像仓库之间的网络:
防火墙规则是否放行了相应端口(通常是 443)
是否因为集群没有公网 IP 导致无法访问公网仓库
如果是自建 Harbor 等私有仓库,检查仓库服务是否正常
临时可以把镜像从可直接拉取的地方事先 docker pull 再 docker save / docker load 导入到集群节点上,但这个是应急方案,不是长期解决方向。
验证
kubectl describe pod-n | grep -A 3 "Events"
看到 Pulling → Pulled → Created → Started 事件链,说明镜像拉取成功。
方向六:ResourceQuota / LimitRange 限制
现象
Events 中出现:
Error creating: pods "" is forbidden: exceeded quota: compute-resources, requested: cpu=500m,memory=2Gi, used: cpu=7800m,memory=30Gi, limited: cpu=8,memory=32Gi
或者 Pod 创建失败了而不是 Pending(这种情况下 kubectl get pod 可能根本看不到这个 Pod,但 ReplicaSet / Deployment 的 Events 里会有记录)。
关键区别:ResourceQuota 是在准入阶段拦截的,Pod 根本不会进入调度队列。所以严格来说这不属于 Pending 排查,但在实际工作中,用户看到 Deployment 创建后 Pod 一直没起来,第一反应也是「Pod Pending」,因此有必要列入排查方向。
原理
ResourceQuota 是 Namespace 级别的资源总量限制,包括:
计算资源配额:CPU requests/limits、Memory requests/limits 的总和
存储资源配额:PVC 总容量、PVC 总数量
对象数量配额:Service、ConfigMap、Secret、Deployment 等对象的数量
LimitRange 是 Namespace 级别的单个 Pod/容器资源限制:
设置容器/Pod 的默认 requests 和 limits(没有显式声明时生效)
设置容器/Pod 的 requests 上限和下限
如果容器声明的 requests 超过了 LimitRange 的 max,也会被拒绝
检查步骤
第一步:看 ReplicaSet 的 Events
如果 Pod 因为 Quota 限制没有被创建出来,kubectl get pod 看不到,但 kubectl get rs 能看到 ReplicaSet 的期望副本数和可用副本数不一致。用 describe 看 RS 的 Events:
kubectl describe rs-n | tail -20
第二步:检查 Namespace 的 ResourceQuota
kubectl get resourcequota -nkubectl describe resourcequota -n
输出示例:
Name: compute-resources Namespace: production Resource Used Hard -------- ---- ---- limits.cpu 6 8 limits.memory 28Gi 32Gi requests.cpu 3500m 4 requests.memory 7Gi 16Gi pods 12 20
这里 requests.cpu 的 Used = 3500m,Hard = 4000m,只剩 500m 可用。新 Pod 如果请求超过 500m 就会被拦截。
第三步:检查 LimitRange
kubectl get limitrange -nkubectl describe limitrange -n
如果 LimitRange 设了 defaultRequest.cpu: 500m,而你的 Pod 没有显式声明 requests,那 Pod 会被自动设置为 500m。如果 Namespace 的 CPU 配额只剩不到 500m,Pod 就会因为超出配额被拒。
修复方案
方案 A:调整 ResourceQuota 限制
kubectl edit resourcequota-n
或者用 patch:
kubectl patch resourcequota-n --patch '{"spec":{"hard":{"requests.cpu":"6","requests.memory":"24Gi"}}}'
调大限制之前,确认集群总体资源是否足够。如果你把 CPU 配额从 4 核调成 8 核,但集群里所有节点的 CPU 加起来就只有 6 核,Scheduler 还是会因为资源不足而调不上去——配额只是准入控制,不管实际调度。
方案 B:清理不用的资源,释放配额
# 找出占用资源但已经不需要的 Pod/Deployment kubectl get pods -n--sort-by='.status.startTime' # 找出已完成的 Job kubectl get jobs -n # 删除已完成的 Pod 和 Job kubectl delete jobs -n --field-selector status.successful=1
已完成的 Job Pod 默认会保留(K8s 1.23+ 可以用 TTL Controller 自动清理),这些 Pod 虽然已经 terminated 但仍然占用资源配额。
方案 C:调整 Pod 的 requests,减少单 Pod 占用
和方向一里的做法一样——根据实际使用量下调 requests。
验证
调整后重建 Pod(如果是 Deployment 管理的,可以用 kubectl rollout restart deployment 触发重建):
kubectl rollout restart deployment-n kubectl rollout status deployment -n
检查 ResourceQuota 使用量和 Pod 状态:
kubectl describe resourcequota-n | tail -10 kubectl get pods -n | grep
方向七:PriorityClass 抢占与拓扑约束
现象
Pod 一直 Pending,Events 中没有资源不足、没有污点、没有节点选择器不匹配……一切看起来都正常。这时需要检查两个比较冷门的方向:调度优先级和拓扑分布约束。
原理
PriorityClass:Kubernetes 支持给 Pod 设置优先级。如果节点资源不够,Scheduler 会选择抢占(Preempt)低优先级的 Pod,把它们驱逐出去给高优先级的 Pod 腾地方。如果高优先级 Pod 调度上来需要抢占,但被抢占的 Pod 虽然有更低优先级但却满足不了安全驱逐条件(比如有 PDB 限制了最小可用实例数),那么新 Pod 就会一直 Pending。
TopologySpreadConstraints:这个是 K8s 1.19 之后引入的功能,用来控制 Pod 在不同拓扑域(可用区、节点、机架等)之间的分布均衡度。如果约束过于严格,而集群拓扑结构不支持,就会导致 Pod 无法调度。
一个典型的场景:集群只有 2 个 zone,你设置了 TopologySpreadConstraints 要求每个 zone 上的 Pod 数量差值不超过 1,但你已经有了 3 个 Pod 全部在 zone-A 上(zone-B 节点坏了),这时候新 Pod 要求 zone-B,但 zone-B 没有可用节点,而现有 Pod 的分布已经无法通过调度新 Pod 来平衡——调度器就会放弃。
检查步骤
第一步:检查 Pod 的 PriorityClass
kubectl get pod-n -o jsonpath='{.spec.priorityClassName}'
如果非空,查看该 PriorityClass 的配置:
kubectl get priorityclass-o yaml
重点看 value(数字越大优先级越高)和 globalDefault。
第二步:检查是否发生抢占失败
kubectl get events -n--sort-by='.lastTimestamp' | grep Preempt
如果看到 Preempting 相关事件,说明有抢占行为。看到 FailedPreemption 说明抢占没成功。
第三步:检查 PodDisruptionBudget
kubectl get pdb -nkubectl describe pdb -n
PDB 限制了同时不可用的 Pod 数量。如果 PDB 设了 minAvailable: 1 而被抢占的目标应用只有 1 个副本,那 Scheduler 不能抢占它,高优先级 Pod 会一直 Pending。
第四步:检查 TopologySpreadConstraints
kubectl get pod-n -o jsonpath='{.spec.topologySpreadConstraints}' | python3 -m json.tool
重点看:
maxSkew:允许的最大偏差值
topologyKey:拓扑域的划分依据
whenUnsatisfiable:值为 DoNotSchedule 时,不满足约束就不调度;值为 ScheduleAnyway 时可以在不满足约束时仍调度。注意:从 K8s 1.25 开始,whenUnsatisfiable 被标记为弃用,新增 nodeAffinityPolicy 和 nodeTaintsPolicy 两个字段来分别控制节点亲和性和污点的匹配策略,但 whenUnsatisfiable 在当前版本仍可正常使用。
如果把 whenUnsatisfiable 设成了 DoNotSchedule 但集群拓扑确实满足不了,Pod 就会一直 Pending。
用下面这条命令检查集群各拓扑域的 Pod 分布情况:
kubectl get pods -n-o wide --sort-by='.spec.nodeName'
修复方案
场景一:优先级问题
如果是因为 PDB 限制了抢占,需要评估:
是否可以调整 PDB 的值(比如 maxUnavailable: 1 改成 maxUnavailable: 2)
是否可以先临时增加被抢占应用的副本数,再让 Scheduler 执行抢占
是否可以通过扩容节点来解决资源竞争
不建议直接删除 PDB,PDB 保障的是可用性,删除可能导致更大的故障。
场景二:TopologySpreadConstraints 过于严格
把 whenUnsatisfiable 从 DoNotSchedule 改成 ScheduleAnyway:
topologySpreadConstraints: - maxSkew: 1 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: ScheduleAnyway labelSelector: matchLabels: app: my-app
或者调大 maxSkew:
topologySpreadConstraints: - maxSkew: 3 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: DoNotSchedule
场景三:集群拓扑不满足约束
如果约束要求每个 zone 的 Pod 数量差不大于 1,但集群实际只有 1 个 zone(或者 1 个 zone 的节点全部异常),就需要调整 topologyKey 来适配实际情况,例如从 topology.kubernetes.io/zone 改成 kubernetes.io/hostname。
验证
kubectl get pod-n -o wide
确认 Pod 已调度成功并 Running。同时观察拓扑分布的均衡度:
# 按节点统计 Pod 分布 kubectl get pods -n-l app= -o wide --no-headers | awk '{print $7}' | sort | uniq -c | sort -rn
注意:-o wide 输出中 NODE 列的位置可能因为节点名长度变化而偏移,如果 awk 取到的列不对,改用下面更可靠的方式:
kubectl get pods -n-l app= -o jsonpath='{range .items[*]}{.spec.nodeName}{" "}{end}' | sort | uniq -c | sort -rn
四-B、额外排查技巧
以上七个方向覆盖了大部分场景,但生产环境总会有些意料之外的情况。下面补充几个进阶技巧。
技巧一:直接看 Scheduler 日志
如果七个方向都排查过了还是找不到原因,可以考虑直接看 Scheduler 的日志。Scheduler 在调度失败时会输出详细原因:
kubectl logs -n kube-system -l component=kube-scheduler --tail=100
如果 Scheduler 是多副本高可用部署的,每个副本的日志都要看,因为同一个 Pod 的调度请求可能被不同的 Scheduler 实例处理(取决于 Leader Election 当前的 leader 是哪个):
# 先找到当前 Leader kubectl get lease -n kube-system kube-scheduler -o yaml | grep holderIdentity # 然后看对应 Pod 的日志 kubectl logs -n kube-system--tail=200 | grep -i "error|fail|unable"
日志级别默认是 --v=2,如果信息不够,可以临时调高 Scheduler 的 verbosity。
注意:kubeadm 部署的集群中,Scheduler 以静态 Pod 方式运行(由 kubelet 管理,不在 Deployment 中),修改日志级别需要编辑控制平面节点上的静态 Pod 清单文件:
# SSH 到控制平面节点 sudo vi /etc/kubernetes/manifests/kube-scheduler.yaml # 在 spec.containers[0].command 中找到 --v=2,改成 --v=4
kubelet 会检测到文件变更并自动重启 Scheduler Pod。如果集群是托管服务(GKE/EKS/AKS),控制平面由云厂商管理,日志级别需要查看对应厂商的文档。
调高日志级别会增加磁盘 I/O 和日志量,排查完毕后改回去。生产环境尤其要注意日志量突增可能导致节点磁盘压力。
技巧二:检查节点是否 Ready 且可调度
有时候节点看起来正常,但实际上处于不可调度状态。排查时容易默认所有 worker 节点都是 Ready 并且 Schedulable 的,但实际可能不是:
# 检查所有节点的 Ready 状态和是否被 cordon
# 注意:下面用 jsonpath 精确匹配 Ready condition,避免依赖数组顺序
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{" "}{.spec.unschedulable}{" "}{range .status.conditions[?(@.type=="Ready")]}{.status}{end}{"
"}{end}'
输出三列:节点名、unschedulable(true 表示被 cordon)、Ready 状态(True/False/Unknown)。
关键看两点:
unschedulable 是 true → 节点已被 cordon,Scheduler 不会把 Pod 调上去
Ready 是 False 或 Unknown → 节点异常,也不会被调度(除非 Pod 配置了对应的 Toleration)
字段 unschedulable 的语义是「是否禁止调度」,true 表示禁了,false 表示正常——和中文习惯相反,容易误读。
此外,检查节点的 Conditions 是否异常:
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{" "}{range .status.conditions[?(@.status=="True")]}{.type}{" "}{end}{"
"}{end}'
任何 Condition 的 Status 是 True 且 Type 为 MemoryPressure、DiskPressure、PIDPressure、NetworkUnavailable 的情况都会阻止调度。
技巧三:理解资源单位
排查资源不足时,经常要看 CPU 和内存的数值。Kubernetes 对 CPU 和内存使用不同的单位体系,容易混淆:
| 单位 | 含义 | 示例 |
|---|---|---|
| m | 毫核,1000m = 1 核 CPU | 500m = 0.5 核 |
| Ki | Kibibyte,1024 字节 | 256Ki |
| Mi | Mebibyte,1024 KiB | 256Mi = 256 × 1024 × 1024 字节 |
| Gi | Gibibyte,1024 Mi | 2Gi |
| 无单位 | CPU 为核数,内存默认为字节 | cpu: 1 = 1 核;memory: "536870912" = 512Mi |
注意:Kubernetes 的内存使用二进制单位(Mi、Gi),而云厂商虚机规格通常用十进制(GB)。1Gi ≈ 1.074GB,8Gi 的 Pod request 在名义上 8GB 的节点上可能刚好够,也可能差一些。排查资源问题时,把节点可分配内存从 kubectl describe node 输出中的值记下来,用的就是 Ki/Mi/Gi,不需要自己换算。
技巧四:containerd 与 dockershim 差异
如果集群容器运行时从 Docker 换成了 containerd(Kubernetes 1.24 之后移除了 dockershim),容器操作的命令会有变化。在排查镜像拉取问题时需要注意:
| 操作 | Docker | containerd |
|---|---|---|
| 拉取镜像 | docker pull | crictl pull 或 ctr image pull |
| 查看本地镜像 | docker images | crictl images 或 ctr image ls |
| 查看容器 | docker ps | crictl ps |
| 查看日志 | docker logs | crictl logs |
| 删除镜像 | docker rmi | crictl rmi 或 ctr image rm |
crictl 是 CRI(Container Runtime Interface)的标准客户端工具,所有兼容 CRI 的运行时(containerd、CRI-O)都可以用。而 ctr 是 containerd 的原生命令行,功能更全但不是所有集群都默认装了。
另外,containerd 的镜像命名规范比 Docker 更严格,镜像引用必须包含完整的 registry 地址和 tag。例如 nginx:latest 在 containerd 中需要写成 docker.io/library/nginx:latest,否则可能拉取失败。如果你在 containerd 环境中手动拉镜像测试连通性时提示 invalid image reference,先检查镜像名是否是完整路径。
技巧五:kubectl 模拟调度(Dry-Run)
Kubernetes 没有内置的「模拟调度」命令,但可以通过查看 Scheduler 日志或者使用第三方工具来模拟。如果你想快速验证修改后的 YAML 会不会被准入控制拦截,可以用 --dry-run=server:
kubectl apply -f pod.yaml --dry-run=server -n
如果输出 pod/
注意:dry-run 只验证准入控制,不会执行调度模拟。它能帮你发现 ResourceQuota、LimitRange 等准入问题,但发现不了资源不足、污点等问题。
技巧六:Scheduling Gate(Kubernetes 1.26+)
从 Kubernetes 1.26 开始引入了 Scheduling Gate 功能。Pod 可以声明 schedulingGates,Scheduler 在调度时会把带 Gate 的 Pod 跳过,直到 Gate 被外部控制器移除。如果你的集群开启了此功能,而某个 Pod 的 schedulingGate 没有被正确移除,Pod 会一直 Pending 且 Events 中没有任何 FailedScheduling 事件,因为 Scheduler 根本就没有试图调度它。
检查方式:
kubectl get pod-n -o jsonpath='{.spec.schedulingGates}'
如果输出非空(比如 [{"name":"example.com/foo"}]),说明有 Gate 没移除。需要找到设置 Gate 的控制器,确认它为什么没有及时移除。
这个方向比较少见,但如果你排查完前面七个方向都没找到原因,可以看一眼。
当你面对一个 Pending 的 Pod,按以下顺序排查:
kubectl get pod-n | v STATUS 是 Pending? | +--> No: Pending 和 ContainerCreating 之间的过渡 | 或者 Pod 还没被创建(看 RS Events) | v Yes kubectl describe pod -n | tail -30 | v Events 说什么? | +--> "Insufficient cpu/memory/storage" | |--> 方向一:节点资源不足 | |--> kubectl top nodes 看资源使用 | |--> 调低 requests 或扩容节点 | +--> "taint ... didn't tolerate" | |--> 方向二:污点与容忍 | |--> kubectl describe node | grep Taints | |--> 加 Toleration 或移除污点 | +--> "didn't match node selector" | |--> 方向三:NodeSelector/Affinity | |--> kubectl get nodes --show-labels | |--> 加标签或修改 nodeSelector | +--> "didn't match pod affinity/anti-affinity" | |--> 方向三:Pod 亲和性/反亲和性 | |--> 检查拓扑域和副本数 | |--> 放宽约束或增加节点 | +--> "Preempting" / "FailedPreemption" | |--> 方向七:优先级与抢占 | |--> kubectl get priorityclass | |--> 检查 PDB 限制 | +--> Events 中无调度相关错误? |--> kubectl get pvc -n (检查 PVC 状态) | |--> 方向四:PVC 绑定问题 | +--> kubectl get resourcequota -n | |--> 方向六:Quota 配额问题 | +--> kubectl get pod -o yaml | grep topologySpreadConstraints | |--> 方向七:拓扑分布约束 | +--> kubectl describe pod | grep -i image |--> 方向五:镜像拉取问题
以上顺序不是随机的,是按「出现频率从高到低」排列的。工作中有 80% 的 Pod Pending 问题都可以在前三个方向中找到答案。
六、生产环境操作守则
排查和修复 Pending 问题时,有些操作如果处理不当,会让小问题变成大故障。
6.1 驱逐 Pod 的风险
kubectl drain 和 kubectl delete pod 都可能导致服务短暂不可用。在驱逐之前,必须确认:
# 1. 确认被驱逐的 Pod 是否有 PDB 保护 kubectl get pdb -n# 2. 检查 Pod 所在的 Service 有没有多个就绪副本 kubectl get endpoints -n # 3. 检查 Pod 是否属于 StatefulSet(StatefulSet 的有序性可能会影响驱逐) kubectl get pod -n -o jsonpath='{.metadata.ownerReferences[0].kind}'
驱逐之前先在低峰期一个节点一个节点操作,不要批量 drain。
6.2 修改调度策略的风险
修改污点、NodeSelector、Affinity 如果在 Deployment 层面改,会触发滚动更新
如果配合 kubectl edit node 直接改节点属性,影响是实时的——正在该节点上运行的 Pod 不会被驱逐,但新调度会立即生效
在修改之前,保留一份当前配置的备份:
kubectl get deployment-n -o yaml > backup-deployment-$(date +%Y%m%d-%H%M).yaml
6.3 扩容节点的注意事项
云环境加节点一般几分钟就能 Ready,但自建集群可能需要 10 分钟以上
节点加进来后,确认 kubectl get nodes 看到 STATUS 是 Ready,再判断问题是否解决
如果是通过 Cluster Autoscaler 自动扩节点,确认 Autoscaler 的日志中没有异常
kubectl logs -n kube-system -l app=cluster-autoscaler --tail=50
6.4 调整 ResourceQuota 的注意事项
调大配额时确认集群真有那么多资源
不要永久删除 ResourceQuota(除非你确认这个 Namespace 不再需要限制),应该 patch 调大上限
配额调大后需要重建受影响的 Pod 或触发滚动更新才能让新 Pod 创建
6.5 执行窗口建议
| 操作类型 | 建议执行窗口 | 风险等级 |
|---|---|---|
| 调整 Deployment requests/limits | 低峰期 | 中(会触发滚动更新) |
| kubectl drain 节点 | 低峰期 | 高(驱逐所有 Pod) |
| 修改节点污点 | 维护窗口 | 高(影响调度) |
| 删除 PVC | 维护窗口 | 极高(可能丢数据) |
| 调整 ResourceQuota | 随时(仅改准入控制) | 低 |
| 给节点加标签 | 随时 | 低 |
| 扩容节点 | 随时 | 低(只加不减) |
6.6 回滚方案
每次操作之前,明确记录回滚步骤。例如改了 Deployment 的 resources,回滚就是:
kubectl rollout undo deployment-n
改了节点污点,回滚就是打回去:
kubectl taint nodes= :
把命令记下来,别等出了问题再想「刚才我到底执行了什么」。
七、总结
Pod Pending 的本质是「Scheduler 找不到合适的节点」。排查的核心思路是顺着 Scheduler 的过滤链一一排除障碍:
先看 kubectl describe pod 的 Events,它是第一线索
如果 Events 指向资源不足——调 requests 或扩容
如果 Events 指向污点——检查 Toleration 或移除污点
如果 Events 指向选择器——对比标签、放宽约束
如果 Events 没有明确调度错误——检查 PVC、ResourceQuota、镜像、优先级、拓扑约束
每一个修复都要验证——看 Events 出现 Scheduled,看 Pod 变成 Running
最后记住一条原则:生产环境的调度问题,能用「加资源」解决的不要用「删东西」解决;能用「调参数」解决的不要用「改架构」解决。循序渐进,先瞄准再动手。
全部0条评论
快来发表一下你的评论吧 !