
ある朝、K3sクラスタの複数ノードで突然Podが次々とEvictedされ始めた。kubectl get pod -Aを叩くと、Evictedの文字がずらりと並ぶ。真っ先に疑うのはディスク容量不足だ。ところがdf -hを確認すると、どのノードも空き容量は十分にある。それでもノードはDiskPressureのままで、新しいPodをスケジュールできない。
この記事では、K3s/Kubernetes運用で見落とされがちなkubeletのeviction managerとinode枯渇の関係を、実際に起こりうるトラブルシュートの流れで解説する。ディスク容量(バイト)だけを監視していると、K3sのPodが次々Evictedされる本当の原因に気づけない。こうした「気づけないノードリソース枯渇」は、深夜のアラート対応を属人化させる典型パターンでもある。KuboのようなマネージドK3s環境を検討している場合、この記事で解説する監視ポイントがそのまま判断材料になるはずだ。
1. 犯人はdf -hには映らない。inodeという「ファイル作成能力」の枯渇

Linuxのファイルシステムは、ディスク容量(バイト)とは別にinodeという管理領域を持つ。inodeはファイルやディレクトリのメタデータ(所有者、パーミッション、データブロックの位置など)を記録する構造体で、ファイルを1つ作るたびに1つ消費される。ext4のようなファイルシステムは、フォーマット時に総容量から一定の比率(inode_ratio、デフォルト16384バイトにつき1inode)でinode総数を確保し、この数は後から変更できない。
つまり、大きなファイルを少数置いている分には余裕があっても、小さなファイルを大量に作るワークロードではバイト容量より先にinodeが枯渇する。クラウドネイティブ技術の普及団体であるCNCFが2026年10月に公開した調査では、ある実クラスタでディスク使用率83%・inode使用率67%という状態から、Node.jsのnode_modulesディレクトリ(1スナップショットあたり21,553ファイル)が複数のコンテナイメージレイヤーに重複して存在していたことが原因と特定されている(CNCF blog)。同記事の試算では、標準的なext4のinode_ratio設定の場合、ディスク使用率わずか25%の時点でinodeが枯渇しうるという。
なぜ気づきにくいのか
df -hはバイト単位の空き容量しか表示しない- 監視ダッシュボードの多くはディスク使用率(%)をバイトベースで集計している
- ログ・キャッシュ・一時ファイルなど「小さなファイルが大量にできる」処理は普段意識されにくい
2. kubeletが監視している6つのシグナルと、効かない早期警戒

Kubernetesの公式ドキュメントによると、kubeletはノードリソースの圧力を検知するために複数のeviction signalを監視している(Kubernetes公式: Node-pressure Eviction)。ディスク・inode関連だけでも次の6シグナルが存在する。
| ファイルシステム | 対象 | シグナル |
|---|---|---|
| nodefs | ノード本体の容量 | nodefs.available |
| nodefs | ノード本体のinode | nodefs.inodesFree |
| imagefs | イメージストアの容量 | imagefs.available |
| imagefs | イメージストアのinode | imagefs.inodesFree |
| containerfs | 書き込み層の容量 | containerfs.available |
| containerfs | 書き込み層のinode | containerfs.inodesFree |
クラウドネイティブ監視ツールを提供するNetdataの解説によれば、Linuxでのデフォルトのhard eviction閾値は、nodefs: 利用可能容量10%未満・inode空き5%未満、imagefs: 利用可能容量15%未満・inode空き5%未満とされている(Netdata: Kubernetes node DiskPressure)。
バイトには早期警戒があるのに、inodeには無い
ここに構造的なギャップがある。kubeletのイメージガベージコレクションは、imagefsの使用率が85%(高水位)に達した時点で不要イメージの削除を始め、80%(低水位)まで下げる、というバイトベースの早期介入が組み込まれている。ところがinodeについては、hard eviction閾値の5%未満という一点のみが監視対象で、バイトのような段階的な早期警戒が存在しない。CNCFの記事はこれを「kubeletはinodeを監視してはいるが、緊急事態になるまで動かない」と評している(CNCF blog)。
K3sもこの仕組みを継承しているため、node_modulesのような小ファイル大量生成ワークロードを動かしているK3sクラスタでは、同じ落とし穴が発生しうる。
3. df -iで診断し、hard evictionの恐さを理解する

