Späť na blog

'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:

  1. df -h ukazuje bajty, df -i ukazuje inody—potrebuješ oboje na pochopenie zdravia disku
  2. Jeden súbor = jeden inode bez ohľadu na veľkosť—milión 1-bajtových súborov používa milión inodov
  3. OverlayFS amplifikuje spotrebu inodov—copy-up a whiteouty vytvárajú extra inody
  4. Container image churn vyčerpáva inody—vrstvy akumulujú súbory naprieč images
  5. 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

Súvisiace články

Citujte tento článok

Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.

Michal Drozd. "'No space left on device' s 40% voľného disku: Inode a OverlayFS Death Spiral". https://www.michal-drozd.com/sk/blog/kubernetes-inode-exhaustion-overlayfs/ (Publikované 7. decembra 2025).