SSD Nodes Learn 🎉 VPS от $5.50/мес
Руководства Matt ConnorАвтор: Matt Connor

Почему df показывает диск заполнен, а du нет

Диск переполнен, но du не видит занятого места. Узнайте, как найти удаленные файлы, которые удерживаются открытыми процессами через lsof, и очистить пространство без перезагрузки.

Почему df показывает, что диск заполнен, а du — нет

df сообщает, что диск заполнен, в то время как du не может найти занятое место, потому что какой-то процесс удерживает файл, который был удален. Удаление файла убирает его имя из каталога. Блоки данных освобождаются только тогда, когда закрыт последний открытый файловый дескриптор, указывающий на этот inode. du обходит имена файлов, поэтому он ничего не учитывает. df запрашивает у файловой системы количество выделенных блоков, поэтому он продолжает учитывать файл, у которого больше нет имени.

Это руководство воспроизводит данную ситуацию на обычном Ubuntu VPS с использованием уже установленных инструментов, находит удерживающий процесс с помощью /proc и освобождает место без перезагрузки. Ниже описаны другие причины того же симптома: отсутствие свободных записей в таблице inode, файлы, скрытые под точкой монтирования, и блоки, зарезервированные для root.

Выполняйте каждую команду и читайте полученный вывод. Значения зависят от вашего диска, поэтому сравнивайте состояние «до» и «после» на своей машине, а не с цифрами, приведенными в руководстве.

Что подсчитывает df и что подсчитывает du

df (disk free) запрашивает у каждой смонтированной файловой системы её собственные данные: сколько блоков существует, сколько из них выделено и сколько свободно. Утилита не открывает каталоги. Ответ включает каждый выделенный блок, включая блоки, принадлежащие файлам, на которые не указывает ни одна запись в каталогах.

du (disk usage) делает обратное. Она начинает работу с указанного вами пути, читает каталоги, выполняет stat для каждой найденной записи и суммирует блоки. Файл без имени для неё невидим. Также невидимы каталоги, к которым утилита не имеет доступа, поэтому у обычного пользователя итоговый результат меньше, чем у root. Запустите du от имени sudo, прежде чем делать выводы на основе сравнения.

При сравнении этих утилит важны две опции.

  • -x ограничивает du одной файловой системой. Без этой опции du / переходит во все файловые системы, смонтированные внутри /, и выдает итоговое значение, которое df / не учитывала.
  • -s выводит одну итоговую строку для каждого аргумента вместо одной строки для каждого каталога.

Это позволяет запускать обе команды параллельно для нужной файловой системы.

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

df отвечает мгновенно. du может выполняться несколько минут на больших файловых системах, так как утилита выполняет stat для каждого файла. Если итоговые значения сильно различаются, а du была запущена от имени root с флагом -x, значит, недостающее место занято данными, у которых нет имени.

Воспроизведение несоответствия намеренно

Выполните это на тестовом VPS. Все нижеприведенное использует bash и coreutils, поэтому ничего устанавливать не нужно.

Запишите начальное состояние файловой системы, содержащей /var/tmp.

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

Вторая команда выводит количество использованных байтов без округления, что делает проверку в конце точной.

Теперь создайте файл. Его размер берется из свободного места, о котором сообщает сама машина, поэтому демонстрация подойдет для любого диска.

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

$(...) — это подстановка команды: оболочка выполняет команду внутри, и вывод становится значением free. Если этот синтаксис вам в новинку, подстановка команд в bash подробно его описывает. fallocate резервирует реальные блоки без записи данных, поэтому команда завершается мгновенно. На файловой системе, которая это не поддерживает, команда завершится ошибкой, и head -c $((free / 10)) /dev/zero > ghost.bin выполнит ту же задачу, записав байты на диск.

Сравните этот df -h . с тем, который вы записали ранее. Столбец использованного места увеличился, а столбец доступного — уменьшился.

Теперь удерживайте файл открытым другим процессом, а затем удалите его.

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

Перенаправление — это весь секрет. sleep infinity < ghost.bin & запускает фоновый процесс, стандартным вводом которого является этот файл, поэтому оболочка открывает файл и передает дескриптор sleep, который удерживает его открытым. $! содержит идентификатор процесса (PID) этой фоновой задачи. rm затем удаляет имя файла, пока дескриптор остается открытым.

Прочитайте вывод. ls не может найти файл, так как имя удалено. du вернулся к исходным значениям, так как он обходит имена файлов. df не изменился, так как блоки все еще распределены. Файловая система и дерево каталогов теперь не согласуются, и разрыв между ними — это файл, который вы только что удалили.

