Cloud & Infrastruktur

Kubernetes-Inode-Erschöpfung: Warum Warnungen zum Speicherplatz die eigentliche Bedrohung übersehen

Kubelet bietet keine Frühwarnung bei Inode-Verknappung, was zu plötzlichen Pod-Evictions führt, selbst wenn ausreichend Speicherplatz vorhanden scheint. Erfahren Sie, wie kleine Dateien in Container-Images dieses stille Versagen auslösen.

Illustration comparing small numerous items versus large few items to represent inodes versus disk space
Für diesen Artikel generierte Illustration

Automatisch aus dem englischen Original übersetzt.

Ein Kubernetes-Worker-Knoten hat kürzlich eine kritische Warnung ausgelöst, dass das Dateisystem voll wird, obwohl die Standarddiagnose reichlich verfügbaren Speicherplatz anzeigte. Der Übeltäter war nicht die Byte-Nutzung, sondern die Erschöpfung der Inodes – eine Ressourcenbegrenzung, die Kubelet nur im Krisenfall überwacht, an sie proaktiv wie die Speicherkapazität zu verwalten.

Was passiert ist

Ein SRE, der eine NodeFilesystemFilesFillingUp-Warnung untersuchte, fand einen widersprüchlichen Zustand vor: Die Festplatte war zu 83 % voll, aber die Inode-Tabelle war nur zu 67 % belegt, mit lediglich 5,3 Millionen freien Inodes. Die Warnung wurde aufgrund des Trends des Inode-Verbrauchs ausgelöst, nicht aufgrund des absoluten Pegels. Während die Speichernutzung mit zwei Mechanismen überwacht wird – Hintergrund-Garbage-Collection und harte Eviction –, haben Inodes nur eine Verteidigungslinie: die harte Eviction.

Die Image-Garbage-Collection von Kubelet läuft, wenn die Byte-Nutzung 85 % überschreitet, und löscht ungenutzte Images, um die Nutzung auf 80 % zu senken. Dieser Prozess ignoriert jedoch die Anzahl der Dateien vollständig. Unter Linux überwacht Kubelet zwar nodefs.inodesFree und imagefs.inodesFree, löst aber erst dann Maßnahmen aus, wenn die freien Inodes unter 5 % fallen. Das bedeutet, dass die erste automatisierte Reaktion auf Inode-Belastung auch die disruptivste ist: die Eviction laufender Pods zur Rettung des Knotens.

Die Untersuchung ergab, dass der Knoten nicht unter Log-Aufblähung oder Volume-Leaks litt. Stattdessen stammte der Inode-Verbrauch vom Snapshot-Speicher von containerd. Jede ausgepackte Image-Ebene erstellt ein Verzeichnis mit Dateien, und Images, die Tausende kleiner Dateien enthalten, verbrauchen schnell Inodes. In diesem Fall trug ein einzelnes Node.js-Paket mehr als 21.000 Dateien pro Snapshot bei. Da mehrere Versionen desselben Images auf dem Knoten beibehalten wurden, wurden Millionen von Inodes verbraucht, während der Speicherplatz relativ verfügbar blieb.

Wie es funktioniert

Dateisysteme weisen Inodes zum Zeitpunkt der Erstellung basierend auf einem Inode-Verhältnis zu, typischerweise ein Inode pro 16.384 Bytes Kapazität bei ext4. Diese Zahl ist festgelegt; sie kann später nicht erhöht werden. Kleine Dateien sind ineffizient, da jede nicht leere Datei mindestens einen 4 KiB-Block und einen Inode verbraucht. Wenn ein Dateisystem mit Ein-Byte-Dateien gefüllt ist, erschöpfen sich die Inodes bereits bei einer Nutzung von nur 25 % des Speicherplatzes.

Im betroffenen Knoten deutete die Berechnung auf ein engeres Inode-Verhältnis von etwa 8.192 Bytes pro Inode hin, wahrscheinlich konfiguriert während der initialen Bereitstellung. Selbst mit dieser Anpassung würden sich die Inodes bei 50 % Speichernutzung erschöpfen, wenn sie mit kleinen Dateien gefüllt wären. Der overlayfs-Snapshotter von containerd packt jede eindeutige Ebene in ihr eigenes Verzeichnis aus. Er dedupliziert komprimierte Ebenen nach Digest, teilt aber nichts zwischen ausgepackten Snapshots. Wenn zwei Builds Ebenen mit unterschiedlichen Digests erzeugen – sogar aufgrund von Zeitstempeländerungen –, speichert containerd zwei vollständige Kopien jeder Datei.

