SSD Nodes Learn 🎉 VPS desde $5.50/mes
Guías Matt ConnorPor Matt Connor

`df` lleno y `du` no encuentra el espacio

Si `df` indica que el disco está lleno pero `du` no lo explica, localiza el archivo eliminado que un proceso aún mantiene abierto y libera espacio sin reiniciar.

Por qué df indica que el disco está lleno y du muestra lo contrario

df informa de que el disco está lleno, mientras du no encuentra ese espacio porque un proceso todavía mantiene abierto un archivo que se eliminó. Al eliminar un archivo, se borra su nombre del directorio. Los bloques de datos sólo se liberan cuando se cierra el último descriptor de archivo abierto que apunta a ese inode. du recorre los nombres, por lo que no cuenta ese espacio. df consulta al sistema de archivos cuántos bloques están asignados, por lo que sigue contando el archivo que ya no tiene nombre.

Esta guía reproduce el problema en un VPS de Ubuntu básico con herramientas que ya están instaladas, identifica el proceso que mantiene abierto el archivo mediante /proc y libera el espacio sin reiniciar. A continuación se describen otras causas del mismo síntoma: una tabla de inodos sin entradas libres, archivos ocultos bajo un punto de montaje y bloques reservados para root.

Ejecute cada comando y revise su propia salida. Los valores dependen de su disco. Compare los valores iniciales y finales en su propio equipo, en lugar de compararlos con una cifra incluida en una guía.

Qué cuenta df y qué cuenta du

df (disk free) consulta cada sistema de archivos montado para obtener sus propios datos: cuántos bloques existen, cuántos están asignados y cuántos están libres. Nunca abre un directorio. La respuesta incluye todos los bloques asignados, incluidos los bloques pertenecientes a un archivo al que no apunta ninguna entrada de directorio.

du (disk usage) hace lo contrario. Comienza en la ruta que se le proporciona, lee los directorios, obtiene los metadatos de cada entrada que encuentra y suma los bloques. Un archivo sin nombre no es visible para este comando. Lo mismo ocurre con cualquier directorio que no tenga permiso para leer. Por eso un usuario normal obtiene un total menor que root. Ejecute du con sudo antes de sacar conclusiones de la comparación.

Dos opciones son importantes cada vez que compare ambos comandos.

  • -x mantiene du en un solo sistema de archivos. Sin esta opción, du / entra en todos los sistemas de archivos montados debajo de / y produce un total que df / nunca estaba midiendo.
  • -s muestra una línea de resumen por argumento en lugar de una línea por directorio.

Así se obtiene el par de comandos que se deben ejecutar en paralelo sobre el sistema de archivos que se quiere comprobar.

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

df responde de inmediato. du tarda minutos en un sistema de archivos grande porque obtiene los metadatos de cada archivo durante el recorrido. Cuando los dos totales difieren mucho y du se ejecutó como root con -x, el espacio que falta está asignado a algo que no tiene nombre.

Reproduzca la discrepancia de forma intencionada

Haga esto en un VPS de prueba. Todo lo siguiente usa bash y coreutils, por lo que no se instala nada.

Registre el estado inicial del sistema de archivos que contiene /var/tmp.

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

El segundo comando muestra los bytes usados sin redondear, por lo que la comprobación final es exacta.

Ahora cree un archivo. Su tamaño se basa en el espacio libre que informa la propia máquina, por lo que la demostración se adapta a cualquier disco.

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

$(...) es una sustitución de comandos: el shell ejecuta el comando que contiene y la salida se convierte en el valor de free. Si esta sintaxis es nueva para usted, la sustitución de comandos en bash la explica correctamente. fallocate reserva bloques reales sin escribir en ellos, por eso termina al instante. En un sistema de archivos que no admite esta operación, el comando falla, y head -c $((free / 10)) /dev/zero > ghost.bin hace lo mismo escribiendo los bytes.