Поиск процесса, удерживающего удаленный файл

Каждый открытый файловый дескриптор отображается в /proc/<pid>/fd/ как символическая ссылка на файл, к которому он относится. Когда файл удален (unlinked), ядро помечает цель этой ссылки как удаленную. Таким образом, поиск процесса-держателя сводится к поиску ссылки, цель которой имеет такой признак.

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

find /proc//fd -lname ' (deleted)' 2>/dev/null

-lname сопоставляет цель символической ссылки, а не ее имя, %p выводит путь к дескриптору, а %l показывает, на что он указывает. Идентификатор процесса (PID) является вторым элементом выводимого пути. Запускайте команду с sudo, иначе вы сможете прочитать только /proc/<pid>/fd для собственных процессов. Перенаправление stderr отсеивает сообщения от процессов, которые завершились во время обхода find.

На нагруженном сервере в любой момент времени открыто несколько удаленных файлов, большинство из которых малы и безвредны. Отсортируйте их по размеру, чтобы наиболее значимые оказались вверху списка.

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

find /proc//fd -lname ' (deleted)' -printf '%p\t%s\n' 2>/dev/null | sort -k2 -n

stat -L переходит по ссылке к самому inode, поэтому %s сообщает размер файла, у которого больше нет имени. Сортировка по этому числу выводит самый большой файл первым.

Затем определите процесс, которому принадлежит нужный дескриптор. Путь в верхней части списка содержит оба необходимых числа, поэтому сначала сохраните их в переменные, заменив PID и N на значения из вашего вывода.

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

PID=...; N=...
ps -p $PID -o comm,etime
stat /proc/$PID/fd/$N

ps называет программу и показывает время ее работы. stat -L выводит размер и количество выделенных блоков удаленного inode. Вместе они отвечают на главный вопрос: какой сервис удерживает этот файл в памяти.

Если на машине уже установлен lsof, команда sudo lsof +L1 выведет список открытых файлов, количество ссылок на которые упало до нуля, и покажет их размеры в одной таблице. В минимальном образе Ubuntu этот пакет отсутствует, а установка пакета на файловую систему без свободного места может завершиться неудачей, поэтому метод с обходом /proc является универсальным и работает всегда.

Освобождение места без перезагрузки

Перезагрузка решает проблему, но это неверный первый шаг: она останавливает сервис и уничтожает улики. Существует четыре более щадящих способа, расположенных в порядке их применения.

Сначала скопируйте данные, если они вам еще нужны. Чтение по пути дескриптора позволяет считать данные из активного inode.

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

Это единственный случай, когда удаленный файл легко восстановить, поэтому руководство восстановление файлов, удаленных с помощью rm -rf начинается с вопроса о том, удерживает ли какой-либо процесс файл открытым. Как только последний дескриптор будет закрыт, этот путь будет потерян.

Второй способ — очистка файла через дескриптор. Путь /proc ведет к тому же inode, поэтому усечение файла освобождает блоки, пока процесс продолжает работу.

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

Этот метод работает корректно, если процесс открыл файл в режиме добавления (append mode), так как каждая запись в этом случае идет в текущий конец файла. Если это не так, процесс сохраняет старое смещение записи, поэтому следующая запись попадет далеко в конец файла и воссоздаст его с «дырой» в начале. «Дыра» не занимает места на диске, поэтому блоки остаются свободными, а df сохраняет только что освобожденное пространство. Восстанавливается только размер: если запустить sudo stat -L "/proc/$pid/fd/$n" снова после записи процесса, утилита покажет старый размер рядом с количеством блоков, которое ему уже не соответствует. Перезапустите процесс, если хотите, чтобы отсчет размера начался с нуля.

Третий способ — попросить сервис переоткрыть логи. Демон, у которого удалили файл лога, — распространенный реальный пример этой проблемы. Многие демоны переоткрывают файлы логов при получении сигнала: nginx использует SIGUSR1, а rsyslog — SIGHUP. Проверьте документацию для вашего демона, вместо того чтобы гадать, так как неверный сигнал, отправленный не тому демону, приведет к его остановке.

sudo systemctl kill -s USR1 nginx

Четвертый способ — перезапуск юнита. sudo systemctl restart <unit> закрывает все дескрипторы, которые удерживал старый процесс, поэтому блоки гарантированно возвращаются в систему. В приведенной выше демонстрации держателем является sleep, который вы запустили сами, поэтому его завершения достаточно.

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

Сравните количество использованных байтов со значением, которое вы записали до создания файла. Они снова совпадают, а find больше не показывает ваш дескриптор. Проверка с помощью той же команды, которая выявила проблему, — это полезная привычка.

