Skip to content

필사 모드: Kubernetes Pod Pending 状态排查 — 读懂调度器留下的拒绝理由

中文
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

引言 — Pod 卡在 Pending 已经 10 分钟了

发布显示成功,但 Pod 一动不动。

kubectl get pod -n analytics

NAME                          READY   STATUS    RESTARTS   AGE
clickhouse-0                  0/1     Pending   0          11m
report-worker-7f4c8d9b6-hs2p  0/1     Pending   0          11m
report-worker-7f4c8d9b6-k9vt  1/1     Running   0          11m

Pending 的意思是 Pod 对象已经创建,但还没有确定落到哪个节点上。这不是容器的问题,而是放置问题,所以看日志什么也看不到。因为容器压根就没启动。

kubectl logs clickhouse-0 -n analytics

Error from server (BadRequest): container "clickhouse" in pod "clickhouse-0" is waiting to start: ContainerCreating

好在调度器每次都会把无法放置的原因记进事件里。那一句话就是诊断的全部。

调度器拒绝消息的解读方法

kubectl describe pod clickhouse-0 -n analytics | tail -6

Events:
  Type     Reason            Age                   From               Message
  ----     ------            ----                  ----               -------
  Warning  FailedScheduling  11m                   default-scheduler  0/5 nodes are available: 2 Insufficient cpu, 1 node(s) had untolerated taint {dedicated: batch}, 2 node(s) didn't match Pod's node affinity/selector. preemption: 0/5 nodes are available: 5 Preemption is not helpful for scheduling.

必须先弄清这句话的结构。

  • 0/5 — 全部 5 个节点中有 0 个可用。前面那个数字是通过筛选的节点数
  • 冒号后面是按节点统计的淘汰理由。2 + 1 + 2 = 5,与节点数正好对得上
  • 最后的 preemption: 子句表示,即便用基于优先级的抢占也腾不出位置

第二项蕴含着关键信息。调度器的过滤链对每个节点都停在第一个失败的插件上。所以一个节点只会被统计进一个理由里。既缺 CPU 又带污点的节点,只会显示排在前面的那一个理由。落到实操上,这意味着:改掉报出来的理由后紧接着冒出下一个理由,是正常现象。必须一次剥一层。

事件的默认保留时长是一小时,所以在停留已久的 Pending Pod 上可能已经消失了。这时就重建 Pod,重新取出事件。

kubectl get events -n analytics --field-selector reason=FailedScheduling --sort-by=.lastTimestamp | tail -3

LAST SEEN   TYPE      REASON             OBJECT             MESSAGE
3m41s       Warning   FailedScheduling   pod/clickhouse-0   0/5 nodes are available: 2 Insufficient cpu, ...

原因分支与诊断

原因调度器消息诊断命令解决
CPU、内存请求量超出Insufficient cpu, Insufficient memorykubectl describe node 的 Allocated resources调低 requests 或扩容节点
没有容忍度had untolerated taintkubectl describe node 的 Taints添加 tolerations 或移除污点
选择器、亲和性不匹配didn't match Pod's node affinity/selectorkubectl get nodes --show-labels给节点打标签或放宽条件
PVC 未绑定pod has unbound immediate PersistentVolumeClaimskubectl describe pvc确认 StorageClass 存在且制备器正常
卷可用区不匹配volume node affinity conflict查看 PV 的 nodeAffinity在同一可用区准备节点
Pod 反亲和性自我排除didn't match pod anti-affinity rules比较副本数与拓扑数放宽为 preferred 或增加节点
违反分布约束didn't match pod topology spread constraintskubectl get pods -o wide 查看当前分布放宽为 ScheduleAnyway,调整 maxSkew
单节点 Pod 数量上限Too many podskubectl get node -o jsonpath 查询 allocatable.pods修改 CNI 配置或扩容节点
节点 NotReady、cordonnode(s) were unschedulablekubectl get nodes恢复节点或 uncordon

资源不足 — 判定基准是 requests 而不是用量

这是最常见,同时也最经常被误诊的一支。

kubectl top node

NAME           CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
ip-10-0-1-14   842m         21%    4118Mi          31%
ip-10-0-2-77   1105m        27%    5203Mi          39%
ip-10-0-3-91   667m         16%    3877Mi          29%

每个节点的 CPU 都在 20% 上下,内存也在 30% 上下。如果从这里得出「资源明明有富余,为什么调度不上去」的结论,就会白白浪费好几个小时。调度器不看实际用量。它只看节点上已放置的那些 Pod 的 requests 总和。

kubectl describe node ip-10-0-1-14 | grep -A10 "Allocated resources"

Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests       Limits
  --------           --------       ------
  cpu                3730m (93%)    6200m (155%)
  memory             11544Mi (89%)  15Gi (118%)
  ephemeral-storage  0 (0%)         0 (0%)
  pods               24             24

