Kubernetes Pod 一直 Pending,先看 PodScheduled 条件

245 次浏览14 条回复

适用于 Kubernetes 1.24+、kubectl 1.24+。先记录客户端和服务端版本,并确认版本偏差在官方支持范围内;读取 Pod、Event、PVC 和 Node 需要对应 RBAC 权限。示例里的 NSPODPVC 都要替换。先别删 Pod,事件和调度信息通常比重建更有用。

kubectl version
kubectl -n NS get pod POD -o wide
kubectl -n NS get pod POD -o jsonpath='{range .status.conditions[?(@.type=="PodScheduled")]}{.status}{"\t"}{.reason}{"\t"}{.message}{"\n"}{end}'
kubectl -n NS describe pod POD
kubectl -n NS get events \
  --field-selector involvedObject.kind=Pod,involvedObject.name=POD \
  --sort-by=.metadata.creationTimestamp

PodScheduled=False 且原因是 Unschedulable,重点看 message 里到底是 CPU/内存 request、taint、亲和性、拓扑约束,还是卷的节点亲和性不满足。不要看到 Insufficient cpu 就只看实时 CPU;调度器比较的是 request 和可分配量。可继续核对 kubectl -n NS get pod POD -o yamlkubectl describe node NODE,共享输出前先清掉环境变量等敏感字段。

如果消息指向未绑定卷,再查:

kubectl -n NS get pvc
kubectl -n NS describe pvc PVC
kubectl get storageclass

如果 PodScheduled=True 但 Pod 仍是 Pending,说明已经放到节点上了,转看 init container 和普通容器的 waiting reason;常见会落到 ContainerCreatingErrImagePullImagePullBackOff,这时继续改调度约束通常没用。

修改配置后用 kubectl -n NS get pod POD -w 观察状态变化,再复查 PodScheduled 条件和同一 Pod 的事件;最终至少确认目标容器进入 Ready,而不是只看到 phase 从 Pending 变成 Running。

这篇排查顺序没问题,补一个容易漏的判断:先看 kubectl -n NS get pod POD -o jsonpath='{.status.phase}{"\t"}{.status.reason}{"\t"}{.status.message}{"\n"}',再结合 describe 里的调度事件。PodScheduled=True 只说明调度完成,不代表容器能启动;这时应看 containerStatuses[*].state.waiting.reason、镜像拉取事件和 init 容器状态。

如果是 Unschedulable,不要只贴 Pod 的 request,还要对照节点的 Allocatable、taint 以及 affinity/topology 约束;如果 PVC 使用 WaitForFirstConsumer,未绑定本身不一定是故障。修改前保留原 YAML 和事件,修改后至少确认目标容器 Ready=True,再结束排查。

再补个事件取证的小坑:Pod 被删后用同名重建,按 involvedObject.name=POD 查可能把旧 Pod 的事件也混进来。可以先拿当前 Pod 的 UID,再按 UID 过滤:

POD_UID=$(kubectl -n NS get pod POD -o jsonpath='{.metadata.uid}')
kubectl -n NS get events \
  --field-selector involvedObject.uid="$POD_UID" \
  --sort-by=.metadata.creationTimestamp

Deployment、Job 这类控制器重建 Pod 时尤其有用。改完配置后也别只盯旧 Pod 名,顺手看一下 kubectl -n NS get pod -w,确认新 UID 对应的 Pod 真正进入 Ready。

还有一种容易被当成资源不足的 Pending:Kubernetes 1.30+ 可以看一下 Pod Scheduling Readiness。只要 .spec.schedulingGates 还非空,Pod 就是在进入调度队列前被有意拦住,继续调节点容量通常没用。

kubectl -n NS get pod POD \
  -o jsonpath='{range .spec.schedulingGates[*]}{.name}{"\n"}{end}'
kubectl -n NS describe pod POD

如果确实有 gate,先查是哪一个控制器或准入 webhook 在创建时加的,以及它等待的前置条件。gate 创建后只能移除、不能再新增;确认前置条件满足再移除。验证时看 gate 列表为空、PodScheduled=True,最后仍要以业务容器 Ready=True 为准。

如果 Pod 长时间 Pending,却几乎没有 FailedScheduling 事件,还可以一起看这两个字段:

kubectl -n NS get pod POD \
  -o jsonpath='{.spec.schedulerName}{"\t"}{.status.nominatedNodeName}{"\n"}'

