Cuprins

De ce rm nu eliberează întotdeauna spațiul pe disc?

Atunci când execuți comanda rm pe Linux, poți observa o situație aparent contradictorie: fișierul dispare din listările directorului, dar spațiul ocupat pe disc rămâne neschimbat. df arată același procent de utilizare ca înainte, deși du confirmă că fișierul nu mai există. Ce se întâmplă, de fapt?

Cum funcționează ștergerea pe Linux

Fiecare fișier de pe un sistem de fișiere Linux este reprezentat intern printr-un inode - o structură de date care conține metadatele fișierului (permisiuni, proprietar, dimensiune, pointeri către blocurile de date de pe disc) și un contor de legături (link count).

Numele fișierului dintr-un director nu este decât o legătură (hardlink) către inode-ul respectiv. Același inode poate avea mai multe nume, în directoare diferite - toate sunt legături egale.

Ce face de fapt rm

Comanda rm apelează syscall-ul unlink(). Acesta:

  1. Elimină intrarea numelui din directorul părinte
  2. Decrementează contorul de legături al inode-ului

Blocurile de date de pe disc sunt eliberate doar când:

  1. Contorul de legături ajunge la zero (nu mai există niciun nume care să refere inode-ul), ȘI
  2. Nu există niciun descriptor de fișier deschis care să refere inode-ul

Dacă un proces are fișierul deschis în momentul ștergerii, kernelul marchează inode-ul ca (deleted), dar păstrează blocurile alocate atâta timp cât procesul nu închide descriptorul.

Diagnosticarea problemei

Găsirea fișierelor șterse care ocupă spațiu

Comanda de bază pentru identificarea fișierelor marcate (deleted) dar încă deschise de procese:

sudo lsof +L1

Opțiunea +L1 afișează fișierele al căror link count este mai mic decât 1 (adică zero - șterse din sistemul de fișiere, dar cu descriptori activi).

Rezultatul arată ceva de forma:

COMMAND     PID   USER   FD   TYPE DEVICE SIZE/OFF NLINK   NODE NAME
nginx      1234   root   10w   REG    8,1  2147483648     0 123456 /var/log/nginx/access.log (deleted)
mysqld     5678  mysql   11w   REG    8,1  5368709120     0 789012 /var/lib/mysql/binlog.000123 (deleted)

Câmpul SIZE/OFF arată câți octeți ocupă fișierul pe disc, iar NLINK=0 confirmă că a fost șters.

Filtrarea rezultatelor

# Filtrare după dimensiune (mai mare de 100MB)
sudo lsof +L1 | awk 'NR==1 || $7 > 104857600'
 
# Filtrare după serviciu specific
sudo lsof +L1 | grep nginx
 
# Sortare după dimensiune (descrescător)
sudo lsof +L1 | sort -k7 -rn | head -20

Verificarea discrepanței df vs du

Dacă bănuiești că spațiul nu a fost eliberat, compară ieșirile:

# Spațiu real pe partiție (ce vede kernelul)
df -h /var/log
 
# Spațiu calculat din fișiere vizibile (ce vede du)
du -sh /var/log

O diferență semnificativă între cele două indică fișiere (deleted) cu descriptori activi. Poți calcula și totalul spațiului blocat pe întreg sistemul:

sudo lsof +L1 | awk 'NR>1 {sum += $7} END {printf "Total blocat: %.2f GB\n", sum/1073741824}'

Verificare rapidă cu /proc

Poți inspecta direct ce fișiere are deschis un proces (de exemplu PID 1234):

ls -la /proc/1234/fd/ | grep deleted

Eliberarea spațiului

Varianta 1: Repornirea serviciului responsabil

Cea mai simplă soluție - procesul care ține fișierul deschis va primi semnalul SIGTERM, va închide descriptorii, iar kernelul eliberează blocurile:

sudo systemctl restart nume-serviciu

Avantaj: simplu, curat, resetează și bufferele interne ale serviciului.

Dezavantaj: produce o întrerupere scurtă a serviciului (downtime). Pentru servicii critice, planifică o fereastră de mentenanță sau folosește varianta cu truncate de mai jos.

Varianta 2: Golirea fișierului fără a închide descriptorul

Aceasta este metoda recomandată pentru fișiere de log - nu rupe descriptorul de fișier al procesului, deci nu există niciun risc de erori sau pierderi de date în aplicație.

# Metoda 1 - redirecționare shell
> /cale/catre/fisier.log
 
# Metoda 2 - truncate (mai explicită, preferată în scripturi)
truncate -s 0 /cale/catre/fisier.log

Operatorul > deschide fișierul în mod scriere cu trunchiare și îl închide imediat, fără să scrie nimic. Procesul care ținea fișierul deschis continuă să scrie în același descriptor - acum la offset 0. Ambele metode sunt echivalente.

De ce nu rm + touch?

# GREȘIT pentru loguri active:
rm /var/log/nginx/access.log
touch /var/log/nginx/access.log

rm șterge intrarea din director, dar procesul continuă să scrie în inode-ul vechi. touch creează un fișier nou cu un inode nou, complet diferit. Rezultatul: procesul ignoră fișierul nou și scrie în continuare în inode-ul (deleted). Ai acum două probleme: spațiu blocat și log-uri pierdute.

Operatorul > și truncate modifică fișierul existent, la același inode - procesul vede modificarea imediat.

Automatizarea cu logrotate

Pe sisteme de producție, rotația logurilor se face cu logrotate, configurat cu directiva copytruncate - exact mecanismul descris mai sus.

Exemplu de configurare /etc/logrotate.d/nginx:

/var/log/nginx/*.log {
    daily
    missingok
    rotate 14
    compress
    delaycompress
    notifempty
    copytruncate
}

copytruncate copiază conținutul curent al fișierului într-un fișier backup, apoi trunchiează originalul - fără a semnaliza procesul să redeschidă fișierul.

Alte cauze ale discrepanței df/du

Fișierele cu descriptori activi nu sunt singura cauză. Alte situații în care df și du nu sunt de acord:

Cauză Explicație
Fișiere șterse cu FD activi Subiectul principal al acestui articol
Snapshot-uri filesystem Btrfs, ZFS sau LVM snapshots ocupă spațiu invizibil pentru du
Fișiere cu găuri (sparse files) du raportează dimensiunea logică, nu blocurile fizice alocate
Sistemul de fișiere rezervat ext4 rezervă ~5% pentru root (reserved-blocks-count)
Atribute extinse (xattrs) Metadate suplimentare ocupă spațiu necontabilizat de du

Rezumat

Resurse suplimentare