实际用量 21%,预留量 93%。站在调度器的角度,这个节点已经几乎塞满了。原因通常是几个把 requests 开得过大的 Pod。

kubectl get pods -A --field-selector spec.nodeName=ip-10-0-1-14 \
  -o custom-columns='NS:.metadata.namespace,POD:.metadata.name,CPU:.spec.containers[*].resources.requests.cpu,MEM:.spec.containers[*].resources.requests.memory' \
  | sort -k3 -hr | head -6

NS          POD                            CPU     MEM
analytics   spark-executor-3               1500m   4Gi
analytics   spark-executor-1               1500m   4Gi
search      indexer-6c4d9f7b8-h2klp        500m    2Gi
payments    ledger-5f8b7c6d9-nm3xt         200m    1Gi

两个 Spark 执行器预留了 3 个 CPU 核,实际上几乎没怎么用。

这里必须把 requests 与 limits 的角色区分清楚。

  • requests — 调度的唯一基准。用节点的 allocatable 减去这些值来计算剩余空位。到了容器运行阶段,对 CPU 而言体现为 cgroup 权重,对内存而言则不带任何强制力
  • limits完全不参与调度。它只是运行期的上限。CPU limits 会引发限流,内存 limits 会引发 OOM 杀进程

所以 limits 开得再大也不会导致调度不上去,反过来,requests 开得大就算实际不用也会占住节点。

常见的错误答案:干脆把 requests 删掉让调度通过。Pod 是能起来,但 QoS 类别会变成 BestEffort,节点一旦承压就第一个被驱逐。而且该 Pod 在调度器的计算里被当作 0,会让节点过度密集,连邻居 Pod 都跟着有风险。正确的解法是以实际用量为依据重新核算 requests。

kubectl top pod -A --containers --sort-by=cpu | head -6

NAMESPACE   POD                 NAME              CPU(cores)   MEMORY(bytes)
analytics   spark-executor-3    executor          180m         1204Mi
analytics   spark-executor-1    executor          166m         1189Mi
search      indexer-6c4...      indexer           410m         1802Mi

预留了 1500m,却只用 180m。降到 400m 左右,就能整整空出一个节点。

节点不肯接收的情况

污点与容忍度

kubectl get nodes -o custom-columns='NAME:.metadata.name,TAINTS:.spec.taints[*].key,EFFECT:.spec.taints[*].effect'

NAME           TAINTS                              EFFECT
ip-10-0-1-14   dedicated                           NoSchedule
ip-10-0-2-77   <none>                              <none>
ip-10-0-3-91   node.kubernetes.io/disk-pressure    NoSchedule
ip-10-0-4-22   nvidia.com/gpu                      NoSchedule

只有第二个节点没有污点。第三个节点上的污点不是人打上去的,而是 kubelet 自动打上去的。看到 disk-pressure、memory-pressure、pid-pressure、unreachable、not-ready 这一类污点,那就是节点故障信号,而不是放置策略。不该靠加容忍度绕过去,而应该把节点修好。

如果是有意为之的污点,就加上容忍度。

spec:
  tolerations:
    - key: dedicated
      operator: Equal
      value: batch
      effect: NoSchedule

节点选择器与亲和性不匹配

消息里出现 affinity/selector 时,先确认符合条件的节点是否真的存在。

kubectl get pod clickhouse-0 -n analytics -o jsonpath='{.spec.nodeSelector}'

{"disktype":"nvme","topology.kubernetes.io/zone":"ap-northeast-2c"}

kubectl get nodes -l disktype=nvme,topology.kubernetes.io/zone=ap-northeast-2c

No resources found

把条件一个一个去掉试试,问题出在哪一边就显现出来了。

kubectl get nodes -l disktype=nvme

NAME           STATUS   ROLES    AGE   VERSION
ip-10-0-1-14   Ready    <none>   9d    v1.31.4

kubectl get nodes -l topology.kubernetes.io/zone=ap-northeast-2c

No resources found

nvme 节点是有的,但那个可用区里一个节点都没有。要么是可用区条件写错了,要么是该可用区的节点组被缩到了 0。

单节点 Pod 数量上限与 NotReady

如果消息是 Too many pods,那问题在于数量而不是资源。

kubectl get nodes -o custom-columns='NAME:.metadata.name,MAXPODS:.status.allocatable.pods'

NAME           MAXPODS
ip-10-0-1-14   29
ip-10-0-2-77   29
ip-10-0-3-91   29

在 EKS 上使用 VPC CNI 时,上限由节点能挂载的 ENI 与 IP 数量决定,所以实例类型偏小的话,即便 CPU 和内存还有富余也接不下更多 Pod。需要开启前缀委派,或者把实例类型调大。

节点本身掉线的情况也很常见。

kubectl get nodes