実際にPodがEvictedされた際の調査手順は、Kubernetes向けオブザーバビリティを提供するOneUptimeの診断ガイドが具体的だ(OneUptime: DiskPressure/Inode診断ガイド)。要点は以下の通り。
# Evicted Podの詳細確認
kubectl describe pod <pod-name> -n <namespace>
kubectl get events -n <namespace> --field-selector involvedObject.name=<pod-name>
# 該当ノードのDiskPressure状態
kubectl get node <node-name> -o json | jq '.status.conditions[] | select(.type == "DiskPressure")'
# ★ここが核心: バイトとinodeを両方確認する
df -h / /var/lib/kubelet /var/log
df -i / /var/lib/kubelet /var/log
df -hでは空き容量が十分あるのにdf -iでinode使用率が100%近くになっていれば、原因はinode枯渇で確定だ。
Hard evictionの恐さ
Kubernetes公式ドキュメントが明記しているとおり、hard eviction thresholdを超えた場合、kubeletはgrace period 0秒でPodを強制終了する。しかもこのnode-pressure evictionは、通常のAPI経由のeviction(kubectl drain等)とは異なり、PodDisruptionBudgetもコンテナのterminationGracePeriodSecondsも一切尊重しない(Kubernetes公式: Node-pressure Eviction)。つまり、可用性を守るために設定したPDBがあっても、ノードリソース圧力の前では無力化される。これが「気づいたときには既に障害が広がっている」事態を招く理由だ。
さらに、Pod自身のephemeral-storage制限やemptyDirのsizeLimitを超過した場合も、Pod単位でevictionがトリガーされる(Kubernetes公式: Local ephemeral storage)。ノード全体の圧力とPod単位の制限超過、2つの経路があることを理解しておく必要がある。
4. K3s/containerdならではの落とし穴

K3sはデフォルトでcontainerdをコンテナランタイムとして使用する。containerdはイメージレイヤーを/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/配下に展開して管理しており、各番号付きディレクトリが1つのイメージレイヤーに対応する。CNCFの記事が指摘するように、node_modulesに限らずPythonの仮想環境、PHPのvendorディレクトリ、Rubyのgemなど、大量の小ファイルで構成される依存関係を含むイメージは同じ問題を起こしうる(CNCF blog)。
またK3sのGitHub issueでは、cgroup v2環境でcontainerd-shimが大量のinotifyインスタンスを生成し、「Too many open files」エラーが発生した事例も報告されている(k3s-io/k3s Issue #10020)。これはinodeそのものの枯渇とは別の問題だが、「ノードのファイル関連リソースが見えないところで枯渇する」という点で根は同じだ。ディスク容量・inode・ファイルディスクリプタの3つは、いずれも監視ダッシュボードの初期設定では見落とされやすい。
小規模なオンプレ/エッジ環境でK3sを運用している場合、ノード台数が少なく冗長性が低い分、1台のinode枯渇がクラスタ全体の可用性に直結しやすい点にも注意したい。データを外部に出せないオンプレ要件がある場合は、Kubo On-Premiseのようにノード監視込みでAir-Gapped環境に展開できる選択肢もある。
5. 再発防止策——マルチステージビルド・ログローテーション・監視

inode枯渇によるEvictedを防ぐには、イメージ設計と監視の両面から対策する必要がある。
イメージを軽くする
Dockerの公式ドキュメントが解説するマルチステージビルドを使い、ビルド専用ステージと実行用ステージを分離すれば、node_modulesのような開発時依存関係を本番イメージから除外できる(Docker公式: Multi-stage builds)。
# ビルドステージ
FROM node:22 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 実行ステージ(node_modulesを含まない)
FROM node:22-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --production
CMD ["node", "dist/index.js"]
ログとemptyDirに上限を設ける
- コンテナログのローテーション: kubeletの
containerLogMaxSize・containerLogMaxFilesを設定 - emptyDirを使うPodには
sizeLimitを必ず指定し、無制限の小ファイル生成を防ぐ
監視とクリーンアップ
node_filesystem_files_freeのようなinode関連メトリクスを、バイト使用率とは別にアラート設定する(80%の時点で警戒するのが目安)- 緊急時は
crictl rmi --pruneで未使用イメージを一括削除できる。ある事例ではディスク使用率71%から20%まで削減できたと報告されている(virtualizationhowto.com: Kubernetesクラスタのディスク容量解放)
これらの対策は単発のコマンドではなく、「ビルド時に小ファイルを持ち込まない」「運用中は上限とアラートで気づけるようにする」という2段構えの設計が肝になる。KuboのようなマネージドK3s環境では、この手のノードリソース監視をCaptain UI上で可視化し、inode使用率を含めたアラートをあらかじめ組み込んだ状態で提供している。自前でPrometheusのアラートルールを設計・運用する手間を省きたい場合は検討の余地がある。
まとめ
K3s/Kubernetesの運用で「ディスクは余っているのにPodがEvictedされる」という現象に遭遇したら、まず疑うべきはdf -hでは見えないinode枯渇だ。kubeletはnodefs/imagefs/containerfsの計6シグナルを監視しているが、inodeに対してはバイトのような段階的な早期警戒が組み込まれておらず、hard evictionに達するとPodDisruptionBudgetも無視してPodを強制終了する。
K3sではcontainerdのoverlayfsスナップショットが小ファイル中心のイメージで肥大化しやすいため、マルチステージビルドでの依存関係除外、ログローテーション、emptyDirのsizeLimit設定、inodeメトリクスの監視という対策を組み合わせておきたい。
こうしたノードリソースの可視化・監視を自前で構築する負荷が気になる場合は、Kubo Cloudでのマネージド運用も選択肢になる。ディスク容量に加えてinode使用率までカバーした監視を、追加のPrometheus設計なしで利用できる。K3sベースでEKS/AKS/GKEと比べてもコスト効率よく、本格的なKubernetesの運用をそのまま継続できる点も検討材料になるはずだ。気になる場合はお問い合わせから相談してみてほしい。