Compare este df -h . con el que registró. La columna de espacio usado ha aumentado y la columna de espacio disponible ha disminuido.

Ahora mantenga el archivo abierto desde otro proceso y, después, elimínelo.

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

La redirección es todo el mecanismo. sleep infinity < ghost.bin & inicia un proceso en segundo plano cuya entrada estándar es ese archivo, por lo que el shell abre el archivo y entrega el descriptor a sleep, que lo mantiene abierto. $! contiene el ID de proceso de esa tarea en segundo plano. rm elimina entonces el nombre mientras el descriptor sigue abierto.

Lea la salida. ls no encuentra el archivo porque el nombre ya no existe. du vuelve a estar cerca del valor inicial porque recorre los nombres. df no ha cambiado porque los bloques siguen asignados. El sistema de archivos y el árbol de directorios ya no coinciden, y la diferencia entre ambos es el archivo que acaba de eliminar.

Buscar el proceso que mantiene abierto el archivo eliminado

Cada descriptor de archivo abierto aparece en /proc/<pid>/fd/ como un enlace simbólico al archivo al que hace referencia. Cuando se elimina el archivo, el kernel marca como eliminado el destino de ese enlace. Por tanto, para encontrar el proceso que lo mantiene abierto hay que buscar un enlace cuyo destino contenga esa marca.

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

-lname busca coincidencias en el destino de un enlace simbólico, no en su nombre; %p muestra la ruta del descriptor y %l muestra aquello a lo que apunta. El ID del proceso es el segundo elemento de la ruta que imprime. Ejecútelo con sudo, porque de lo contrario sólo podrá leer /proc/<pid>/fd en sus propios procesos. La redirección de stderr descarta los mensajes de procesos que terminan mientras find recorre el sistema.

Un servidor con mucha carga mantiene varios archivos eliminados abiertos en cualquier momento. La mayoría son pequeños y no causan problemas. Ordénelos por tamaño para que los más relevantes queden arriba.

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 sigue el enlace hasta el propio inode, por lo que %s muestra el tamaño del archivo que ya no tiene nombre. Ordenar por ese número coloca primero el archivo más grande.

A continuación, identifique el proceso asociado al descriptor principal. La ruta que aparece al principio de la lista contiene los dos números que necesita. Guárdelos primero en variables y sustituya PID y N por los valores que muestre su propia lista.

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

ps muestra el nombre del programa y cuánto tiempo lleva ejecutándose. stat -L muestra el tamaño y el número de bloques asignados del inode eliminado. Juntos responden a la pregunta importante: qué servicio mantiene vivo este archivo.

Si el equipo ya tiene lsof, sudo lsof +L1 muestra en una sola tabla los archivos abiertos cuyo número de enlaces ha disminuido a cero y sus tamaños. No está instalado en una imagen mínima de Ubuntu. Además, la instalación de un paquete en un sistema de archivos sin espacio libre puede fallar. Por eso, el recorrido con /proc es la opción que siempre funciona.

Libere el espacio sin reiniciar

Un reinicio sí lo corrige, pero es una mala primera medida: deja el servicio fuera de línea y destruye las pruebas. Hay cuatro opciones menos agresivas, en este orden.

Primero, copie los datos fuera del sistema si todavía los necesita. Leer la ruta del descriptor permite acceder al inode activo.

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

Este es el único caso en el que resulta fácil recuperar un archivo eliminado. Por eso recuperar archivos eliminados con rm -rf empieza preguntando si un proceso todavía mantiene abierto el archivo. Cuando se cierra el último descriptor, esa posibilidad desaparece.

Segundo, vacíe el archivo mediante el descriptor. La ruta /proc apunta al mismo inode, por lo que truncarlo libera los bloques mientras el proceso sigue ejecutándose.

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