NAME           STATUS                     ROLES    AGE   VERSION
ip-10-0-1-14   Ready                      <none>   9d    v1.31.4
ip-10-0-2-77   Ready,SchedulingDisabled   <none>   9d    v1.31.4
ip-10-0-3-91   NotReady                   <none>   9d    v1.31.4

SchedulingDisabled 表示有人执行过 cordon。绝大多数情况是维护完之后忘了 uncordon。

存储与放置约束

PVC 未绑定,以及正常的 Pending

Pod 是 Pending、PVC 也是 Pending 时,大多数人会怀疑存储,但这里有两种必须区分开的情形。

先看真正出问题的那种。

kubectl describe pvc clickhouse-data -n analytics | tail -5

Events:
  Type     Reason              Age                From                         Message
  ----     ------              ----               ----                         -------
  Warning  ProvisioningFailed  9m (x14 over 12m)  persistentvolume-controller  storageclass.storage.k8s.io "fast-ssd" not found

StorageClass 的名字写错了。这是迁移集群时名字发生变化的典型情况。

kubectl get storageclass

NAME            PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
gp3 (default)   ebs.csi.aws.com         Delete          WaitForFirstConsumer   true                   61d
io2             ebs.csi.aws.com         Delete          WaitForFirstConsumer   true                   61d

再看正常的那种。

kubectl describe pvc report-cache -n analytics | tail -4

Events:
  Type    Reason                Age                From                         Message
  ----    ----                  ----               ----                         -------
  Normal  WaitForFirstConsumer  12s (x8 over 2m)   persistentvolume-controller  waiting for first consumer to be created before binding

这不是错误。VOLUMEBINDINGMODE 为 WaitForFirstConsumer 的 StorageClass,会在 Pod 被放置到节点之后,才在与该节点相同的可用区里创建卷。必须先放置 Pod 才会有卷,所以 PVC 处于 Pending 完全是按设计运作。

问题就出在这里。如果 Pod 因为别的原因放不上去,PVC 也会永远停在 Pending。于是人们盯着 PVC 去修存储,而真正要修的其实是 Pod 的调度失败。如果 PVC 是因为 WaitForFirstConsumer 在等待,就请忽略 PVC,只看 Pod 的 FailedScheduling 消息

卷已经创建之后再更换节点,可用区就可能对不上。

kubectl describe pod clickhouse-0 -n analytics | grep FailedScheduling

  Warning  FailedScheduling  4m  default-scheduler  0/5 nodes are available: 5 node(s) had volume node affinity conflict.

意思是卷在 ap-northeast-2a,而那个可用区里没有可调度的节点。

kubectl get pv -o custom-columns='NAME:.metadata.name,ZONE:.spec.nodeAffinity.required.nodeSelectorTerms[*].matchExpressions[*].values' | head -3

NAME                                       ZONE
pvc-8f3c1d5b-2e7a-94c0-f13d-68b5a2e9c47f   [ap-northeast-2a]

反亲和性导致的自我排除

如果给 3 个副本加上主机粒度的必需反亲和性,而节点只有 2 个,那么第三个 Pod 会永远 Pending。

spec:
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        - labelSelector:
            matchLabels:
              app: report-worker
          topologyKey: kubernetes.io/hostname
kubectl describe pod report-worker-7f4c8d9b6-hs2p -n analytics | grep FailedScheduling

  Warning  FailedScheduling  9m  default-scheduler  0/2 nodes are available: 2 node(s) didn't match pod anti-affinity rules.

这条约束确实有必须使用的场合,但副本数一旦超过拓扑数,它就会悄无声息地卡住发布。如果目的是可用性,通常用 preferred 就够了。

spec:
  affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
        - weight: 100
          podAffinityTerm:
            labelSelector:
              matchLabels:
                app: report-worker
            topologyKey: kubernetes.io/hostname

topologySpreadConstraints

分布约束比反亲和性温和,但只要把 whenUnsatisfiable 留成 DoNotSchedule,一样会被卡住。

spec:
  topologySpreadConstraints:
    - maxSkew: 1
      topologyKey: topology.kubernetes.io/zone
      whenUnsatisfiable: DoNotSchedule
      labelSelector:
        matchLabels:
          app: report-worker

有 3 个可用区,但其中一个可用区的节点全部塞满时,即使别的可用区还有位置,也会因为守不住 1 的偏斜而被拒绝放置。

kubectl get pods -n analytics -l app=report-worker -o wide

NAME                            READY   STATUS    NODE           ZONE
report-worker-7f4c8d9b6-k9vt    1/1     Running   ip-10-0-1-14   2a
report-worker-7f4c8d9b6-m2xd    1/1     Running   ip-10-0-2-77   2b
report-worker-7f4c8d9b6-hs2p    0/1     Pending   <none>         <none>

