SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor

df pieno ma du non torna: dove finisce lo spazio

Se df indica il disco pieno ma du non trova lo spazio, individua file eliminati ancora aperti, processi coinvolti e altre cause senza riavviare il VPS.

Perché df indica il disco come pieno mentre du mostra un risultato diverso

df segnala che il disco è pieno, mentre du non trova lo spazio occupato perché un processo mantiene aperto un file che è stato eliminato. L’eliminazione di un file rimuove il relativo nome dalla directory. I blocchi di dati vengono rilasciati solo quando viene chiuso l’ultimo file descriptor aperto che punta a quell’inode. du percorre i nomi dei file, quindi non conta nulla. df chiede al filesystem quanti blocchi sono allocati, quindi continua a conteggiare il file che non ha più un nome.

Questa guida riproduce il problema su un VPS Ubuntu standard usando strumenti già installati, individua il processo che mantiene aperto il file tramite /proc e libera lo spazio senza riavviare il sistema. Seguono anche le altre cause dello stesso sintomo: una tabella degli inode senza entry libere, file nascosti sotto un mount point e blocchi riservati a root.

Esegui ogni comando e leggi l’output del tuo sistema. I valori dipendono dal disco in uso. Confronta quindi i valori prima e dopo sul tuo computer, invece di confrontarli con un valore riportato in una guida.

Cosa conta df e cosa conta du

df (disk free) interroga ogni filesystem montato per ottenere i relativi dati: quanti blocchi esistono, quanti sono allocati e quanti sono liberi. Non apre mai una directory. Il risultato comprende ogni blocco allocato, inclusi i blocchi appartenenti a un file a cui non punta alcuna voce di directory.

du (disk usage) fa l’opposto. Parte dal percorso indicato, legge le directory, recupera gli attributi di ogni voce trovata e somma i blocchi. Un file senza nome non viene rilevato. Lo stesso vale per qualsiasi directory che non può leggere. Per questo un utente normale ottiene un totale inferiore rispetto a root. Eseguire du con sudo prima di trarre conclusioni dal confronto.

Due opzioni sono importanti ogni volta che si confrontano i due comandi.

  • -x mantiene du in un solo filesystem. Senza questa opzione, du / attraversa ogni filesystem montato sotto / e produce un totale che df / non stava mai misurando.
  • -s stampa una sola riga riepilogativa per argomento, invece di una riga per directory.

Questa è la coppia di comandi da eseguire affiancati sul filesystem interessato.

df -h /
sudo du -xhs / 2>/dev/null

df restituisce subito il risultato. du richiede alcuni minuti su un filesystem di grandi dimensioni, perché recupera gli attributi di ogni file durante l’analisi. Quando i due totali sono molto diversi e du è stato eseguito come root con -x, lo spazio mancante è allocato a qualcosa che non ha nome.

Riprodurre intenzionalmente la discrepanza

Eseguire questa procedura su una VPS di test. Tutto ciò che segue usa bash e coreutils, quindi non viene installato nulla.

Registrare lo stato iniziale del filesystem che contiene /var/tmp.

cd /var/tmp
df -h .
df --output=used -B1 .

Il secondo comando stampa i byte utilizzati senza arrotondamenti. In questo modo il controllo finale è esatto.

Creare ora un file. La sua dimensione deriva dallo spazio libero riportato dalla macchina stessa, quindi la dimostrazione si adatta a qualsiasi disco.

free=$(df --output=avail -B1 . | tail -n 1)
fallocate -l $((free / 10)) ghost.bin
ls -l ghost.bin
df -h .

$(...) è la sostituzione di comando: la shell esegue il comando al suo interno e l'output diventa il valore di free. Se questa sintassi è nuova, la sostituzione di comando in bash è descritta in dettaglio. fallocate riserva blocchi reali senza scrivervi dati, perciò termina immediatamente. Se il filesystem non supporta questa funzione, il comando non riesce e head -c $((free / 10)) /dev/zero > ghost.bin svolge lo stesso compito scrivendo i byte.

Confrontare questo df -h . con quello registrato. La colonna dello spazio utilizzato è aumentata e quella dello spazio disponibile si è ridotta.

Mantenere ora il file aperto da un altro processo, quindi eliminarlo.