Esto funciona correctamente cuando el proceso que escribe abrió el archivo en modo append, porque cada escritura se dirige al final actual del archivo. Si no lo hizo, el proceso conserva su antiguo desplazamiento de escritura. Por tanto, la siguiente escritura se realiza muy avanzada dentro del archivo y lo recrea con un hueco al principio. Un hueco no se asigna, así que los bloques siguen libres y df conserva el espacio que acaba de recuperar. Lo que reaparece es sólo el tamaño: ejecute sudo stat -L "/proc/$pid/fd/$n" de nuevo después de que el proceso escriba, y mostrará el tamaño antiguo junto a un recuento de bloques que ya no coincide. Reinicie el proceso cuando también quiera que el tamaño vuelva a empezar desde cero.

Tercero, pida al servicio que vuelva a abrir sus registros. Un daemon cuyo archivo de registro se eliminó mientras lo tenía abierto es la versión real más habitual de este problema. Muchos daemons vuelven a abrir sus archivos de registro al recibir una señal: nginx usa SIGUSR1 y rsyslog usa SIGHUP. Consulte la documentación del daemon correspondiente en lugar de adivinar, porque enviar la señal incorrecta al daemon equivocado lo detiene.

sudo systemctl kill -s USR1 nginx

Cuarto, reinicie la unidad. sudo systemctl restart <unit> cierra todos los descriptores que mantenía abiertos el proceso anterior, por lo que los bloques vuelven a estar disponibles con seguridad. En la demostración anterior, el proceso que mantiene el descriptor es un sleep que inició usted mismo, así que basta con finalizarlo.

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

Compare los bytes usados con el valor que registró antes de crear el archivo. Vuelven a coincidir y find ya no muestra el descriptor. Verificarlo con el mismo comando que encontró el problema es el hábito que conviene conservar.

Observar cómo cambia ese valor es más fácil que ejecutar df manualmente una y otra vez. watch repite un comando en un intervalo fijo y vuelve a imprimir la salida en el mismo lugar, por lo que watch df -h / muestra cómo cambia la columna de espacio usado a medida que el espacio vuelve a estar disponible.

Cuando los totales coinciden y el disco sigue lleno

Si df y un du -x de root coinciden, no hay ningún archivo eliminado implicado. Las causas restantes son de otro tipo y cada una requiere una comprobación distinta.

Sin inodos, no sin bloques

Un inodo contiene los metadatos de un archivo. ext4 crea un número fijo de inodos al crear el sistema de archivos, por lo que este puede quedarse sin inodos aunque todavía tenga bloques libres. La creación de archivos nuevos falla aunque df -h muestre espacio disponible.

df -h /
df -i /

El primer comando cuenta bloques y el segundo cuenta inodos. Compare la columna de uso de cada uno. Un uso bajo de bloques y un uso de inodos en el límite indican que el problema es un número muy grande de archivos muy pequeños.

df rechaza -i y --output en la misma invocación. Por tanto, cuando necesite leer los recuentos sin formato o pasarlos a otro comando, seleccione los campos de inodos por nombre y omita -i.

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

Esas columnas contienen los mismos datos contables que muestra df -i, en un formato que puede analizar por partes.

Busque los archivos contando entradas en lugar de bytes.

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

Repita el mismo comando un nivel más abajo sobre el directorio que aparezca primero, hasta llegar al árbol que está creando los archivos. Si su du no admite --inodes, sudo find /var -xdev -type f | wc -l cuenta un subárbol de forma más lenta.

La solución consiste en eliminar o mover esos archivos. No puede añadir inodos a un sistema de archivos ext4 existente, porque el recuento se fija en el momento de mkfs. Por tanto, aumentarlo requiere volver a crear el sistema de archivos y restaurarlo desde una copia de seguridad. XFS asigna los inodos según los necesita, por lo que no alcanza un límite fijo de la misma forma. Una máquina que ejecuta contenedores alcanza ambos límites antes que la mayoría, porque las capas de imágenes contienen muchos archivos pequeños. En esa máquina, liberar espacio de Docker en el disco de un VPS es la solución específica y recupera mucho más espacio que una limpieza general del sistema de archivos.

