Kubernetes中Pod一直Pending无法启动的故障排查

描述

一、问题背景

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 -n 
kubectl 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 nodes  node.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 node  disktype=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 -n 
kubectl 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 -n 
kubectl 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 -n 
kubectl 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/ created (server dry run),说明准入控制层通过,资源配额、LimitRange 没有拦截。如果被拦截,会直接返回错误原因。

注意: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

最后记住一条原则:生产环境的调度问题,能用「加资源」解决的不要用「删东西」解决;能用「调参数」解决的不要用「改架构」解决。循序渐进,先瞄准再动手。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分