'No space left on device' s 40% voľného disku: Inode a OverlayFS Death Spiral
Inody nam dosli davno pred diskom, a bolelo to. “Postgres padá s ‘No space left on device’ ale máme 40% voľného disku.” Inžinier na pohotovosti skontroloval df -h—veľa miesta. Skontrolovali PersistentVolumeClaims—hlboko pod kapacitou. Dokonca skontrolovali du -sh /* na node—nič nezvyčajné. Ale PostgreSQL kontajner stále padal v crash loop, logujúc zlyhania zápisu do WAL súborov.
Pripojil som sa na node cez SSH a spustil df -i. Využitie inodov: 98%. Vtedy všetko zapadlo. Node filesystem mal veľa voľných bajtov, ale takmer žiadne inody. Každý súbor, adresár a symlink spotrebuje jeden inode, bez ohľadu na jeho veľkosť. Vyčerpali sme našu alokáciu inodov zatiaľ čo sme sotva dotkli alokáciu bajtov—a žiadny z našich monitoring dashboardov nesledoval využitie inodov.
Vinníkom bol logging sidecar ktorý zapisoval individuálne JSON súbory pre každý log záznam namiesto appendovania do jedného súboru. V priebehu týždňov vytvoril milióny malých súborov v overlayfs vrstvách. Súbory samotné boli malé—celkovo možno 500MB—ale spotrebovali 12 miliónov inodov. Keď sa Postgres pokúsil vytvoriť nový WAL segment, kernel vrátil ENOSPC aj keď bolo 40GB voľného miesta. Z pohľadu Postgresu to vyzeralo ako disk corruption alebo zlyhanie storage systému.
Čo to robilo obzvlášť zákerným bol overlayfs amplifikačný efekt. Container images sú zložené z vrstiev, a každá vrstva udržuje svoje vlastné inode účtovníctvo. Kontajner s mnohými vrstvami, spúšťajúci workloady ktoré vytvárajú veľa malých súborov, môže vyčerpať inody prekvapivo rýchlo.
Prostredie: Kubernetes 1.28+, containerd s overlayfs, nody spúšťajúce efemérne workloady, logging a debugging nástroje
Pochopenie vyčerpania Inodov
Bajty vs Inody
Filesystem resources sú dvojdimenzionálne:
Bajty (čo df -h ukazuje):
┌─────────────────────────────────────┐
│████████████████████░░░░░░░░░░░░░░░░│ 60% použité
└─────────────────────────────────────┘
Sleduje: Koľko dát je uložených
Inody (čo df -i ukazuje):
┌─────────────────────────────────────┐
│███████████████████████████████████░│ 98% použité ← PROBLÉM!
└─────────────────────────────────────┘
Sleduje: Koľko súborov/adresárov existuje
Jeden inode na:
- Bežný súbor (akákoľvek veľkosť, 1 bajt alebo 1TB)
- Adresár
- Symbolický link
- Named pipe
- Socket súbor
1KB log súbor a 1GB video oboje používajú presne 1 inode.
Ako sa stane vyčerpanie Inodov
Bežné vzory ktoré vyčerpávajú inody:
1. Veľa malých súborov (per-request logy, temp súbory)
10 miliónov 100-bajtových súborov = 10M inodov + ~1GB bajtov
2. Container image churn (veľa vrstiev naskladaných)
Každá image vrstva má vlastné súbory
100 images × 10,000 súborov/image = 1M inodov
3. Debug/profiling artefakty
Core dumps, heap dumps, trace súbory
pprof profily vytvárajúce tisíce temp súborov
4. Package managery mimo kontroly
node_modules, pip downloads, maven caches
npm install vytvárajúci 50,000+ súborov
5. Log rotácia bez cleanup
logrotate vytvárajúci očíslované súbory navždy
Každý rotovaný súbor: 1 inode
OverlayFS Amplifikácia
Štruktúra container overlayfs:
Dolné vrstvy (read-only, z image):
├── layer1/ (base image: 5,000 súborov)
├── layer2/ (dependencies: 20,000 súborov)
└── layer3/ (application: 2,000 súborov)
Horná vrstva (zapisovateľná, container runtime):
└── upper/ (container zápisy idú sem)
Merged view (čo kontajner vidí):
└── merged/ (únia všetkých vrstiev)
Problémové scenáre:
1. Modifikácia súboru z dolnej vrstvy:
- Copy-up do hornej vrstvy = nový inode v hornej
- Originálny inode v dolnej stále existuje
- 1 logický súbor = 2 spotrebované inody
2. Vytváranie veľa temp súborov:
- Všetky idú do hornej vrstvy
- Každý temp súbor = 1 inode
- Zmazanie súboru vytvorí "whiteout" (ďalší inode!)
3. Veľa bežiacich kontajnerov:
- Každý kontajner má vlastnú hornú vrstvu
- Horné vrstvy sú na root filesystéme node
- Všetky kontajnery zdieľajú rovnaký pool inodov
Diagnostika vyčerpania Inodov
Skontroluj využitie Inodov
# Základná kontrola inodov
df -i
# Výstup:
# Filesystem Inodes IUsed IFree IUse% Mounted on
# /dev/sda1 12582912 12345678 237234 98% /
# ↑ NEBEZPEČIE!
# Ak IUse% > 90%, si v nebezpečnej zóne
# Ak IUse% = 100%, kontajnery budú padať s ENOSPC
# Skontroluj špecifické mount pointy
df -i /var/lib/containerd
df -i /var/lib/docker
df -i /var/lib/kubelet
Nájdi Inode-Heavy adresáre
# Počítaj súbory rekurzívne (pomalé ale presné)
find /var/lib/containerd -type f | wc -l
# Rýchlejšie: Použi filesystem debugging nástroje
# Pre ext4:
debugfs -R "stats" /dev/sda1 | grep -i inode
# Nájdi adresáre s najviac súbormi
for dir in /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/*/fs; do
echo "$(find "$dir" -type f 2>/dev/null | wc -l) $dir"
done | sort -rn | head -20
# Nájdi kontajnery s najviac súbormi v hornej vrstve
for upper in /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/*/fs; do
if [ -d "$upper" ]; then
count=$(find "$upper" -type f 2>/dev/null | wc -l)
echo "$count $upper"
fi
done | sort -rn | head -10
Identifikuj vinníka
# Nájdi ktorý kontajner/pod vytvára súbory
# Skontroluj overlay mount info
mount | grep overlay
# Získaj container ID z mount cesty
# /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/12345/fs
# 12345 je snapshot ID
# Mapuj snapshot na kontajner
crictl ps -o json | jq -r '.containers[] | "\(.id) \(.metadata.name)"'
# Skontroluj aktuálny počet súborov kontajnera
CONTAINER_ID="abc123"
ROOTFS=$(crictl inspect $CONTAINER_ID | jq -r '.info.runtimeSpec.root.path')
find $ROOTFS -type f | wc -l
# Sleduj vytváranie súborov v reálnom čase
inotifywait -m -r /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/ \
-e create -e delete 2>/dev/null | head -100
Oprava
Okamžite: Vyčisti Inody
# Zmaž staré container images (na node)
crictl rmi --prune
# Odstráň zastavené kontajnery
crictl rm $(crictl ps -a -q --state exited)
# Vyčisti containerd snapshots
# VAROVANIE: Rob to len počas maintenance window
ctr -n k8s.io snapshots rm <snapshot-id>
# Vynúť garbage collection
crictl gc
# Nájdi a zmaž očividný temp file spam
find /var/lib/containerd -name "*.tmp" -mtime +1 -delete
find /var/lib/containerd -name "*.log" -size 0 -delete
Oprav Aplikáciu
# Zlé: Jeden súbor per log záznam
for event in events:
with open(f'/logs/event_{event.id}.json', 'w') as f:
json.dump(event, f)
# Vytvára milióny súborov
# Dobré: Appenduj do jedného súboru
with open('/logs/events.jsonl', 'a') as f:
for event in events:
f.write(json.dumps(event) + '\n')
# Vytvára 1 súbor
# Lepšie: Použi structured logging do stdout
import logging
import json
logger = logging.getLogger()
for event in events:
logger.info(json.dumps(event))
# Vytvára 0 súborov (logy idú do container runtime)
// Zlé: Temp súbor per request
func handleRequest(r *Request) {
f, _ := ioutil.TempFile("", "request-*")
// Spracuj s temp súborom
// Zabudni zmazať
}
// Dobré: Použi pamäť alebo jeden temp súbor s cleanup
func handleRequest(r *Request) {
f, _ := ioutil.TempFile("", "request-*")
defer os.Remove(f.Name()) // Vždy uprac
defer f.Close()
// Spracuj
}
// Lepšie: Použi in-memory buffer
func handleRequest(r *Request) {
buf := bytes.NewBuffer(nil)
// Spracuj v pamäti
}
Kubernetes Resource Limits
# Limituj ephemeral storage (zahŕňa inody nepriamo)
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
resources:
limits:
ephemeral-storage: "2Gi" # Celkové ephemeral storage
requests:
ephemeral-storage: "1Gi"
# Kubelet evictne pod ak prekročí limit
# Použi emptyDir s size limitom pre temp súbory
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
volumeMounts:
- name: temp
mountPath: /tmp
volumes:
- name: emptyDir
emptyDir:
sizeLimit: 500Mi # Ohraničuje celkovú veľkosť
# Poznámka: NEohraničuje počet inodov priamo
Prevencia na úrovni Node
# Zvýš počet inodov pri formátovaní filesystému
# (Možné len pri vytvorení filesystému)
mkfs.ext4 -N 50000000 /dev/sda1 # 50 miliónov inodov
# Pre existujúce nody: Použi XFS namiesto ext4
# XFS alokuje inody dynamicky, ťažšie sa vyčerpajú
mkfs.xfs /dev/sda1
# Konfiguruj containerd na použitie separátneho filesystému
# /etc/containerd/config.toml
[plugins."io.containerd.grpc.v1.cri".containerd]
snapshotter = "overlayfs"
# Daj snapshots na dedikovaný volume s viac inodmi
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
root = "/data/containerd" # Separátny volume
Image Hygiena
# Zlé: Veľa vrstiev s veľa súbormi
FROM node:18
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
RUN npm prune --production
# Výsledok: node_modules vo viacerých vrstvách = inode explózia
# Dobré: Multi-stage build, minimálny finálny image
FROM node:18 AS builder
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:18-slim
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
# Výsledok: Menej vrstiev, menej duplikátnych súborov
Monitoring
Prometheus Alerty
groups:
- name: inode-exhaustion
rules:
- alert: InodeVyuzitiVysoke
expr: |
(1 - node_filesystem_files_free{mountpoint="/"}
/ node_filesystem_files{mountpoint="/"}) > 0.85
for: 10m
labels:
severity: warning
annotations:
summary: "Využitie inodov >85% na {{ $labels.instance }}"
description: "Skontroluj temp file spam alebo container image churn"
- alert: InodeVyuzitiKriticke
expr: |
(1 - node_filesystem_files_free{mountpoint="/"}
/ node_filesystem_files{mountpoint="/"}) > 0.95
for: 5m
labels:
severity: critical
annotations:
summary: "Využitie inodov >95% na {{ $labels.instance }}"
description: "Bezprostredné ENOSPC pre vytváranie nových súborov"
- alert: InodeVsBytesNesulad
expr: |
# Vysoké využitie inodov ale nízke využitie bajtov = veľa malých súborov
(1 - node_filesystem_files_free{mountpoint="/"}
/ node_filesystem_files{mountpoint="/"}) > 0.8
AND
(1 - node_filesystem_avail_bytes{mountpoint="/"}
/ node_filesystem_size_bytes{mountpoint="/"}) < 0.5
for: 30m
labels:
severity: warning
annotations:
summary: "Nesúlad inodov/bajtov na {{ $labels.instance }}"
description: "Detekované veľa malých súborov - vyšetri temp file spam"
Grafana Dashboard Queries
# Percentuálne využitie inodov
(1 - node_filesystem_files_free{mountpoint="/"} / node_filesystem_files{mountpoint="/"}) * 100
# Voľné inody (raw počet)
node_filesystem_files_free{mountpoint="/"}
# Miera využívania inodov (trendujeme k vyčerpaniu?)
deriv(node_filesystem_files_free{mountpoint="/"}[1h])
# Porovnanie bajtov vs inodov
# Panel 1: Využitie bajtov %
(1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100
# Panel 2: Využitie inodov %
(1 - node_filesystem_files_free{mountpoint="/"} / node_filesystem_files{mountpoint="/"}) * 100
# Ak inode % >> byte %, máš veľa malých súborov
Checklist
## Prevencia a reakcia na vyčerpanie Inodov
### Detekcia
- [ ] Skontroluj df -i (nie len df -h)
- [ ] Porovnaj inode% vs byte% (nesúlad = veľa malých súborov)
- [ ] Nájdi adresáre s najviac súbormi
- [ ] Identifikuj kontajner/pod vytvárajúci súbory
### Okamžitá oprava
- [ ] Vyčisti temp súbory a logy
- [ ] Prune nepoužívané container images
- [ ] Zmaž staré snapshots
- [ ] Reštartuj problémové workloady
### Prevencia
- [ ] Pridaj monitoring inodov do dashboardov
- [ ] Alert na využitie inodov > 85%
- [ ] Nastav ephemeral storage limity na podoch
- [ ] Použi multi-stage Docker builds
### Opravy aplikácie
- [ ] Loguj do stdout, nie do individuálnych súborov
- [ ] Čisti temp súbory po použití
- [ ] Použi append-mode pre logy, nie file-per-entry
- [ ] Limituj cache/temp adresáre
Záver
Vyčerpanie inodov je jeden z tých failure módov ktorý sa zdá že by sa nemal stávať v 2024. Je to old-school Unix problém s ktorým sa moderní inžinieri zriedka stretávajú—kým kontajnery ho neurobia znova relevantným. Overlayfs storage driver, kombinovaný s workloadmi ktoré generujú veľa malých súborov, môže vyčerpať inody prekvapivo rýchlo. A pretože väčšina monitoring dashboardov sleduje len využitie bajtov, problém je neviditeľný kým kontajnery nezačnú zlyhávať s kryptickými ENOSPC chybami.
Základný problém je že df -h klame vynechaním. Ukazuje ti využitie bajtov, ktoré je takmer vždy v poriadku. Neukazuje ti využitie inodov, ktoré môže byť na 98%. Keď sa Postgres pokúsi vytvoriť nový WAL súbor a dostane ENOSPC, žiadne pozeranie na “40% voľného disku” ti nepomôže diagnostikovať to. Musíš vedieť spustiť df -i.
Kľúčové princípy:
- df -h ukazuje bajty, df -i ukazuje inody—potrebuješ oboje na pochopenie zdravia disku
- Jeden súbor = jeden inode bez ohľadu na veľkosť—milión 1-bajtových súborov používa milión inodov
- OverlayFS amplifikuje spotrebu inodov—copy-up a whiteouty vytvárajú extra inody
- Container image churn vyčerpáva inody—vrstvy akumulujú súbory naprieč images
- Loguj do stdout, nie per-request súborov—nechaj container runtime riešiť log agregáciu
Skontroluj využitie inodov tvojich nodov teraz. Ďalšie záhadné ENOSPC možno už prichádza.
Súvisiace články
- Kubernetes Ephemeral Storage Limits - Manažment container storage
- PostgreSQL WAL konfigurácia - Manažment WAL súborov
Súvisiace články
Kubernetes OOM Killer: Prečo Kontajner Zomiera pri 50% Pamäte
Kontajner má 4GB memory limit ale OOM kill pri 2GB used. Kernel buffers, page cache a cgroup accounting triky spôsobujú skoré OOMKills. Tu je celý obraz.
Pakety prichadzaju ale aplikacia timeoutuje: rp_filter pasca v Kubernetes
tcpdump ukazuje pakety ktore prichadzaju, ale aplikacia nic nevidi. Vinik: Linux reverse path filtering ticho zahadzuje pakety predtym nez dosiahnu iptables, sposobene asymetrickym routovanim.
tcpdump vidí SYN, ale služba timeoutuje: pasca listen backlogu
Klienti timeoutujú, tcpdump ukazuje SYN (niekedy aj SYN-ACK), ale aplikácia nič neloguje. Častý vinník: Linux listen/accept fronty, ktoré sa pri load-e alebo CPU starvation preplnia.
Linux Page Cache Thrashing v Kontajneroch: Keď Voľná Pamäť Nie Je Voľná
Váš kontajner má 2GB voľné ale beží pomaly. Page cache sa počíta proti memory limitu. File I/O vytláča code pages. Vysvetlím s benchmarkmi a riešeniami.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.