sleep infinity < ghost.bin &
holder=$!
rm ghost.bin
ls -l ghost.bin
df -h .
sudo du -xhs . 2>/dev/null

La redirezione è l'elemento fondamentale. sleep infinity < ghost.bin & avvia un processo in background il cui standard input è quel file. La shell apre quindi il file e passa il descrittore a sleep, che lo mantiene aperto. $! contiene l'ID del processo del job in background. rm rimuove quindi il nome mentre il descrittore è ancora aperto.

Leggere l'output. ls non trova il file perché il nome non esiste più. du è tornato vicino al valore iniziale perché percorre i nomi. df non è cambiato perché i blocchi sono ancora allocati. Il filesystem e l'albero delle directory ora non sono coerenti e la differenza tra i due rappresenta il file appena eliminato.

Individuare il processo che mantiene aperto il file eliminato

Ogni descrittore di file aperto compare in /proc/<pid>/fd/ come collegamento simbolico al file a cui fa riferimento. Quando il file viene scollegato, il kernel contrassegna come eliminato il target di quel collegamento. Per individuare il processo che lo mantiene aperto, bisogna quindi trovare un collegamento il cui target riporti quel contrassegno.

sudo find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null

-lname cerca il target di un collegamento simbolico invece del relativo nome, %p stampa il percorso del descrittore e %l mostra il relativo target. Il process ID è il secondo elemento del percorso stampato. Eseguire il comando con sudo, perché altrimenti è possibile leggere /proc/<pid>/fd soltanto per i propri processi. Il redirect di stderr elimina i messaggi generati dai processi che terminano mentre find attraversa la struttura.

Un server operativo mantiene aperti diversi file eliminati in qualsiasi momento. La maggior parte è di piccole dimensioni e non causa problemi. Ordinarli per dimensione consente di mantenere in cima soltanto quelli interessanti.

sudo bash -c 'for fd in /proc/[0-9]*/fd/*; do
  target=$(readlink "$fd" 2>/dev/null) || continue
  case "$target" in
    *"(deleted)") echo "$(stat -Lc %s "$fd" 2>/dev/null) $fd $target" ;;
  esac
done' | sort -rn | head

stat -L segue il collegamento fino all'inode stesso, quindi %s restituisce la dimensione del file che non ha più un nome. Ordinando in base a quel numero, il file più grande viene visualizzato per primo.

Identificare quindi il processo associato al descrittore trovato. Il percorso nella prima riga dell'elenco contiene entrambi i numeri necessari. Inserirli prima in alcune variabili, sostituendo PID e N con i valori mostrati dal proprio elenco.

pid=PID
n=N
ps -o pid,user,etime,args -p "$pid"
sudo stat -L "/proc/$pid/fd/$n"

ps identifica il programma e mostra da quanto tempo è in esecuzione. stat -L stampa la dimensione e il numero di blocchi allocati dell'inode eliminato. Insieme, questi dati rispondono alla domanda principale: quale servizio mantiene in uso questo file.

Se il sistema dispone già di lsof, sudo lsof +L1 elenca i file aperti il cui link count è sceso a zero e ne mostra le dimensioni in un'unica tabella. Questo comando non è presente in un'immagine Ubuntu minimale. Inoltre, l'installazione di un pacchetto su un filesystem senza spazio libero può fallire a sua volta. Per questo, l'analisi con /proc è la variante che funziona sempre.

Libera lo spazio senza riavviare

Un riavvio risolve il problema, ma è la scelta sbagliata come primo intervento: interrompe il servizio e distrugge le evidenze. Esistono quattro opzioni meno invasive, da provare in quest’ordine.

Per prima cosa, copia i dati altrove se vuoi conservarli. Leggere il percorso del descrittore significa leggere l’inode ancora utilizzato.

sudo cp /proc/<pid>/fd/<n> /root/recovered.log

Questo è l’unico caso in cui è facile recuperare un file eliminato. Per questo recuperare file eliminati con rm -rf inizia chiedendo se un processo mantiene ancora aperto il file. Quando si chiude l’ultimo descrittore, questa possibilità scompare.

Secondo, svuota il file tramite il descrittore. Il percorso /proc conduce allo stesso inode, quindi il troncamento libera i blocchi mentre il processo continua a funzionare.