Наблюдать за изменением этого значения проще, чем постоянно запускать df вручную. watch повторяет команду через заданный интервал и обновляет вывод на экране, поэтому watch df -h / покажет, как меняется столбец использованного места по мере его освобождения.

Когда итоговые значения совпадают, а диск всё ещё переполнен

Если df и root du -x показывают одинаковые результаты, значит, проблема не связана с удалёнными файлами. Оставшиеся причины имеют другую природу, и для каждой из них предусмотрена своя проверка.

Исчерпание инодов при наличии свободного места

Инод (inode) содержит метаданные одного файла. При создании файловой системы ext4 количество инодов фиксируется, поэтому файловая система может исчерпать иноды, даже если свободные блоки еще есть. В этом случае создание новых файлов завершается ошибкой, хотя df -h показывает наличие свободного места.

df -h /
df -i /

Первая команда подсчитывает блоки, а вторая — иноды. Сравните столбец использования (use) для каждой из них. Низкий уровень использования блоков при достижении лимита инодов означает, что проблема вызвана огромным количеством очень маленьких файлов.

df не позволяет использовать -i и --output в одном вызове. Если вам нужны «сырые» значения для чтения или передачи другой команде, выбирайте поля инодов по имени и не используйте -i.

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

Эти столбцы содержат те же данные, что выводит df -i, но в формате, удобном для обработки.

Найдите файлы, подсчитав количество записей, а не байтов.

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

Повторяйте эту команду для каждого уровня вложенности в каталоге, который оказался лидером по количеству файлов, пока не найдете дерево, создающее эти файлы. Если ваш du не поддерживает --inodes, используйте sudo find /var -xdev -type f | wc -l для медленного подсчета поддерева.

Решение заключается в удалении или перемещении этих файлов. Вы не можете увеличить количество инодов в существующей файловой системе ext4, так как их число фиксируется во время mkfs. Увеличение лимита требует пересоздания файловой системы и восстановления данных из резервной копии. XFS выделяет иноды по мере необходимости, поэтому она не имеет такого жесткого ограничения. Машина, на которой работают контейнеры, достигает обоих лимитов быстрее остальных, так как слои образов содержат множество мелких файлов. Для такой машины специфическим решением является очистка дискового пространства Docker на VPS, что позволяет освободить гораздо больше места, чем общая очистка файловой системы.

Скрытое пространство под точкой монтирования

В каталоге могут находиться файлы до того, как на него будет смонтирована файловая система. Если смонтировать файловую систему поверх такого каталога, файлы останутся на своих местах: они по-прежнему занимают место, учитываются командой df, но становятся недоступны по именам. Команда du не видит их, так как точка монтирования перекрывает их.

Продемонстрируем это с помощью tmpfs, для которой не требуется свободное место на диске. Для выполнения этой части необходима машина, на которой разрешено монтирование, поэтому пример подходит для 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

В середине вывода ls показан пустой каталог. Копия никуда не исчезла: она по-прежнему находится на корневой файловой системе и вернется в тот момент, когда вы выполните размонтирование. Теперь представьте сервис, который в течение месяца записывал логи по этому пути, прежде чем кто-то смонтировал поверх него том.

Чтобы найти скрытые данные на работающем сервере, смонтируйте корневую файловую систему второй раз в другом месте. Bind-mount позволяет увидеть файловую систему без учета вложенных в нее других файловых систем.

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

Все, что отображается в этом списке, но отсутствует по обычному пути, скрыто под точкой монтирования. Размонтируйте bind-mount после завершения работы, иначе последующий вызов du без флага -x учтет одни и те же файлы дважды.

Зарезервированные блоки для root

Файловая система ext4 резервирует часть блоков для пользователя root. Это позволяет администратору войти в систему и выполнить восстановление, даже если диск полностью заполнен. Процесс, запущенный от имени обычного пользователя, столкнется с нехваткой места раньше, хотя df все еще будет показывать небольшой остаток. Проверьте текущие настройки вашей файловой системы, вместо того чтобы полагаться на значения по умолчанию.

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

Эта команда выводит общее количество блоков и количество зарезервированных блоков в одних и тех же единицах измерения, поэтому их соотношение можно вычислить напрямую. df отображает в столбце доступного места (available) объем, который может использовать обычный пользователь. Именно поэтому сумма использованного и доступного места меньше общего размера раздела. Разница между ними — это и есть резерв.