Kubelet korreliert die Byte-Nutzung nicht mit der Inode-Nutzung. Es wartet auf den Schwellenwert von 5 % freien Inodes, bevor es handelt. Zu diesem Zeitpunkt befindet sich der Knoten bereits im Notfallmodus. Das Fehlen eines Zwischenschritts für eine „weiche“ Eviction oder Garbage-Collection für Inodes bedeutet, dass Ingenieure keine Warnung erhalten, bis die Pod-Stabilität gefährdet ist.

Wichtige Details

  • Die Image-Garbage-Collection von Kubelet wird bei 85 % Byte-Nutzung ausgelöst, hat aber keinen entsprechenden Schwellenwert für die Inode-Nutzung.
  • Die harte Eviction für Inodes aktiviert sich erst, wenn die freien Inodes unter 5 % fallen, was sie zu einer Maßnahme der letzten Wahl macht.
  • Ext4-Dateisysteme haben eine feste Inode-Anzahl, die bei der Erstellung bestimmt wird, typischerweise eine pro 16.384 Bytes, sofern nicht angepasst.
  • Containerd entpackt jeden eindeutigen Layer-Digest in ein separates Snapshot-Verzeichnis und dupliziert dabei kleine Dateien über verschiedene Versionen hinweg.
  • Ein einzelnes Node.js-Paket wie @mui/icons-material kann mehr als 21.000 Dateien zu einer einzigen Image-Ebene beitragen.
  • Die Verwendung von du --inodes -xS /var | sort -rh hilft dabei, Verzeichnisse mit hoher Dateianzahl zu identifizieren, ohne Dateisystemgrenzen zu überschreiten.

Warum das wichtig ist

Für Infrastruktur-Ingenieure deckt dieses Verhalten eine Blindstelle im Standard-Monitoring auf. Die meisten Teams verfolgen die Prozentsätze der Speichernutzung genau und gehen davon aus, dass ein Wert unter 80 % Sicherheit gewährleistet. Anwendungen, die viele kleine Dateien generieren – wie solche mit großen node_modules, Python-Virtual-Environments oder vendored Dependencies –, können jedoch die Inodes lange vor dem kritischen Speicherplatzverbrauch erschöpfen. Dies führt zu unerwarteten Pod-Evictions und Knoteninstabilitäten, die scheinbar nichts mit den Speichermetriken zu tun haben.

Die Ursache liegt oft in den Build-Praktiken und nicht in der Cluster-Konfiguration. Dockerfiles mit nur einer Stufe, die Quellcode und Abhängigkeiten in das finale Image kopieren, erstellen Ebenen, die mit kleinen Dateien gepackt sind. Ohne ordnungsgemäße .dockerignore-Regeln oder Multi-Stage-Builds kann jeder CI-Lauf neue Layer-Digests aufgrund von Zeitstempeländerungen erzeugen, was Knoten dazu zwingt, mehrere Kopien dieser dateireichen Ebenen beizubehalten. Das Verständnis dieses Mechanismus ermöglicht es Teams, den Fokus vom reaktiven Debugging von Knoten auf die proaktive Image-Optimierung zu verlagern.

Was Sie tun können

  • Überprüfen Sie Ihre Container-Images auf hohe Dateianzahlen, indem Sie du --inodes auf gebauten Artefakten verwenden, bevor Sie sie in Registries pushen.
  • Implementieren Sie Multi-Stage-Dockerfiles, um sicherzustellen, dass nur kompilierte Artefakte und Produktionsabhängigkeiten das finale Image erreichen.
  • Verwenden Sie .dockerignore, um node_modules, .git und Build-Caches von COPY-Anweisungen auszuschließen, um unnötige Dateiduplizierung zu verhindern.
  • Überwachen Sie die Inode-Nutzung parallel zum Speicherplatz in Ihrem Observability-Stack.

Tools aus dem Bytechap-Shop

Weiterlesen

Alle Artikel