Kubernetes の Disk Pressure Eviction: どの領域が埋まり、何が回収され、どの Pod が退避されるか
ノードの空きが nodefs.available<10% などの閾値を割ると、kubelet はまず Container GC と Image GC で回収を試み、それでも足りなければ Pod を 1 つずつ退避する。退避の順番は ephemeral-storage の request を超えているかで決まるので、request を実際の使用量に合わせて書いておくのがいちばん効く。
どんなときに使う?
- Pod が
Evicted(The node was low on resource: ephemeral-storage)になった原因を調べたい - ノードに
node.kubernetes.io/disk-pressureの Taint が付いて新しい Pod が乗らない - オンプレや自前クラスタで、ディスク構成(imagefs を分けるか)や kubelet の閾値を決めたい
メモリ側の OOMKilled と Eviction の違いは Kubernetes の「メモリ使用量」は一つではない: OOMKilled と Eviction の見分け方 にまとめてある。まず kubectl describe pod の status と Event で、コンテナの終了理由(OOM)なのか kubelet による退避なのかを切り分ける。
3 つのディスク区分
| 区分 | 中身 | 確かめ方 |
|---|---|---|
nodefs | /var/lib/kubelet(disk-backed な emptyDir など)、/var/log/pods のログ | df -h / |
imagefs | コンテナイメージと Writable Layer(containerd なら root 配下の snapshotter) | df -h /var/lib/containerd が / と同じデバイスなら専用ではない |
containerfs | Writable Layer だけの区分。containerd では分離できない(kubelet の CRI Stats Provider 側も未対応) | — |
imagefs を専用にする設定は kubelet ではなくランタイム側にある(containerd の config.toml の root を別ディスクに置く)。kubelet は起動時にデバイス番号を比べて自動判定する。
Pod の ephemeral-storage に数えられるもの
ephemeral-storage に- 数える: Writable Layer、コンテナログ、disk-backed な emptyDir、
/etc/hosts、configMap ボリューム - 数えない: イメージ本体、PVC、
medium: Memoryの emptyDir(こちらはメモリ使用量として計上) - disk-backed emptyDir は
du相当で 1 分ごとに集計。削除済みでも開きっぱなしのファイルは見えないことがある - 実測は Summary API で見られる:
kubectl get --raw "/api/v1/nodes/<node>/proxy/stats/summary" \
| jq '.pods[] | select(.podRef.name=="<pod>") | .["ephemeral-storage"]'usedBytes が Pod の合計。availableBytes / capacityBytes はノードの nodefs の値で、Pod の limit までの残りではない。
閾値と判定の流れ
- 監視ループは 10 秒ごと(コード上の固定値。設定では変えられない)
- 既定の hard threshold は
nodefs.available<10%、imagefs.available<15%、nodefs.inodesFree<5%、imagefs.inodesFree<5%。soft threshold は既定なし(使うなら grace period 必須) - 閾値を割ると
DiskPressure=True。kubelet は Critical 以外の新規 Pod を拒否し、NodeLifecycleController がdisk-pressure:NoScheduleの Taint を付ける(kubelet のログだけ見ても Taint の理由は出ない) - まずノードレベル回収(停止済みコンテナの削除と、使われていないイメージの全削除)をして再評価。足りていれば Pod は退避しない
- 足りなければ Pod を退避。ノード閾値による退避は 1 ループで最大 1 Pod、hard なら猶予 1 秒
EvictionPressureTransitionPeriod(既定 5 分)は「解消後も Condition を True に保つ時間」で、True になるまで 5 分待つ設定ではない
Pod 自身の ephemeral-storage limit 超過や emptyDir の sizeLimit 超過は、ノード閾値とは別にその前に評価され、DiskPressure でなくても 1 ループで複数 Pod が退避されうる。
退避される順番(容量不足のとき)
ephemeral-storageの request を超えている Pod- Priority が低い Pod
- 「使用量 − request」が大きい Pod
request を書いていない Pod は request 0 扱いなので、少しでも書けば「超過」側に入る。QoS クラス(Guaranteed など)はディスクの退避順位には関係しない。inode 不足には inode の request がないので、この順位付けでは守れない。
ハマりどころ
--eviction-hardを一部だけ上書きすると、書かなかった signal は無効になる。 既定値を残すならmergeDefaultEvictionSettings: true- imagefs を専用にした構成では、nodefs が閾値を割っても追加の GC が走らない(reclaim 関数が空)。ログと emptyDir の側はローテーションと
sizeLimitと監視で自衛する containerLogMaxSize(既定10Mi)/containerLogMaxFiles(既定5)はコンテナ単位。ノード全体の上限はなく、10 秒ごとのポーリングなので一時的に超える。短命な Job を大量に回すとログだけで nodefs が埋まる- アプリが
/app/logs/app.logのように独自パスに書くログはローテーション対象外で、Writable Layer を際限なく食う sizeLimitや limit は書き込みを止めるものではなく、測って超過を見つけたら退避する事後チェック(tmpfs の emptyDir だけはカーネルのsize=が効く)- 退避された Pod のログは消えるので、調査用のログは Loki などへ転送しておく
対策チェックリスト
# Pod 側
resources:
requests:
ephemeral-storage: 2Gi # 実測に合わせる(退避順位とスケジューリングに効く)
limits:
ephemeral-storage: 4Gi # 暴走した Pod を検知して退避
volumes:
- name: scratch
emptyDir:
sizeLimit: 1Gi
- name: cache
emptyDir:
medium: Memory
sizeLimit: 256Mi # tmpfs はメモリを食うので必ず明示
---
# Job は終わったら消す
apiVersion: batch/v1
kind: Job
spec:
ttlSecondsAfterFinished: 3600
---
# ノード側: 使い捨てイメージが多いクラスタ
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
imageGCHighThresholdPercent: 70
imageGCLowThresholdPercent: 60
imageMaximumGCAge: 168h # 既定 0s(無効)。未使用のまま居座るイメージを掃除- ログは標準出力・標準エラーに出す。キャッシュや一時ファイルは Writable Layer ではなく emptyDir に置く(再起動のたびに前世代の Writable Layer が 1 つ残るため)
FreeDiskSpaceFailedが出ていたら、イメージ以外(ログ・ボリューム)が原因のことが多い
監視するメトリクス
- node_exporter:
node_filesystem_avail_bytes/node_filesystem_size_bytes/node_filesystem_files_free/node_filesystem_files(nodefs と imagefs のマウントポイントそれぞれ) - kube-state-metrics:
kube_node_status_condition{condition="DiskPressure",status="true"} - kubelet:
kubelet_evictions_total{eviction_signal="..."}、kubelet_image_garbage_collected_total、GC 失敗はkubelet_runtime_operations_errors_total{operation_type="remove_image"|"remove_container"} EvictionThresholdMetなどの Event はメトリクスに出ないので、event exporter かログ転送で拾う
注意
- 出典記事は kubelet v1.37.1 / containerd v2.4.1 の実装を読んだもの。既定値や挙動はバージョンやマネージド環境の追加設定で変わる
- 公式ドキュメントの「hard threshold は猶予 0 秒」「監視間隔は
housekeeping-intervalで設定可能」は、コード上はそれぞれ 1 秒・固定 10 秒
出典
- @yosshi_ · 2026-10-20
- Qiita「Kubernetes ノードのディスク領域と Disk Pressure Eviction について」: qiita.com