Измените его с помощью sudo tune2fs -m <percent> "$dev". Изменения вступают в силу немедленно и не требуют перемонтирования. Уменьшение резерва на отдельной файловой системе для данных оправдано. На корневой файловой системе оставьте достаточно места, чтобы root мог записывать данные, так как полностью заполненную корневую файловую систему гораздо сложнее восстановить. tune2fs работает с ext2, ext3 и ext4. В XFS аналогичная настройка отсутствует.

Почему утилита du может вводить в заблуждение

Четыре особенности du приводят к получению итоговых значений, которые выглядят неверно.

  • Жесткие ссылки: du учитывает inode один раз, даже если на него указывают несколько имен, поэтому дерево каталогов с множеством жестких ссылок показывает размер меньше, чем сумма размеров входящих в него файлов.
  • Разреженные файлы: du сообщает о фактически выделенных блоках, тогда как ls -l показывает номинальный размер файла. Используйте флаг --apparent-size, чтобы увидеть другое значение.
  • Права доступа: при запуске от имени обычного пользователя du пропускает файлы, которые не может прочитать, и занижает итоговый размер. Ошибки, которые она выводит, пользователи часто перенаправляют в /dev/null и перестают их просматривать.
  • Границы файловых систем: без флага -x утилита du / учитывает все файловые системы, смонтированные внутри /, поэтому её итоговый результат может превышать данные, которые сообщает df /.

Утилита df также имеет особенность, о которой стоит знать. Она сообщает данные по каждой файловой системе отдельно, поэтому запускайте её для конкретного пути, на который приходится неудачная запись. Отдельный раздел /boot заполняется по мере накопления пакетов ядра, и удаление старых ядер в Ubuntu — это задача, отличная от очистки места на /.

Порядок действий при реальном инциденте

  1. Запустите df -h <path> и df -i <path> для файловой системы, на которую не удалось записать данные, а не для / по привычке.
  2. Запустите sudo du -xh -d 1 <mountpoint> 2>/dev/null | sort -h, а затем перейдите в самый большой каталог.
  3. Если du не может объяснить данные, которые df показывает как занятые, выполните поиск в /proc удаленных файлов, которые всё ещё открыты.
  4. Если показатели совпадают, выполните bind mount файловой системы в другое место и проверьте наличие файлов, скрытых под точкой монтирования.
  5. Если достигнут лимит использования inode, считайте количество файлов, а не их размер в байтах.

Каждый шаг здесь — это команда, вывод которой можно прочитать. В этом заключается разница между исправлением проблемы и догадками.

FAQ

Why does df show the disk as full when du finds much less?

The usual reason is a file that was deleted while a process still had it open. Removing the file removes its directory entry, so du has no name to walk and stops counting it. The inode and its blocks stay allocated until the last descriptor closes, and df counts allocated blocks. Search /proc/<pid>/fd for symbolic links whose target is marked as deleted and you have both the file and the process holding it. Before trusting the comparison, confirm you ran du as root and with -x, because an ordinary user silently skips directories it cannot read.

How do I find a deleted file that is still open without lsof?

Use the kernel's own record of open descriptors. sudo find /proc/[0-9]*/fd -lname '*(deleted)' -printf '%p -> %l\n' 2>/dev/null lists every descriptor pointing at a file with no name, and the process ID sits inside the path it prints. sudo stat -Lc %s on one of those descriptor paths reports its size, so you can sort them and pick the one that matters. This needs no package at all, which matters because installing one on a filesystem with no free space can fail.

Can I free the space without killing the process?

Sometimes. sudo truncate -s 0 /proc/<pid>/fd/<n> reaches the same inode through the descriptor and releases its blocks while the process keeps running. That is cleanest when the process opened the file in append mode, because its writes always go to the current end. If it did not, the write offset stays where it was and the next write recreates the file with a hole at the front, so the reported size springs back while the blocks under the hole stay free. Restarting the unit, or signalling it to reopen its logs with the signal its documentation names, is the fix that leaves no sparse file behind.

df shows free space but writes still fail. What else can it be?

Check inodes with df -i on the same path, since a filesystem with free blocks and no free inodes rejects new files. Check whether the write runs as a non-root user against an ext4 filesystem where only the reserved blocks are left, which sudo tune2fs -l on the device will show. Check that you are reading the filesystem the write actually targets, because a separate /boot or /var fills independently of /.

Why does du report a bigger total than df?

du without -x crosses into every filesystem mounted under the path you gave it, so it adds up several filesystems while df describes one. Bind mounts make it worse, because the same files are counted once under each path they appear at. Add -x to keep du on a single filesystem, and give df the same path, so both commands are describing the same thing.