Ошибка записи "No space left on device" выглядит однозначно: место кончилось. Администратор открывает /var, запускает подсчёт по дереву и получает противоречие: утилита обхода насчитывает девять гигабайт, а утилита по счётчикам файловой системы настаивает на девятнадцати. Обе правы, просто смотрят на раздел с разных сторон: одна идёт по именам из записей каталогов, другая читает таблицу свободных блоков в суперблоке. Ниже механика расхождения, четыре типовые причины невидимой занятости и порядок проверки, который приводит цифры к согласию.
Расхождение показателей df и du как отправная точка поиска пропавшего места
Диагностика начинается с двух снимков. Команда df читает счётчики из суперблока и отвечает мгновенно. Команда du идёт по записям каталогов и складывает объёмы файлов, до которых может дойти по именам, поэтому на большом разделе работает секунды и минуты. Ключ -x у du удерживает утилиту в границах одного тома, иначе в сумму попадут соседние точки монтирования:
df -h /var
du -shx /var
du -xh /var | sort -rh | head -25
Третья команда строит рейтинг подкаталогов: sort -rh понимает человекочитаемые размеры и ставит наверх самые крупные. На проблемном сервере снимки расходятся заметно:
Filesystem Size Used Avail Use% Mounted on
/dev/sdb1 20G 19G 0 100% /var
8.9G /var
Девятнадцать занятых гигабайт против неполных девяти в дереве. Если бы цифры сходились, ответ лежал бы в банальной чистке: тяжёлые журналы, кэши, слои контейнеров. Когда du насчитывает вдвое меньше, объём держит то, чего в обходе нет физически.
В строке df прячутся два независимых расхождения. Первое, Used против суммы дерева, только что измерено. Второе видно в арифметике: Used и Avail дают в сумме девятнадцать при разделе в двадцать гигабайт. Недостающий гигабайт это резерв файловой системы: в ext по умолчанию пять процентов объёма отводится пользователю root, и для остальных процессов эти блоки выглядят занятыми. Защита разумна, потому что обычная запись не способна довести раздел до состояния, при котором даже root не запишет строку в системный журнал. Проверка резерва на разделе семейства ext:
tune2fs -l /dev/sdb1 | grep -i "Reserved block"
Reserved block count: 262144
Reserved blocks uid: 0 (user root)
Двести шестьдесят две тысячи сто сорок четыре блока по четыре килобайта это ровно гигабайт. Поэтому картина "Used 19G, Avail 0" не означает полного отказа в записи: root ещё пишет, и у его служб остаётся запас хода. Размер резерва меняется tune2fs -m с числом процентов, а решение принимают по роли раздела: на корневом резерв бережёт систему от остановки, на разделе с журналами пять процентов часто простаивают без пользы.
Интерактивный разбор дерева каталогов через ncdu и границы его обзора
Утилита ncdu упаковывает логику du в интерактивный экран и убирает ручной перебор. Пакет лежит в репозиториях основных дистрибутивов и ставится одной командой, например apt install ncdu. Полная картина по /var требует запуска от root:
ncdu -x /var
После сканирования появляется список: размер записи, полоса доли относительно самого крупного соседа и имя, сортировка по убыванию. Навигация на одиночных клавишах: стрелки и Enter спускают в каталог и возвращают наверх, s и n переключают сортировку по размеру и имени, g меняет отрисовку доли, i показывает свойства записи, r пересчитывает каталог, d запускает удаление, q закрывает экран.
Клавиша d с журналами заслуживает осторожности: удаление растущего файла прямо из ncdu повторяет классическую ошибку, из-за которой место и не возвращается. Если служба пишет через открытый дескриптор, между rm и освобождением объёма лежит целый перезапуск. Живой тяжёлый журнал сначала обнуляют, затем разбирают причину роста:
truncate -s 0 /var/log/app/server.log
Обнуление возвращает блоки немедленно. Нюанс в режиме записи: журнал с флагом дописывания продолжит писаться с нуля, а файл без флага продолжит запись со старой позиции и превратится в разрежённый, у которого заявленный размер велик, а блоков под ним мало.
Переключатель a меняет способ измерения между занятыми блоками и заявленным размером из метаданных. Разница между режимами сама по себе диагностический признак: образ диска в разрежённом формате заявляет сотню гигабайт, а по блокам весит вдвое меньше; миллион мелких файлов наоборот, каждый съедает минимум один блок, и заявленный размер выходит скромнее реального расхода.
Что обычно набирает вес в /var. Каталог /var/lib хранит состояние служб: у контейнерного окружения это образы, слои и тома, быструю сводку даёт docker system df, а журналы контейнеров растут без ограничений, если у драйвера json-file не задан лимит в /etc/docker/daemon.json. Каталог /var/log собирает журналы: объём журнала systemd показывает journalctl --disk-usage, сжатие до заданного размера выполняет journalctl --vacuum-size=500M. В /var/cache кэши менеджеров пакетов, в /var/spool очереди почты и печати.
Границы обзора у ncdu те же, что у du: утилита идёт по именам из записей каталогов. Удалённый файл с открытым дескриптором, спрятанное под точкой монтирования содержимое и заполненная таблица inode остаются за кадром. Поэтому итог ncdu читают с одним вопросом: если сумма дерева заметно меньше занятости по df, дальше разбираются не каталогами, а дескрипторами и точками монтирования.
Удалённые файлы с открытыми дескрипторами держат место в разделе
Механика удаления файла в Linux расходится с бытовой интуицией. Команда rm убирает не файл, а имя: из каталога исчезает запись, счётчик ссылок inode падает до нуля, но метаданные и блоки остаются закреплёнными за ядром, пока открыт хотя бы один дескриптор. Файл теряет имя и становится невидимым для du, ncdu и любого поиска по пути, а занятые блоки остаются в счётчиках суперблока и честно отражаются в df. Куда девается место после удаления? Никуда: оно закреплено за inode до последнего дескриптора.
Сценарий почти всегда один. Служба журналирования держит открытый дескриптор на файл в /var/log, файл растёт, администратор видит заполнение и среди ночи удаляет его командой rm, чтобы вернуть место. Имя исчезает, объём не возвращается: служба продолжает писать через старый дескриптор, и занятость даже растёт, потому что записи ничего не мешает.
Проверить подозрение можно без сторонних утилит, через каталог /proc:
ls -l /proc/812/fd | grep deleted
lrwx------ 1 root 64 Jun 12 02:31 7 -> /var/log/syslog (deleted)
Строка с пометкой "(deleted)" означает дескриптор на файл без имени. Место под ним освободится в момент закрытия дескриптора: когда процесс завершится, будет перезапущен или откроет новый файл. Перезагрузка тоже закрывает дескрипторы, но это самый дорогой путь: она убирает симптом сразу на всех службах, а причина остаётся неразобранной.
Поиск удалённых открытых файлов через lsof и освобождение занятого места
Каталог /proc подсказывает, есть ли подозрение вообще, но для полной картины нужна утилита lsof. Ключ +L с числом включает в вывод колонку счётчика ссылок и оставляет только записи со значением меньше единицы, то есть удалённые открытые файлы. Отбор по /var делает обычный конвейер:
lsof +L1 | grep /var
На сервере из примера ответ находится сразу:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
rsyslogd 812 root 7w REG 8,17 10737418240 0 4242 /var/log/syslog (deleted)
Одна строка закрывает все десять гигабайт разницы. COMMAND называет держателя, PID даёт его номер, SIZE/OFF показывает объём невидимого файла, NLINK с нулём подтверждает удаление, NAME хранит последний известный путь. Освобождение сводится к закрытию дескриптора, для службы под systemd это перезапуск:
systemctl restart rsyslog
Одиночному процессу отправляют сигнал завершения командой kill -TERM 812, но перед этим стоит понять его роль: завершение пишущей службы останавливает и запись в её журнал. После перезапуска контрольный df -h /var показывает занятость около девяти гигабайт, а повторный lsof +L1 остаётся пустым.
Профилактика укладывается в нормальную нарезку журналов. Утилита logrotate после среза отправляет службе сигнал, та открывает новый файл по прежнему пути, а срезанный экземпляр сжимается и покидает сервер по расписанию:
/var/log/nginx/*.log {
weekly
rotate 10
compress
missingok
postrotate
systemctl reload nginx.service >/dev/null
endscript
}
Вариант copytruncate обходится без перезапуска: файл копируется, оригинал обнуляется, служба продолжает писать в тот же дескриптор. Плата за экономию времени это окно между копированием и обнулением, в котором параллельная запись способна потерять несколько строк.
Исчерпание таблицы inode при свободных байтах на разделе
Второй класс невидимой занятости зеркален первому: байты свободны, а записывать некуда. У файловой системы конечны два ресурса, блоки и записи таблицы inode. Каждая запись хранит метаданные файла и указатели на блоки, фиксируется в момент создания файловой системы и не расширяется на лету. Файлу любого размера нужна ровно одна запись, каталогу тоже. Когда таблица исчерпана, создание файла заканчивается той же ошибкой "No space left on device", хотя df -h показывает гигабайты свободного места. Показатели inode выведены отдельной командой:
df -i /var
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sdb1 1310720 1310711 9 100% /var
Одинаковые сообщения при разных причинах и есть главный подвох: различие видно только сравнением df -h с df -i. Заполненную таблицу почти всегда объясняет масса мелочи: сессии веб-приложений, где каждому посетителю отводится отдельный файл, почтовые хранилища с письмом на файл, очереди и кэши мелкими фрагментами. Найти каталог-рекордсмен помогает du с ключом --inodes, который считает записи вместо байтов:
du -x --inodes /var | sort -n | tail -10
Хвост вывода указывает на виновника:
1266643 /var/lib/php/sessions
1266721 /var/lib/php
1266847 /var/lib
1310711 /var
Миллион с четвертью файлов сессий. Очистка по возрасту выполняется find:
find /var/lib/php/sessions -type f -mtime +1 -delete
Команда уберёт файлы старше двух суток. Маска для rm не годится: конструкция rm /var/lib/php/sessions/* упрётся в ограничение длины списка аргументов и откажет с сообщением "Argument list too long", а find с -delete обрабатывает записи по одной и с лимитом не встречается. После очистки контрольный df -i показывает вернувшиеся записи, и раньше этого момента сообщения об отсутствии места не исчезнут.
Спрятанные под точкой монтирования файлы и способ увидеть скрытый объём
Третья причина расхождения живёт в устройстве монтирования. Точка монтирования накрывает каталог новым томом, прежнее содержимое остаётся лежать на нижнем разделе, но любой обход видит только верхний слой. du и ncdu честно обходят то, что смонтировано сверху, а df учитывает занятость каждого тома отдельно, и спрятанные файлы остаются в счётчиках нижнего раздела. Классический сценарий: том для контейнеров смонтировали поверх /var/lib/docker, а прежнее содержимое каталога осталось внизу; то же выходит с аварийной копией перед переносом.
Сначала смотрят карту точек монтирования:
findmnt -R /var
TARGET SOURCE FSTYPE OPTIONS
/var /dev/sdb1 ext4 rw,relatime
/var/lib/docker /dev/sdc1 ext4 rw,relatime
Разбор спрятанного слоя не требует размонтирования. Достаточно bind-копии того тома, на котором лежит накрытый каталог: у простой bind-копии вложенные точки монтирования не воспроизводятся, и под ней видно исходное содержимое. Если скрыт сам каталог /var, копируют корень:
mkdir -p /mnt/inspect
mount --bind / /mnt/inspect
du -shx /mnt/inspect/var
umount /mnt/inspect
Если накрыт вложенный каталог, копируют том /var: mount --bind /var /mnt/inspect, затем du -shx /mnt/inspect/lib/docker. Сумма du и есть спрятанный объём, дальше ненужное удаляют, нужное переносят на верхний слой. Размонтирование верхнего тома тоже открывает вид на спрятанное, но живой /var с работающими службами просто так не освободить.
Практический порядок проверки раздела /var от первой команды до очистки
Всё перечисленное складывается в последовательность на случай первого сообщения о заполнении. Шаги идут от секундных проверок к действиям с перезапуском служб. Полный разбор выглядит так:
- снять показания df -h /var и df -i /var и зафиксировать занятые байты и записи таблицы индексов;
- сверить их с суммой дерева du -shx /var и оценить масштаб расхождения;
- открыть ncdu -x /var, спуститься в самые тяжёлые ветки и найти реальных потребителей объёма;
- запустить lsof +L1 и отыскать удалённые файлы по нулевому счётчику ссылок;
- перезапустить держателя из колонки COMMAND и проверить по df -h возврат места;
- при исчерпании индексов найти каталог-рекордсмен командой du -x --inodes и вычистить мелочь по возрасту;
- просмотреть findmnt -R /var и при подозрении на спрятанный слой заглянуть под него через bind-копию;
- сверить сумму Used и Avail с размером раздела и при недостаче вспомнить о резерве;
- очистить найденное, повторить замеры и убедиться, что цифры сошлись.
Пара правил делает порядок надёжнее. Замеры до и после каждого действия, без них легко принять случайность за результат. И сначала понять, кто держит объём, только потом удалять: быстрое rm в сторону растущего журнала выглядит спасением ровно до момента, когда место не возвращается, а имена для поиска исчезают. Место в разделе не пропадает бесследно: оно занято либо файлом с именем, либо файлом без имени, либо содержимым под точкой монтирования, и у каждого состояния есть своя команда, которая его честно показывает.