Espacio oculto bajo un punto de montaje

Un directorio puede contener archivos antes de montar algo sobre él. Monte un sistema de archivos sobre ese directorio y los archivos subyacentes permanecen exactamente donde estaban: siguen ocupando espacio, siguen contabilizándose en df y ya no se pueden alcanzar por nombre. du no puede verlos porque el montaje los cubre.

Muéstrelo con tmpfs, que no necesita espacio de disco adicional. Esta parte requiere una máquina en la que tenga permiso para montar sistemas de archivos, por lo que funciona en un VPS de KVM.

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

El ls del centro muestra un directorio vacío. La copia no desapareció: sigue en el sistema de archivos raíz y vuelve a aparecer en cuanto desmonte el sistema de archivos. Ahora imagine un servicio que escribió registros en esa ruta durante un mes antes de que alguien montara un volumen sobre ella.

Para encontrar los archivos reales en un servidor en ejecución, monte de nuevo el sistema de archivos raíz en otra ubicación. Un montaje bind muestra un sistema de archivos sin los sistemas de archivos montados dentro de él.

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

Todo lo que aparezca en ese listado pero no bajo la ruta normal está oculto bajo un punto de montaje. Desmonte el montaje bind cuando termine; de lo contrario, un du posterior sin -x contabilizará los mismos archivos dos veces.

Bloques reservados para root

ext4 reserva una parte de sus bloques para el usuario root, de modo que un disco lleno no impida que root inicie sesión y repare el equipo. Un proceso que se ejecuta como un usuario normal llega primero a ese límite, mientras df todavía muestra algo de espacio disponible. Consulte el ajuste del sistema de archivos que utiliza, en lugar de asumir el valor predeterminado.

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

Ese comando muestra el número total de bloques y el número de bloques reservados con las mismas unidades, por lo que la proporción entre ambos valores es directa. df muestra la columna disponible como el espacio que todavía puede utilizar un usuario normal. Por eso, la suma de usado y disponible es menor que el tamaño. La diferencia es la reserva.

Cambie este valor con sudo tune2fs -m <percent> "$dev". El cambio se aplica de inmediato y no requiere volver a montar el sistema de archivos. Reducir la reserva en un sistema de archivos independiente para datos es razonable. En el sistema de archivos raíz, conserve una reserva suficiente para que root pueda seguir escribiendo, porque un sistema de archivos raíz sin ningún espacio libre es mucho más difícil de reparar. tune2fs funciona con ext2, ext3 y ext4. XFS no tiene un ajuste equivalente.

Dónde puede inducir a error du por sí solo

Cuatro comportamientos de du producen totales que parecen incorrectos.

  • Enlaces duros: du cuenta un inode una sola vez aunque varios nombres apunten a él, por lo que un árbol lleno de enlaces duros informa de menos espacio que la suma de sus archivos.
  • Archivos dispersos: du informa de los bloques asignados realmente, mientras que ls -l informa del tamaño aparente. Añada --apparent-size para ver el otro valor.
  • Permisos: si se ejecuta como un usuario normal, du omite lo que no puede leer e informa de un uso inferior al real. Los errores que muestra son los que muchas personas redirigen a /dev/null y dejan de revisar.
  • Límites del sistema de archivos: sin -x, du / cuenta todos los sistemas de archivos montados debajo de /, por lo que su total puede superar el valor que informa df /.

df también tiene un comportamiento que conviene conocer. Informa de cada sistema de archivos por separado, así que ejecútelo sobre la ruta exacta a la que apunta la escritura que falla. Un /boot independiente se llena según su propio calendario a medida que se acumulan los paquetes del kernel, y eliminar kernels antiguos en Ubuntu es una tarea distinta de liberar espacio en /.

