Kubernetes の inode 枯渇:なぜディスク容量のアラートは本当の脅威を見逃すのか
kubelet には inode 枯渇に対する早期警告機能がなく、ディスク容量に余裕があるように見えても突然 Pod が退避されることがあります。コンテナイメージ内の小さなファイルがどのようにこのサイレント障害を引き起こすのかを学びましょう。
英語の原文から自動翻訳されました。
ある Kubernetes ワーカーノードで、ファイルシステムが満杯になりつつあるという重大なアラートが発生しました。しかし、標準的な診断ツールでは十分な空きディスク容量が表示されていました。原因はバイト単位の使用量ではなく、inode(iノード)の枯渇でした。これは kubelet がストレージ容量のように積極的には監視せず、危機的状況に陥った時点で初めて検知するリソース制限です。
何が起きたか
NodeFilesystemFilesFillingUp アラートを調査していた SRE は、矛盾した状態を発見しました。ディスク使用率は 83% であった一方、inode テーブルの使用率は 67% で、残り 530 万個の空き inode がありました。アラートが発火したのは、inode 消費の絶対値ではなく、その消費傾向(トラジェクトリー)によるものでした。ディスク使用量は「バックグラウンドでのガベージコレクション」と「ハード退避」の 2 つの仕組みで監視されていますが、inode に対しては「ハード退避」という唯一の防御手段しかありません。
kubelet のイメージガベージコレクションは、バイト単位の使用量が 85% を超えたときに実行され、未使用のイメージを削除して使用量を 80% に下げます。しかし、このプロセスはファイル数を完全に無視しています。Linux では、kubelet は nodefs.inodesFree と imagefs.inodesFree を監視していますが、空き inode が 5% を下回った場合にのみアクションを実行します。つまり、inode 圧力に対する最初の自動対応は、最も破壊的なもの、すなわちノードを救うために実行中の Pod を退避させることになります。
調査の結果、このノードはログの肥大化やボリュームリークに苦しんでいるわけではありませんでした。inode の減少は containerd のスナップショットストアから来ていました。展開された各イメージレイヤーはファイルのディレクトリを作成し、数千もの小さなファイルを含むイメージは inode を急速に消費します。今回のケースでは、単一の Node.js パッケージがスナップショットあたり 21,000 以上のファイルを追加していました。同じイメージの複数のバージョンがノード上に保持されていたため、ディスク容量には比較的余裕があったにもかかわらず、数百万個の inode が消費されました。
仕組み
ファイルシステムは作成時に inode 比率に基づいて inode を割り当てます。ext4 では通常、容量 16,384 バイトごとに 1 つの inode が割り当てられます。この数は固定されており、後から増やすことはできません。小さなファイルは非効率です。空でない各ファイルは少なくとも 4 KiB ブロックと 1 つの inode を消費するためです。もしファイルシステムが 1 バイトのファイルで埋め尽くされた場合、ディスク容量の 25% しか使用されていない段階で inode が枯渇することになります。
インシデントが発生したノードでは、計算上、約 8,192 バイトごとに 1 つの inode というより厳しい inode 比率が示唆されました。これはおそらく初期プロビジョニング時に設定されたものです。この調整を行っても、小さなファイルで埋め尽くされた場合、ディスク使用率 50% で inode が枯渇します。containerd の overlayfs スナップショットターは、各固有のレイヤーを独自のディレクトリに展開します。圧縮されたレイヤーはダイジェストによって重複排除されますが、展開されたスナップショット間では何も共有されません。もし 2 つのビルドが異なるダイジェストを持つレイヤーを生成した場合(タイムスタンプの変更などによる)、containerd はすべてのファイルの完全なコピーを 2 部保存します。
kubelet はバイト単位の使用量と inode 単位の使用量を相関させていません。アクションを起こす前に 5% の空き inode しきい値を待ちます。その頃には、ノードは緊急モードになっています。inode に対する中間的な「ソフト」退避やガベージコレクションステップがないため、エンジニアは Pod の安定性が危険にさらされるまで警告を受け取れません。
重要な詳細
- kubelet のイメージガベージコレクションはバイト使用量 85% でトリガーされますが、inode 使用量には同等なしきい値がありません。
- inode のハード退避は、空き inode が 5% を下回ったときのみ有効になるため、最後の手段となります。
- Ext4 ファイルシステムのカスタマイズがない限り、作成時に決定される固定の inode 数を持ちます。通常は 16,384 バイトごとに 1 つです。
- containerd は各ユニークなレイヤーダイジェストを個別のスナップショットディレクトリに展開し、バージョン間で小さなファイルを複製します。
@mui/icons-materialなどの単一の Node.js パッケージは、単一のイメージレイヤーに 21,000 以上のファイルを追加することがあります。du --inodes -xS /var | sort -rhを使用すると、ファイルシステム境界を越えずに、ファイル数が多いディレクトリを特定できます。
なぜ重要なのか
インフラストラクチャエンジニアにとって、この挙動は標準的な監視における盲点を露呈させます。多くのチームはディスク使用率のパーセンテージを厳密に追跡し、80% 以下であれば安全だと仮定しています。しかし、大きな node_modules、Python 仮想環境、ベンダー依存関係など、多数の小さなファイルを生成するアプリケーションは、ディスク容量がクリティカルになるずっと前に inode を枯渇させる可能性があります。これにより、ストレージメトリクスとは無関係に見える予期せぬ Pod 退避やノードの不安定性が発生します。
根本原因はしばしばクラスター設定ではなく、ビルドプラクティスにあります。ソースコードと依存関係を最終イメージにコピーする単一ステージの Dockerfile は、小さなファイルで詰まったレイヤーを作成します。適切な .dockerignore ルールやマルチステージビルドがない場合、すべての CI 実行でタイムスタンプの変更に伴う新しいレイヤーダイジェストが生成され、ノードはこれらのファイル過多なレイヤーの複数のコピーを保持せざるを得なくなります。このメカニズムを理解することで、チームは反応的なノードデバッグから、積極的なイメージ最適化へと焦点を移すことができます。
対処法
- レジストリへプッシュする前に、ビルド成果物に対して
du --inodesを使用して、コンテナイメージ内の高いファイル数を監査してください。 - マルチステージ Dockerfile を実装し、コンパイル済みアーティファクトと本番用依存関係のみが最終イメージに含まれるようにしてください。
.dockerignoreを使用して、COPY指示子からnode_modules、.git、ビルドキャッシュを除外し、不要なファイルの複製を防いでください。- ディスク使用量とともに inode 使用量を監視してください。



