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?
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.
Comanda rm apelează syscall-ul unlink(). Acesta:
Blocurile de date de pe disc sunt eliberate doar când:
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.
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.
# 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
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}'
Poți inspecta direct ce fișiere are deschis un proces (de exemplu PID 1234):
ls -la /proc/1234/fd/ | grep deleted
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.
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.
# 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.
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.
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 |
rm apelează unlink() și elimină doar numele fișierului, nu datelesudo lsof +L1 pentru a găsi fișierele (deleted) cu descriptori activi> sau truncate -s 0 (fără downtime)rm + touch pe fișiere active - folosește întotdeauna truncate