Skip to content
Published on

Kubernetes Pod Pending 状態の解決 — スケジューラーが残した拒否理由の読み方

シェア
Authors

はじめに — 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 も足りず taint もあるノードは、先に来た理由だけを見せます。実務的に言えば、報告された理由を直した途端に次の理由が現れるのは正常だ、ということです。一度に一つずつ剥がしていく必要があります。

イベントの既定の保持期間は 1 時間なので、古い 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 resourcesrequests の引き下げまたはノード増設
トレランスなしhad untolerated taintkubectl describe node の Taintstolerations の追加またはテイントの削除
セレクター・アフィニティの不一致didn't match Pod's node affinity/selectorkubectl get nodes --show-labelsノードへのラベル付与または条件の緩和
PVC の未バインドpod has unbound immediate PersistentVolumeClaimskubectl describe pvcStorageClass の存在とプロビジョナーの確認
ボリュームのゾーン不一致volume node affinity conflictPV の 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 エグゼキューター 2 個が CPU 3 コアを予約し、実際にはほとんど使っていません。

ここで 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 程度まで下げれば、ノード 1 台がまるごと空きます。

ノードが受け入れてくれない場合

テイントとトレランス

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

2 番目のノードだけテイントがありません。3 番目のノードのテイントは人が付けたものではなく、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 のノードはありますが、該当ゾーンにはノードが 1 台もありません。ゾーンの条件が誤っているか、そのゾーンのノードグループが 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 台なら、3 個目の 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 が暇そうに見えるからといって、ノードに空きがあるわけではありません。