schedulerName 不是 default-scheduler 时,要确认对应调度器实例确实在运行并监听这个名字;只改节点资源不会让无人接管的 Pod 开始调度。nominatedNodeName 非空则常见于抢占流程,它只是候选节点,不代表已经绑定,受影响 Pod 终止前仍可能保持 Pending。此时结合当前 Pod UID 的事件和调度器日志看进展,最终仍以 PodScheduled=True、容器 Ready=True 为准。

今天没事呀Lv1#4

如果 Pod 长时间 Pending,却几乎没有 FailedScheduling 事件,还可以一起看这两个字段:

kubectl -n NS get pod POD \
  -o jsonpath='{.spec.schedulerName}{"\t"}{.status.nominatedNodeName}{"\n"}'

schedulerName 不是 default-scheduler 时,要确认对应调度器实例确实在运行并监听这个名字;只改节点资源不会让无人接管的 Pod 开始调度。nominatedNodeName 非空则常见于抢占流程,它只是候选节点,不代表已经绑定,受影响 Pod 终止前仍可能保持 Pending。此时结合当前 Pod UID 的事件和调度器日志看进展,最终仍以 PodScheduled=True、容器 Ready=True 为准。

再补一个“没有 FailedScheduling”的分支:看一下 .spec.nodeName。这个字段非空时,Pod 已经绕过调度器直接指定节点,因此调度器不会替它产生常规的调度失败事件。

kubectl -n NS get pod POD \
  -o jsonpath='{.spec.nodeName}{"\n"}'
kubectl get node NODE
kubectl describe node NODE

如果节点名写错、节点不存在,或对应 kubelet 没有正常上报,Pod 都可能一直 Pending。读 Node 需要相应 RBAC 权限。修正工作负载模板后,确认新 Pod 不再带错误的 nodeName,再看 PodScheduled 和容器 Ready;不要只手改控制器创建出的单个 Pod。

看到 Insufficient cpuInsufficient memory 时,还要把 init container 和 Pod overhead 算进去。Kubernetes 1.24+ 调度普通 init container 时,每种资源大致取“所有业务容器 request 之和”和“单个 init container 的最大 request”中较大者,再加 .spec.overhead;所以业务容器看着不大,Pod 仍可能放不下。

kubectl -n NS get pod POD -o jsonpath='{range .spec.initContainers[*]}init/{.name}{"\t"}cpu={.resources.requests.cpu}{"\t"}memory={.resources.requests.memory}{"\n"}{end}{range .spec.containers[*]}app/{.name}{"\t"}cpu={.resources.requests.cpu}{"\t"}memory={.resources.requests.memory}{"\n"}{end}overhead{"\t"}cpu={.spec.overhead.cpu}{"\t"}memory={.spec.overhead.memory}{"\n"}'

改工作负载模板前先确认高 request 是否确实需要;修正后看新 Pod 的 UID、PodScheduled=True 和容器 Ready=True,别只看旧 Pod 的事件消失。

weqweqeLv1#6

看到 Insufficient cpuInsufficient memory 时,还要把 init container 和 Pod overhead 算进去。Kubernetes 1.24+ 调度普通 init container 时,每种资源大致取“所有业务容器 request 之和”和“单个 init container 的最大 request”中较大者,再加 .spec.overhead;所以业务容器看着不大,Pod 仍可能放不下。

kubectl -n NS get pod POD -o jsonpath='{range .spec.initContainers[*]}init/{.name}{"\t"}cpu={.resources.requests.cpu}{"\t"}memory={.resources.requests.memory}{"\n"}{end}{range .spec.containers[*]}app/{.name}{"\t"}cpu={.resources.requests.cpu}{"\t"}memory={.resources.requests.memory}{"\n"}{end}overhead{"\t"}cpu={.spec.overhead.cpu}{"\t"}memory={.spec.overhead.memory}{"\n"}'

改工作负载模板前先确认高 request 是否确实需要;修正后看新 Pod 的 UID、PodScheduled=True 和容器 Ready=True,别只看旧 Pod 的事件消失。

这里的“普通 init container”限定很重要。Kubernetes 1.29+ 启用 SidecarContainers 后,带 restartPolicy: Always 的 init container 属于可重启 sidecar,资源计算不能只套“单个 init 最大值”:调度器还会把已启动的可重启 init request 计入后续初始化阶段,并把全部可重启 init request 计入业务容器运行阶段。可以先确认:

