テック技報最新号へ →
インフラ

Kubernetes の Disk Pressure Eviction: どの領域が埋まり、何が回収され、どの Pod が退避されるか

ノードの空きが nodefs.available<10% などの閾値を割ると、kubelet はまず Container GC と Image GC で回収を試み、それでも足りなければ Pod を 1 つずつ退避する。退避の順番は ephemeral-storage の request を超えているかで決まるので、request を実際の使用量に合わせて書いておくのがいちばん効く。

@yosshi_ · 2026-10-20
#infra#kubernetes#kubelet#eviction#ephemeral-storage#containerd#troubleshooting

どんなときに使う?

  • 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 が / と同じデバイスなら専用ではない
containerfsWritable Layer だけの区分。containerd では分離できない(kubelet の CRI Stats Provider 側も未対応)—

imagefs を専用にする設定は kubelet ではなくランタイム側にある(containerd の config.toml の root を別ディスクに置く)。kubelet は起動時にデバイス番号を比べて自動判定する。

Pod の ephemeral-storage に数えられるもの

  • 数える: Writable Layer、コンテナログ、disk-backed な emptyDir、/etc/hosts、configMap ボリューム
  • 数えない: イメージ本体、PVC、medium: Memory の emptyDir(こちらはメモリ使用量として計上)
  • disk-backed emptyDir は du 相当で 1 分ごとに集計。削除済みでも開きっぱなしのファイルは見えないことがある
  • 実測は Summary API で見られる:
bash
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 までの残りではない。

閾値と判定の流れ

  1. 監視ループは 10 秒ごと(コード上の固定値。設定では変えられない)
  2. 既定の hard threshold は nodefs.available<10%、imagefs.available<15%、nodefs.inodesFree<5%、imagefs.inodesFree<5%。soft threshold は既定なし(使うなら grace period 必須)
  3. 閾値を割ると DiskPressure=True。kubelet は Critical 以外の新規 Pod を拒否し、NodeLifecycleController が disk-pressure:NoSchedule の Taint を付ける(kubelet のログだけ見ても Taint の理由は出ない)
  4. まずノードレベル回収(停止済みコンテナの削除と、使われていないイメージの全削除)をして再評価。足りていれば Pod は退避しない
  5. 足りなければ Pod を退避。ノード閾値による退避は 1 ループで最大 1 Pod、hard なら猶予 1 秒
  6. EvictionPressureTransitionPeriod(既定 5 分)は「解消後も Condition を True に保つ時間」で、True になるまで 5 分待つ設定ではない

Pod 自身の ephemeral-storage limit 超過や emptyDir の sizeLimit 超過は、ノード閾値とは別にその前に評価され、DiskPressure でなくても 1 ループで複数 Pod が退避されうる。

退避される順番(容量不足のとき)

  1. ephemeral-storage の request を超えている Pod
  2. Priority が低い Pod
  3. 「使用量 − 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 などへ転送しておく

対策チェックリスト

yaml
# 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 秒

出典

X の元の投稿を開く