sudo truncate -s 0 "/proc/$pid/fd/$n"
df -h /

Questa procedura funziona correttamente quando il processo che scrive ha aperto il file in modalità append, perché ogni scrittura viene aggiunta alla fine corrente del file. In caso contrario, il processo mantiene la vecchia posizione di scrittura. La scrittura successiva viene quindi eseguita molto più avanti nel file e lo ricrea con un buco nella parte iniziale. Un buco non alloca blocchi, quindi i blocchi restano liberi e df continua a mostrare lo spazio appena restituito. Cambia solo la dimensione: esegui di nuovo sudo stat -L "/proc/$pid/fd/$n" dopo che il processo ha scritto e vedrai la vecchia dimensione accanto a un numero di blocchi che non corrisponde più. Riavvia il processo quando vuoi che anche la dimensione riparta da zero.

Terzo, chiedi al servizio di riaprire i log. Un daemon il cui file di log è stato eliminato mentre era ancora aperto rappresenta il caso concreto più comune di questo problema. Molti daemon riaprono i file di log quando ricevono un segnale: nginx usa SIGUSR1, mentre rsyslog usa SIGHUP. Consulta la documentazione del daemon in uso invece di procedere per tentativi, perché l’invio del segnale sbagliato al daemon sbagliato può arrestarlo.

sudo systemctl kill -s USR1 nginx

Quarto, riavvia l’unità. sudo systemctl restart <unit> chiude tutti i descrittori mantenuti dal vecchio processo, quindi i blocchi tornano sicuramente disponibili. Nell’esempio precedente, il processo che mantiene aperto il file è un sleep avviato da te, quindi è sufficiente terminarlo.

kill $holder
df -h .
df --output=used -B1 .
sudo find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null

Confronta i byte utilizzati con il valore annotato prima di creare il file. I valori coincidono di nuovo e find non mostra più il descrittore. Verificare il risultato con lo stesso comando che ha individuato il problema è una buona abitudine.

Osservare il cambiamento di quel valore è più semplice che eseguire df manualmente più volte. watch ripete un comando a intervalli fissi e ristampa l’output nella stessa posizione, quindi watch df -h / mostra la variazione della colonna dello spazio utilizzato mentre lo spazio torna disponibile.

Quando i totali coincidono ma il disco è ancora pieno

Se df e un du -x root coincidono, non è coinvolto alcun file eliminato. Le cause rimanenti sono di natura diversa e ciascuna richiede una verifica specifica.

Esaurimento degli inode, non dei blocchi

Un inode contiene i metadati di un file. ext4 crea un numero fisso di inode quando viene creato il filesystem, quindi un filesystem può esaurire gli inode anche se dispone ancora di blocchi liberi. La creazione di nuovi file fallisce anche se df -h indica spazio disponibile.

df -h /
df -i /

Il primo comando conta i blocchi, il secondo conta gli inode. Confronta la colonna dell'utilizzo in entrambi gli output. Un utilizzo dei blocchi basso e un utilizzo degli inode vicino al limite indicano la presenza di un numero molto elevato di file molto piccoli.

df rifiuta -i e --output nella stessa invocazione. Quando ti servono i conteggi grezzi da leggere o da passare a un altro comando, seleziona quindi i campi degli inode per nome e non usare -i.

df --output=itotal,iused,iavail,ipcent /

Queste colonne riportano gli stessi dati contabili mostrati da df -i, in un formato che puoi elaborare.

Individua i file contando le voci invece dei byte.

sudo du --inodes -x -d 1 / 2>/dev/null | sort -rn | head

Ripeti lo stesso comando un livello più in basso sulla directory che risulta prima nell'elenco, finché raggiungi l'albero che sta creando i file. Se il tuo du non supporta --inodes, usa sudo find /var -xdev -type f | wc -l per contare un sottoalbero, anche se in modo più lento.

La soluzione consiste nell'eliminare o spostare questi file. Non puoi aggiungere inode a un filesystem ext4 esistente, perché il numero viene fissato al momento della creazione, indicato da mkfs. Per aumentarlo devi quindi ricreare il filesystem e ripristinare i dati da un backup. XFS alloca gli inode secondo necessità, quindi non raggiunge lo stesso tipo di limite fisso. Un computer che esegue container raggiunge entrambi i limiti più rapidamente della maggior parte dei sistemi, perché i livelli delle immagini contengono molti file piccoli. Su quel computer, ridurre l'utilizzo del disco di Docker su un VPS è la soluzione specifica e recupera molto più spazio di una ricerca generica nell'intero filesystem.