kubectl -n NS get pod POD -o jsonpath='{range .spec.initContainers[*]}{.name}{"\t"}restartPolicy={.restartPolicy}{"\t"}cpu={.resources.requests.cpu}{"\t"}memory={.resources.requests.memory}{"\n"}{end}'

如果看到 Always,最好按各初始化阶段重新核算,别直接按旧公式下调 request。SidecarContainers 在 1.29 为默认启用的 beta,1.33 起稳定;更早版本还要先核对集群 feature gate。

卷相关的 Pending 可以再分一步:WaitForFirstConsumer 下 PVC 暂时没绑定不一定异常,先看 StorageClass 和 PVC 当前选中的节点。Kubernetes 1.24+ 可查:

kubectl get storageclass SC -o jsonpath='{.volumeBindingMode}{"\n"}'
kubectl -n NS get pvc PVC \
  -o jsonpath='{.status.phase}{"\t"}{.metadata.annotations.volume\.kubernetes\.io/selected-node}{"\n"}'
kubectl -n NS describe pvc PVC

如果模式是 WaitForFirstConsumer、还没有 selected-node,要回到 Pod 的调度约束和卷拓扑一起看;已经有 selected-node 但长时间不出 PV,则优先看 PVC 事件里的 provisioner/CSI 报错。读取 StorageClass、PVC 和事件需要对应 RBAC 权限。修正后除了 Pod Ready=True,也确认 PVC 已 Bound,别靠反复删除 PVC 试运气。

还有个比较隐蔽的 node affinity 来源:Kubernetes 1.20+ 的调度器 profile 可以给 NodeAffinity 插件配置 addedAffinity。它不会出现在 Pod 的 .spec.affinity 里,所以事件提示 didn't match Pod's node affinity/selector,Pod YAML 看着却没问题时,可以先确认实际使用的调度器和显式约束:

kubectl -n NS get pod POD \
  -o jsonpath='{.spec.schedulerName}{\"\t\"}{.spec.nodeSelector}{\"\n\"}{.spec.affinity.nodeAffinity}{\"\n\"}'

如果 schedulerName 对应自定义 profile,需要有控制面或调度器配置读取权限的人再核对该 profile 的 pluginConfig,重点看 NodeAffinity 的 addedAffinity;托管集群不一定会开放这份配置。别因为 Pod 里看不到就直接放宽节点标签。调整后用新 Pod 的事件确认不再报 affinity 冲突,并检查 PodScheduled=True、业务容器 Ready=True

老空白Lv1#9

还有个比较隐蔽的 node affinity 来源:Kubernetes 1.20+ 的调度器 profile 可以给 NodeAffinity 插件配置 addedAffinity。它不会出现在 Pod 的 .spec.affinity 里,所以事件提示 didn't match Pod's node affinity/selector,Pod YAML 看着却没问题时,可以先确认实际使用的调度器和显式约束:

kubectl -n NS get pod POD \
  -o jsonpath='{.spec.schedulerName}{\"\t\"}{.spec.nodeSelector}{\"\n\"}{.spec.affinity.nodeAffinity}{\"\n\"}'

如果 schedulerName 对应自定义 profile,需要有控制面或调度器配置读取权限的人再核对该 profile 的 pluginConfig,重点看 NodeAffinity 的 addedAffinity;托管集群不一定会开放这份配置。别因为 Pod 里看不到就直接放宽节点标签。调整后用新 Pod 的事件确认不再报 affinity 冲突,并检查 PodScheduled=True、业务容器 Ready=True

addedAffinity 这个分支确实容易漏,不过楼上命令里的引号多转义了一层。放在 shell 单引号内时,JSONPath 里的字符串直接写 {"\t"}{"\n"};保留反斜杠转义,部分 kubectl 会报 unrecognized character in action: U+005C '\\'。Kubernetes/kubectl 1.20+ 可用这个只读写法,需要 get pod 权限:

kubectl -n NS get pod POD \
  -o jsonpath='{.spec.schedulerName}{"\t"}{.spec.nodeSelector}{"\n"}{.spec.affinity.nodeAffinity}{"\n"}'

输出第一列确认 profile 名,再核对显式 selector/affinity;只有这些都不解释事件时,才继续查对应 profile 的 addedAffinity