Pending、抢占与调度失败排障
一次扩容后,十二个新 Pod 有九个保持 Pending。节点面板仍显示总 CPU 空闲,Cluster Autoscaler 也没有立即增加节点。值班同学先重启 Deployment,又把副本数来回调整,Event 却始终重复 0/18 nodes are available。真正的矛盾藏在后半句:一部分节点 CPU 不足,一部分不满足 node affinity,还有一部分带有 Pod 未容忍的 taint。集群总余量并不是这个 Pod 的可用余量。
另一次故障更容易误判。高优先级 Pod 出现 Preempted 相关信息,团队便认定抢占已经释放容量;几分钟后 Pod 仍未绑定,低优先级副本却已经被终止。检查才发现,候选节点上的本地卷拓扑与 Pod 的 PVC 冲突,移除受害 Pod 也无法让硬约束成立。抢占只尝试消除资源竞争,不会修复标签、污点、端口、卷拓扑或调度器名称错误。
Pending 只是阶段,PodScheduled 才是第一处分界
Pending 表示 Pod 已被 API Server 接受,但至少一个容器尚未开始运行。它既可能仍在等待调度,也可能已经绑定节点、正在拉取镜像或挂载卷。因此第一条命令不是重启,而是读取 spec.nodeName 与 PodScheduled 条件:
kubectl get pod <pod> -n <namespace> \
-o custom-columns='NAME:.metadata.name,NODE:.spec.nodeName,SCHEDULED:.status.conditions[?(@.type=="PodScheduled")].status,REASON:.status.conditions[?(@.type=="PodScheduled")].reason'
kubectl describe pod <pod> -n <namespace>spec.nodeName 为空且 PodScheduled=False、reason=Unschedulable,问题仍在调度路径;spec.nodeName 已有值,则应转查 kubelet、镜像、卷或运行时。PodScheduled=True 之后再出现 Pending,继续分析 FailedScheduling 只会追错责任域。Pod 生命周期说明了 Pod phase 与 conditions 的差异,调试运行中 Pod给出了 Event、容器状态和节点侧证据入口。
Event 是聚合后的诊断线索,不是完整审计日志。相同原因可能被合并并增加 count,旧 Event 也会按集群保留策略消失。应同时保存 Pod UID、resourceVersion、调度条件、Event 的 reason/message/count/firstTimestamp/lastTimestamp 等现场字段;对正式文章中的时间示例,则使用 T0、T+N 表达相对顺序。
先确认请求进入了哪一个调度器
默认 Pod 使用 spec.schedulerName: default-scheduler。多 profile 或自定义调度器环境中,名称写错会让 Pod 永远没有调度器消费;同名调度器重复运行而 leader election、RBAC 或锁配置错误,则可能表现为没有活跃实例。先读取最终 Pod,而不是只看 Helm values 或 Deployment 模板:
kubectl get pod <pod> -n <namespace> -o jsonpath='{.spec.schedulerName}{"\n"}'
kubectl get leases -n kube-system
kubectl get pods -n kube-system -l component=kube-scheduler -o wide
kubectl auth can-i get pods --as=system:serviceaccount:<namespace>:<scheduler-service-account>
kubectl auth can-i create pods/binding --as=system:serviceaccount:<namespace>:<scheduler-service-account>托管集群可能不公开控制面 Pod 或日志,此时应使用供应商提供的控制面日志出口;不要为了排障给业务账号补 cluster-admin。自建集群需要读取调度器日志时,使用只读日志权限和受审计的临时访问。Pod、Secret、节点标签和 Event 可能泄露租户名、镜像仓库、拓扑与业务容量,导出工单前要脱敏。
自定义调度器或 profile 的启用不是改一个 Pod 字段就结束。KubeSchedulerConfiguration 当前稳定配置 API 为 kubescheduler.config.k8s.io/v1,profile 名称必须与 Pod 的 schedulerName 一致;同一调度器进程内所有 profile 的 QueueSort 配置还必须兼容。具体字段应以调度器配置和kube-scheduler 配置 API为准。
把 FailedScheduling 拆成可行动的原因集合
典型 Event 类似下面这样:
Warning FailedScheduling default-scheduler
0/18 nodes are available: 6 Insufficient cpu, 4 node(s) didn't match Pod's node affinity/selector,
3 node(s) had untolerated taint {dedicated: batch}, 5 node(s) didn't satisfy pod topology spread constraints.
preemption: 0/18 nodes are available: 6 No preemption victims found for incoming pod,
12 Preemption is not helpful for scheduling.数字不是互斥分桶。同一个节点可能同时 CPU 不足、标签不匹配并带有污点;因此不能把各原因数量相加后与节点总数比较。正确做法是把 Pod 的硬约束逐项与节点事实求交:requests 与 allocatable、node selector/required affinity、taint/toleration、Pod affinity/anti-affinity、topology spread、host port、PVC 与卷拓扑。只有所有 Filter 插件都返回成功的节点,才进入 Score。
下面的快照能把对象事实放在一起:
kubectl get pod <pod> -n <namespace> -o yaml > pending-pod.yaml
kubectl get nodes -o custom-columns='NAME:.metadata.name,READY:.status.conditions[?(@.type=="Ready")].status,CPU:.status.allocatable.cpu,MEMORY:.status.allocatable.memory,TAINTS:.spec.taints'
kubectl get pvc,pv -n <namespace>
kubectl get events -n <namespace> --field-selector involvedObject.name=<pod> --sort-by=.metadata.creationTimestamppending-pod.yaml 是故障现场,可能包含环境信息,不应提交到公开仓库。调度器使用的是最终准入后的 Pod,LimitRange、mutating webhook、sidecar 注入都可能增加 requests 或约束;只检查应用仓库里的原始 YAML 会漏掉这些变化。
requests、可分配量与碎片为何制造“假空闲”
调度器按 Pod requests 做资源可行性判断,不按实时 CPU 使用率,也不把 limits 当成已预留容量。节点还有系统保留、DaemonSet、已绑定 Pod 与扩展资源占用。一个节点即使平均 CPU 使用率很低,只要剩余可分配 request 小于新 Pod 的 request,仍会返回 Insufficient cpu。
用下面的命令对照节点账本:
kubectl describe node <node>
kubectl get pods -A --field-selector spec.nodeName=<node> \
-o custom-columns='NS:.metadata.namespace,NAME:.metadata.name,CPU:.spec.containers[*].resources.requests.cpu,MEM:.spec.containers[*].resources.requests.memory'
kubectl top node <node>kubectl top 回答“最近使用了多少”,describe node 的 Allocated resources 回答“调度账本承诺了多少”,两者用途不同。若许多节点都剩下零散的 CPU 和内存,却没有任何单节点能容纳 Pod,就是容量碎片。降低 request 可能暂时让 Pod 绑定,却会同时影响 HPA 利用率、VPA 推荐与节点伸缩模拟;必须用压测和历史分位验证,而不是把排障变成资源透支。
调度器会暴露延迟、尝试次数和 pending Pod 等指标,不同发行线的指标名与稳定性可能变化,接入前应查调度器指标参考。重点看趋势和标签语义:unschedulable 队列是否持续增长、一次调度尝试在哪个阶段变慢、成功绑定是否恢复,而不是复制一组脱离集群规模的固定阈值。
正向实验:制造一次可解释、可恢复的绑定
实验账号需要在隔离命名空间创建 Namespace、Deployment,读取 Pod/Event;给节点打标签需要更高权限,生产集群应由平台 owner 执行。没有节点写权限时,可以选择一个已经存在的非敏感标签值。下面使用专用实验 key,执行前把 <node> 替换为可调度测试节点:
kubectl create namespace scheduling-lab
kubectl label node <node> diagnostics.example.com/pool=lab
kubectl auth can-i create deployments -n scheduling-lab
kubectl auth can-i list events -n scheduling-lab创建一个 request 很小且具有正确硬约束的 Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: sched-ok
namespace: scheduling-lab
spec:
replicas: 1
selector:
matchLabels: {app: sched-ok}
template:
metadata:
labels: {app: sched-ok}
spec:
nodeSelector:
diagnostics.example.com/pool: lab
containers:
- name: web
image: registry.k8s.io/pause:3.10
resources:
requests: {cpu: 10m, memory: 16Mi}
limits: {memory: 32Mi}保存为临时文件并执行:
kubectl apply -f sched-ok.yaml
kubectl wait --for=condition=Available deployment/sched-ok -n scheduling-lab --timeout=120s
kubectl get pod -n scheduling-lab -l app=sched-ok -o wide
kubectl get events -n scheduling-lab --sort-by=.metadata.creationTimestamp预期证据是 Pod 获得 spec.nodeName=<node>,PodScheduled=True,Deployment 变为 Available。Event 可能包含 Scheduled,但 Event 缺失不推翻最终对象状态;绑定结果、Pod condition 与容器 Ready 才组成更稳定的证据链。如果 Pod 已绑定却未 Ready,应转查镜像拉取或 kubelet,不再归类为调度失败。
反向实验:让硬约束稳定地产生 Pending
把节点选择器改成集群不存在的值,能够稳定触发约束交集为空,而且不会消耗计算资源:
kubectl patch deployment sched-ok -n scheduling-lab --type=merge -p \
'{"spec":{"template":{"spec":{"nodeSelector":{"diagnostics.example.com/pool":"missing"}}}}}'
kubectl rollout status deployment/sched-ok -n scheduling-lab --timeout=30s || true
kubectl get pods -n scheduling-lab -o wide
kubectl describe pod -n scheduling-lab -l app=sched-ok旧 Pod 会在滚动更新策略允许时继续运行,新 ReplicaSet 的 Pod 应保持 Pending,PodScheduled=False,Event 出现 FailedScheduling 和 selector/affinity 不匹配。这里即使增加一个没有 diagnostics.example.com/pool=missing 标签的通用节点,结论也不会改变。这是区分“资源不足可扩容”和“供给模板不满足硬约束”的关键反例。
修复时把字段恢复为 lab,然后观察新 Pod 绑定:
kubectl patch deployment sched-ok -n scheduling-lab --type=merge -p \
'{"spec":{"template":{"spec":{"nodeSelector":{"diagnostics.example.com/pool":"lab"}}}}}'
kubectl rollout status deployment/sched-ok -n scheduling-lab --timeout=120s如果使用 Cluster Autoscaler 或 Karpenter,排障还要证明其候选节点组或 NodePool 能生成带正确标签、污点、实例资源和卷拓扑的节点。只有“扩容器看到 Unschedulable Pod”而没有“Node Ready -> Pod 绑定 -> Pod Ready”的后续证据,不能算容量恢复。
抢占要看 nominatedNodeName、受害者与二次调度
抢占发生在普通 Filter 找不到可行节点之后。调度器尝试寻找一个节点,假设移除较低优先级 Pod 后能让高优先级 Pod 通过过滤,并可能把候选节点写入 status.nominatedNodeName。这不是绑定承诺:受害 Pod 有优雅终止期,其他 Pod 也可能竞争该节点,候选状态还可能在后续调度周期改变。
kubectl get pod <pod> -n <namespace> \
-o jsonpath='{.status.nominatedNodeName}{"\n"}{.status.conditions[?(@.type=="PodScheduled")]}{"\n"}'
kubectl get events -n <namespace> --field-selector involvedObject.name=<pod> --sort-by=.metadata.creationTimestamp
kubectl get pods -A --field-selector spec.nodeName=<candidate-node> -o widePreemption is not helpful for scheduling 通常说明移除低优先级 Pod 仍不能消除某项硬约束;No preemption victims found 则可能是没有足够低优先级受害者,或受害组合无法满足需求。PDB 在抢占受害者选择中是 best effort,并非绝对保护;业务不能把 PDB 当作高优先级滥用的安全阀。Pod Priority 与 Preemption给出了 nominatedNodeName、PDB 语义与非抢占 PriorityClass 的正式说明。
高优先级应映射到明确的业务恢复目标和容量预算。若所有团队都申请最高优先级,系统只会把普通 Pending 变成更破坏性的驱逐、重启与优先级反转。平台团队拥有 PriorityClass,业务 owner 说明恢复等级,SRE 审核受害面与告警,三方责任不能交给应用模板默认值。
卷、端口与准入变更会绕过直觉
有状态负载经常在 CPU、内存都充足时失败。PV 的 node affinity、StorageClass 的 volume binding mode、可用区与 Pod 拓扑必须同时成立。WaitForFirstConsumer 会让卷绑定考虑 Pod 调度约束;过早绑定到错误区域的卷,则可能让所有候选节点在 VolumeBinding 阶段失败。先检查 PVC/PV 和 StorageClass,而不是直接删除数据卷:
kubectl get pvc -n <namespace> <pvc> -o yaml
kubectl get pv <pv> -o yaml
kubectl get storageclass <storage-class> -o yaml
kubectl describe pod <pod> -n <namespace>hostPort 冲突、节点不可调度、未容忍 taint、扩展资源名称错误,也都会缩小可行集合。动态资源分配、设备插件和 CSI 还会引入各自对象状态。排障顺序应保持不变:读取最终 Pod,确定失败插件或原因,再检查该插件依赖的对象;不要从“集群有空闲”直接跳到扩节点。
Webhook 与 LimitRange 也是常见转折点。sidecar 注入可能让每个 Pod 多出一份 request,策略 webhook 可能补 affinity,LimitRange 可能写入默认 request。保存 kubectl get pod -o yaml 与 Deployment 模板的差异,可以证明变化发生在准入之后;审计日志能进一步关联写入者,但其中可能含敏感对象,只允许受控访问。
用一条时间线连接调度、节点供给和业务恢复
可靠的故障记录至少串起这些状态:Pod 创建于 T0,PodScheduled=False 首次出现于 T+1,扩容器识别可扩容原因,节点对象创建并 Ready,Pod 绑定,镜像与卷完成,Pod Ready,业务指标恢复。每一步由不同控制器负责,前一步成功不代表后一步成功。
指标与日志需要共享 Pod UID、namespace、scheduler、节点或控制器标识,才能避免把不同发布批次拼成一条假时间线。调度器日志级别临时升高会增加控制面 I/O 与敏感字段暴露,应限定窗口、保留期和访问者,排障结束后恢复原级别。大规模集群还要预算 Event、指标标签和日志基数,不能为了可观测性反过来压垮 API Server 与存储后端。
选型、升级与退出都要保留可逆路径
大多数工作负载先使用默认调度器、原生放置原语与标准 Event;只有确有新评分逻辑、队列语义或硬件拓扑需求,才引入 profile、自定义插件或独立调度器。自定义实现的代价包括高可用、leader election、RBAC、配置 API、插件兼容、指标、升级和长期 owner。能用 label、taint、affinity、topology spread 表达的问题,不应先改调度器代码。
升级前保存调度器配置、profile 名称、插件启停和参数,使用目标 Kubernetes minor 对应的配置 API 校验;先让少量测试工作负载通过新 profile,比较调度成功率、延迟、原因分布和最终放置,再扩大流量。若 Event 文案或指标名变化,告警与解析规则也要同步验证,不能把字符串当作永远稳定的 API。
退出自定义调度器时,先把工作负载的 schedulerName 迁回仍然可用的 profile,确认没有未调度 Pod,再停止调度器并撤销 ServiceAccount、RoleBinding、日志访问和监控抓取。删除配置前保留受控的回滚窗口;不要先卸载调度器,留下无人消费的 Pod。资源侧还要移除实验标签、临时 PriorityClass 和诊断数据,确认节点供给器不会继续为旧约束创建付费节点。
清理实验并固化团队责任
先删除实验工作负载,再移除节点标签和命名空间,避免标签先消失制造额外 Pending:
kubectl delete deployment sched-ok -n scheduling-lab --wait=true
kubectl label node <node> diagnostics.example.com/pool-
kubectl delete namespace scheduling-lab --wait=true
kubectl get pods -A --field-selector=status.phase=Pending若命名空间无法终止,先检查仍存资源和 finalizer,不要直接清空 finalizers。若实验触发了节点扩容,还要等待或按平台流程回收空节点,核对云实例、卷、IP 与日志保留,避免 Kubernetes 对象删除后费用继续存在。
团队运行手册应固定“先分界、再分型、后修复”的入口:业务 owner 维护 requests、放置约束与优先级理由;平台 owner 维护节点标签、污点、调度器配置和节点供给模板;SRE 维护事件、指标、日志关联与恢复时间线;安全团队控制控制面日志和临时诊断权限。一次调度故障只有在原因已消失、新 Pod 能稳定绑定并 Ready、业务容量恢复、临时权限和资源已核销后,才真正结束。