Spazio nascosto sotto un punto di mount

Una directory può contenere file prima che vi venga montato un filesystem. Montando un filesystem sopra quella directory, i file sottostanti restano esattamente dove si trovavano: continuano a occupare spazio, vengono ancora conteggiati da df, ma non sono più raggiungibili tramite il nome. du non può visualizzarli perché il mount li nasconde.

Dimostralo con tmpfs, che non richiede spazio aggiuntivo sul disco. Questa procedura richiede una macchina sulla quale sia consentito eseguire mount, quindi funziona su un KVM VPS.

sudo mkdir -p /srv/covered
sudo cp /etc/services /srv/covered/
ls /srv/covered
sudo mount -t tmpfs tmpfs /srv/covered
ls /srv/covered
sudo umount /srv/covered
ls /srv/covered

Il ls centrale mostra una directory vuota. La copia non è stata spostata: si trova ancora sul filesystem root e ricompare non appena esegui l’unmount. Immagina ora un servizio che abbia scritto in quel percorso per un mese prima che qualcuno vi montasse sopra un volume.

Per trovare i file reali su un server in esecuzione, monta una seconda volta il filesystem root in un altro percorso. Un bind mount mostra un filesystem senza i filesystem montati al suo interno.

sudo mkdir -p /mnt/rootcheck
sudo mount --bind / /mnt/rootcheck
sudo du -xhs /mnt/rootcheck/* 2>/dev/null | sort -h
sudo umount /mnt/rootcheck

Tutto ciò che compare in quell’elenco ma non nel percorso normale è nascosto sotto un punto di mount. Esegui l’unmount del bind mount al termine, altrimenti un successivo du senza -x conterà due volte gli stessi file.

Blocchi riservati per root

ext4 riserva una parte dei blocchi all'utente root, in modo che un disco pieno non impedisca a root di accedere al sistema e riparare la macchina. Un processo eseguito da un utente normale raggiunge prima questo limite, mentre df mostra ancora un piccolo spazio disponibile. Leggi l'impostazione del tuo filesystem invece di presumere il valore predefinito.

dev=$(df --output=source / | tail -n 1)
sudo tune2fs -l "$dev" | grep -i 'block count'

Il comando stampa il numero totale di blocchi e il numero di blocchi riservati usando la stessa unità, quindi il rapporto tra i due valori è diretto. df indica nella colonna available lo spazio che un utente normale può ancora utilizzare; per questo la somma di spazio usato e disponibile è inferiore alla dimensione totale. La differenza corrisponde allo spazio riservato.

Modifica questo valore con sudo tune2fs -m <percent> "$dev". La modifica viene applicata immediatamente e non richiede il remount. Ridurre lo spazio riservato su un filesystem separato per i dati è ragionevole. Sul filesystem root, lascia una quantità sufficiente per consentire a root di scrivere ancora, perché è molto più difficile riparare un filesystem root completamente privo di spazio libero. tune2fs funziona con ext2, ext3 ed ext4. XFS non dispone di un'impostazione equivalente.

Quando du da solo può fornire indicazioni fuorvianti

Quattro comportamenti di du producono totali apparentemente errati.

  • Hard link: du conta un inode una sola volta anche quando più nomi puntano a esso; per questo, un albero composto da molti hard link riporta un valore inferiore alla somma dei suoi file.
  • File sparse: du riporta i blocchi effettivamente allocati, mentre ls -l riporta la dimensione apparente. Aggiungere --apparent-size per visualizzare l'altro valore.
  • Permessi: se viene eseguito da un utente normale, du ignora ciò che non può leggere e riporta un valore inferiore al reale. Gli errori che stampa sono quelli che molti reindirizzano a /dev/null senza più consultarli.
  • Confini del filesystem: senza -x, du / conta ogni filesystem montato sotto /, quindi il totale può superare quello riportato da df /.

Anche df ha un comportamento importante da conoscere. Riporta ogni filesystem separatamente; perciò va eseguito sul percorso esatto interessato dalla scrittura che non riesce. Un /boot separato si riempie secondo un ciclo indipendente, mentre i pacchetti del kernel si accumulano; rimuovere i vecchi kernel su Ubuntu è quindi un'attività diversa dalla liberazione di spazio su /.

Ordine operativo per un incidente reale

  1. Esegui df -h <path> e df -i <path> sul filesystem di destinazione della scrittura non riuscita, invece di eseguirli automaticamente su /.
  2. Esegui sudo du -xh -d 1 <mountpoint> 2>/dev/null | sort -h, quindi analizza la directory di dimensioni maggiori.
  3. Se du non spiega lo spazio che df indica come utilizzato, cerca in /proc i file eliminati che sono ancora aperti.
  4. Se i due valori corrispondono, esegui il bind mount del filesystem in un altro punto e cerca i file presenti sotto un punto di mount.
  5. Se il limite riguarda l'utilizzo degli inode, conta i file invece dei byte.

Ogni passaggio usa un comando il cui output può essere letto e verificato. Questa è la differenza tra risolvere il problema e procedere per ipotesi.

FAQ

Perché df mostra il disco pieno mentre du rileva un utilizzo molto inferiore?

La causa più comune è un file eliminato mentre un processo lo teneva ancora aperto. La rimozione del file elimina la sua voce dalla directory, quindi du non ha più un nome da percorrere e smette di conteggiarlo. L'inode e i relativi blocchi restano allocati finché non viene chiuso l'ultimo descrittore, mentre df conta i blocchi allocati. Cerca in /proc/<pid>/fd i link simbolici il cui obiettivo è contrassegnato come eliminato: in questo modo trovi sia il file sia il processo che lo mantiene aperto. Prima di considerare attendibile il confronto, verifica di aver eseguito du come root e con -x, perché un utente normale ignora senza segnalarlo le directory che non può leggere.

Come posso trovare un file eliminato che è ancora aperto senza usare lsof?

Usa il registro dei descrittori aperti mantenuto direttamente dal kernel. sudo find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null elenca ogni descrittore che punta a un file senza nome; l'ID del processo è incluso nel percorso visualizzato. sudo stat -Lc %s su uno di questi percorsi dei descrittori restituisce le dimensioni del file, così puoi ordinarli e individuare quello rilevante. Non serve installare alcun pacchetto, aspetto importante perché l'installazione su un filesystem senza spazio libero può non riuscire.

Posso liberare lo spazio senza terminare il processo?

A volte sì. sudo truncate -s 0 /proc/<pid>/fd/<n> raggiunge lo stesso inode attraverso il descrittore e libera i relativi blocchi mentre il processo continua a funzionare. Questa è la soluzione più pulita quando il processo ha aperto il file in modalità append, perché le scritture vengono sempre eseguite alla fine corrente del file. In caso contrario, l'offset di scrittura resta nella posizione precedente e la scrittura successiva ricrea il file con un buco all'inizio; la dimensione riportata torna quindi ad aumentare, mentre i blocchi corrispondenti al buco restano liberi. Riavviare l'unità, oppure inviarle il segnale per riaprire i log indicato nella documentazione, è la soluzione che non lascia file sparse.

df mostra spazio libero, ma le scritture continuano a fallire. Quale può essere la causa?

Controlla gli inode con df -i sullo stesso percorso, perché un filesystem con blocchi liberi ma senza inode disponibili rifiuta la creazione di nuovi file. Verifica se la scrittura viene eseguita da un utente non root su un filesystem ext4 in cui sono rimasti soltanto i blocchi riservati; sudo tune2fs -l sul dispositivo mostra questa situazione. Verifica di leggere il filesystem effettivamente utilizzato dalla scrittura, perché un /boot o un /var separato può riempirsi indipendentemente da /.

Perché du riporta un totale maggiore di df?

du senza -x attraversa ogni filesystem montato sotto il percorso indicato, quindi somma più filesystem mentre df descrive un solo filesystem. I bind mount peggiorano la situazione, perché gli stessi file vengono conteggiati una volta per ciascun percorso in cui compaiono. Aggiungi -x per mantenere du su un solo filesystem e fornisci a df lo stesso percorso, così entrambi i comandi descrivono lo stesso insieme di dati.