如果可用性不是绝对条件,改成 ScheduleAnyway 更实用。此时这条约束只参与打分计算,不会阻止放置。

Cluster Autoscaler 不扩节点的原因

节点不够时,人们期待自动扩缩容会把节点加上去,但不扩的情况其实很常见。这时理由同样会留在事件里。

kubectl describe pod clickhouse-0 -n analytics | grep -A2 NotTriggerScaleUp

  Normal   NotTriggerScaleUp  2m17s (x21 over 9m)  cluster-autoscaler  pod didn't trigger scale-up: 1 max node group size reached, 2 node(s) had untolerated taint {dedicated: batch}, 1 node(s) had volume node affinity conflict

它同时在说三件事。一个节点组已经达到最大规模,两个组带着这个 Pod 无法容忍的污点,还有一个组的卷可用区对不上。自动扩缩容器会预先模拟假设启动一个新节点之后这个 Pod 能否被放置,一旦判定放不上去,就不会去扩节点。

控制器侧的日志和状态 ConfigMap 也要一起看。

kubectl -n kube-system logs deployment/cluster-autoscaler --tail=200 | grep -i "no expansion\|max size\|scale_up"

I0726 09:52:14.118  scale_up.go:300] Pod analytics/clickhouse-0 can't be scheduled on eks-general-2c. Predicate checking error: node(s) had volume node affinity conflict
I0726 09:52:14.119  scale_up.go:472] No expansion options
kubectl -n kube-system get configmap cluster-autoscaler-status \
  -o jsonpath='{.data.status}' | head -12

Cluster-autoscaler status at 2026-07-26 09:52:14:
Cluster-wide:
  Health:      Healthy
  ScaleUp:     NoActivity (ready=5 registered=5)
  ScaleDown:   NoCandidates

NodeGroups:
  Name:        eks-general-2a
  Health:      Healthy (ready=3 cloudProviderTarget=3 minSize=1 maxSize=3)
  ScaleUp:     NoActivity

maxSize 与 cloudProviderTarget 相同,就说明撞上了上限。

常见的错误答案:一看到 Pending 就无脑加节点。反亲和性、分布约束、卷可用区不匹配这几类,要么加节点也解不开,要么节点加到了不相干的可用区,只是把成本推高。应该先读 NotTriggerScaleUp 消息,把它指出的理由消除掉。

防止复发

Pending 大多是 requests 的估算缺乏依据所导致的结果。可以用三道装置把它压下去。

第一,在命名空间上强制默认值与上限。挡住那些漏写 requests 的 Pod。

apiVersion: v1
kind: LimitRange
metadata:
  name: default-requests
  namespace: analytics
spec:
  limits:
    - type: Container
      default:
        cpu: 500m
        memory: 512Mi
      defaultRequest:
        cpu: 100m
        memory: 256Mi
      max:
        cpu: 4
        memory: 16Gi

第二,把 VPA 跑在推荐模式下,拿到基于实际用量的建议值。即便不做到自动应用,光是能看到这些数字就已经有价值。

kubectl get vpa spark-executor -n analytics -o jsonpath='{.status.recommendation.containerRecommendations[0]}'

{"containerName":"executor","lowerBound":{"cpu":"180m","memory":"1000Mi"},"target":{"cpu":"320m","memory":"1400Mi"},"upperBound":{"cpu":"1200m","memory":"3Gi"}}

第三,用告警抓住 Pending 的持续时长。放置失败在应用指标上完全不会体现,所以需要单独的规则。

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: scheduling-alerts
  namespace: monitoring
spec:
  groups:
    - name: scheduling
      rules:
        - alert: PodPendingTooLong
          expr: kube_pod_status_phase{phase="Pending"} == 1
          for: 15m
          labels:
            severity: warning
          annotations:
            summary: "{{ $labels.namespace }}/{{ $labels.pod }} has been Pending for 15m"
        - alert: NodeRequestsNearlyFull
          expr: sum by (node) (kube_pod_container_resource_requests{resource="cpu"}) / on(node) kube_node_status_allocatable{resource="cpu"} > 0.9
          for: 30m
          labels:
            severity: warning

结语 — 调度器早就把答案说出来了

在 Pending 的诊断里,几乎没有需要靠猜的时刻。FailedScheduling 事件的那一句话里,节点数和淘汰理由都已经统计好了,而每条理由都对应上面表格中的某一行。只要记住过滤链在每个节点的第一次失败处就停下,所以理由是按顺序逐个浮现的,这件事就变成了一次剥一层的工作。

值得记住的一句话是这样的。调度是由 requests 的算术决定的,而不是实际用量;kubectl top 看起来清闲,并不代表节点上还有位置。

현재 단락 (1/244)

发布显示成功,但 Pod 一动不动。

작성 글자: 0원문 글자: 10,989작성 단락: 0/244