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

- Name
- Youngju Kim
- @fjvbn20031
はじめに — 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 memory | kubectl describe node の Allocated resources | requests の引き下げまたはノード増設 |
| トレランスなし | had untolerated taint | kubectl describe node の Taints | tolerations の追加またはテイントの削除 |
| セレクター・アフィニティの不一致 | didn't match Pod's node affinity/selector | kubectl get nodes --show-labels | ノードへのラベル付与または条件の緩和 |
| PVC の未バインド | pod has unbound immediate PersistentVolumeClaims | kubectl describe pvc | StorageClass の存在とプロビジョナーの確認 |
| ボリュームのゾーン不一致 | volume node affinity conflict | PV の nodeAffinity を確認 | 同じゾーンにノードを確保 |
| Pod アンチアフィニティによる自己排除 | didn't match pod anti-affinity rules | レプリカ数とトポロジー数を比較 | preferred に緩和またはノードを追加 |
| 分散制約の違反 | didn't match pod topology spread constraints | kubectl get pods -o wide で現在の分布を確認 | ScheduleAnyway に緩和、maxSkew を調整 |
| ノードあたりの Pod 数上限 | Too many pods | kubectl get node -o jsonpath で allocatable.pods を照会 | CNI 設定の変更またはノード増設 |
| ノードの NotReady・cordon | node(s) were unschedulable | kubectl 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 が暇そうに見えるからといって、ノードに空きがあるわけではありません。