Orden de trabajo para un incidente real

  1. Ejecute df -h <path> y df -i <path> en el sistema de archivos donde se intentó realizar la escritura que falló, no en / por reflejo.
  2. Ejecute sudo du -xh -d 1 <mountpoint> 2>/dev/null | sort -h y, después, descienda al directorio de mayor tamaño.
  3. Si du no puede explicar el uso que df informa, busque en /proc archivos eliminados que todavía estén abiertos.
  4. Si ambos resultados coinciden, monte el sistema de archivos mediante un bind mount en otra ubicación y busque archivos ubicados bajo un punto de montaje.
  5. Si el límite corresponde al uso de inodos, cuente archivos en lugar de bytes.

Cada paso contiene un comando cuyo resultado puede leer. Esa es la diferencia entre resolver el problema y hacer conjeturas.

FAQ

¿Por qué df muestra el disco lleno cuando du encuentra mucho menos?

La causa habitual es un archivo que se eliminó mientras un proceso todavía lo tenía abierto. Al eliminar el archivo, se elimina su entrada de directorio, por lo que du ya no tiene un nombre que recorrer y deja de contabilizarlo. El inode y sus bloques siguen asignados hasta que se cierra el último descriptor, y df cuenta los bloques asignados. Busque en /proc/<pid>/fd enlaces simbólicos cuyo destino esté marcado como eliminado y obtendrá tanto el archivo como el proceso que lo mantiene abierto. Antes de confiar en la comparación, confirme que ejecutó du como root y con -x, porque un usuario normal omite silenciosamente los directorios que no puede leer.

¿Cómo encuentro un archivo eliminado que todavía está abierto sin usar lsof?

Use el registro propio del kernel sobre los descriptores abiertos. sudo find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null muestra todos los descriptores que apuntan a un archivo sin nombre, y el ID del proceso aparece dentro de la ruta que imprime. sudo stat -Lc %s sobre una de esas rutas de descriptor informa de su tamaño, de modo que puede ordenarlas y elegir la relevante. Esto no requiere ningún paquete, algo importante porque instalar uno en un sistema de archivos sin espacio libre puede fallar.

¿Puedo liberar el espacio sin terminar el proceso?

A veces. sudo truncate -s 0 /proc/<pid>/fd/<n> accede al mismo inode a través del descriptor y libera sus bloques mientras el proceso sigue ejecutándose. Es la opción más limpia cuando el proceso abrió el archivo en modo de adición, porque sus escrituras siempre se realizan al final actual. Si no lo hizo, el desplazamiento de escritura permanece donde estaba y la siguiente escritura vuelve a crear el archivo con un hueco al principio, por lo que el tamaño informado vuelve a aumentar mientras los bloques situados bajo el hueco siguen libres. Reiniciar la unidad o enviarle la señal que indique su documentación para volver a abrir los registros es la solución que no deja un archivo disperso.

df muestra espacio libre, pero las escrituras siguen fallando. ¿Qué otras causas puede haber?

Compruebe los inodes con df -i en la misma ruta, ya que un sistema de archivos con bloques libres pero sin inodes disponibles rechaza los archivos nuevos. Compruebe si la escritura se ejecuta como un usuario que no es root en un sistema de archivos ext4 donde sólo quedan los bloques reservados; sudo tune2fs -l en el dispositivo lo mostrará. Compruebe que está leyendo el sistema de archivos al que realmente apunta la escritura, porque un /boot o /var independiente se llena por separado de /.

¿Por qué du informa de un total mayor que df?

du sin -x entra en todos los sistemas de archivos montados bajo la ruta indicada, por lo que suma varios sistemas de archivos mientras df describe uno solo. Los montajes bind lo empeoran, porque los mismos archivos se contabilizan una vez por cada ruta en la que aparecen. Añada -x para mantener du en un solo sistema de archivos y proporcione a df la misma ruta, de modo que ambos comandos describan lo mismo.