Утренний мониторинг красный, df -h / показывает 100 процентов, база данных отказывается принимать записи, пакетный менеджер падает на полпути, а в чате разоряются пользователи. Первый инстинкт - перезагрузить сервер и посмотреть, что будет. Это худший из возможных ходов: место перезагрузка не освободит, а часть сервисов после неё может вообще не подняться, потому что при забитом корне не создаются pid-файлы, сокеты и временные каталоги. Правильный порядок другой: быстро высвободить гигабайты на живой системе, понять, кто съел место, и закрыть причину. Ниже рабочая процедура без перезагрузки, разбор ситуации с удалёнными файлами, которые держат место, и список типичных пожирателей.

Чем опасен корень занятый на сто процентов и где система прячет резерв

Переполненный корень бьёт по всему сразу. rsyslog молча выбрасывает события, PostgreSQL и MariaDB откатывают транзакции, потому что не могут записать в временные файлы, ssh при попытке входа сообщает "Could not create session", планировщик cron не в состоянии даже отправить письмо об ошибке. Хуже то, что при таком состоянии ломаются и сами инструменты лечения: makepkg, apt и dnf не могут распаковать архивы, а значит, поставить чистящую утилиту уже не выйдет.

На ext4 у непривилегированных пользователей действительно есть оговорка: при создании файловой системы mkfs.ext4 резервирует 5 процентов блоков под учётную запись root, посмотреть их можно командой tune2fs -l /dev/sda1, поле Reserved block count. Пока есть резерв, сеанс root ещё может писать, а сервисы под обычными учётками - нет. Снижать резерв командой tune2fs -m 1 имеет смысл на больших дисках, где 5 процентов превращаются в сотни гигабайт, но полагаться на этот карман в работе нельзя, он для аварийного сеанса.

Отдельная ловушка - индексные дескрипторы. Команда df -i показывает IUse, и иногда IUse равен 100 процентам при свободном месте: раздел забит миллионами мелких файлов вроде почтовых ящиков, сессий PHP или кеша. Классический df врёт и в другую сторону: файл мог быть удалён, а место за ним числится, пока какой-нибудь процесс держит дескриптор открытым. Прежде чем искать виновника в каталогах, разберитесь, что именно переполнилось: блоки или дескрипторы.

На XFS резерва для root нет вовсе, забитый раздел останавливает всех одинаково, зато у файловой системы есть квоты по проектам: xfs_quota позволяет ограничить конкретный каталог даже без отдельных разделов. И пока вы работаете, не трогайте вслепую /var/lib: удалённый наугад каталог базы или реестра пакетного менеджера превращает переполнение в переустановку. Чистите только то, в чём уверены: журналы, кеши, дампы, временные каталоги.

Экстренная зачистка на живом сервере без перезагрузки

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

journalctl --disk-usage
journalctl --vacuum-size=500M

Чтобы журнал снова не разросся, ограничьте его в /etc/systemd/journald.conf параметром SystemMaxUse=200M и перезапустите службу systemd-journald - это безопасно и не отрывает текущие события.

Порядок безопасной зачистки, когда каждый гигабайт на счету, такой:

  1. подрежьте журнал командой journalctl --vacuum-size=500M, эффект мгновенный;
  2. очистите кеш пакетного менеджера: apt clean либо dnf clean all;
  3. снимите старые ядра и ненужные зависимости через apt autoremove --purge или dnf autoremove;
  4. осмотрите /var/lib/systemd/coredump и /var/crash, там лежат дампы по несколько гигабайт;
  5. проверьте Docker, если он есть: docker system prune -af снимет висящие слои, образы и остановленные контейнеры.

Для Docker есть и точечный инструмент статистики: docker system df показывает, сколько занимают образы, контейнеры, тома и кеш сборки. На нагруженных машинах, где образы пересобирают из непрерывной поставки, именно кеш сборки съедает сотни гигабайт, и тогда выручает docker builder prune -af.

Переполнение дескрипторов и особые случаи XFS и btrfs

Если df -i показал IUse 100 процентов, место ищут не в размере, а в количестве файлов. Утилита du умеет считать дескрипторы так же, как гигабайты:

du --inodes -x --max-depth=1 / | sort -n | tail -20

Обычно виноваты каталоги сессий вроде /var/lib/php/sessions, почтовые хранилища с миллионами маленьких писем или кеш микросервисов, который чистят по расписанию, а не по размеру. Для /var/tmp и /tmp спасает встроенная чистка:

systemd-tmpfiles --clean

У btrfs своя специфика: место заканчивается не только в блоках данных, но и в чанках метаданных, и тогда запись падает при кажущейся свободе. Посмотреть раскладку помогает btrfs filesystem usage /, а освободить занятые чанки - btrfs balance start -dusage=50, операция небыстрая, её запускают в окно низкой нагрузки. Если корень на btrfs лежит на LVM и том только что расширен, файловая система сама не растёт: скажите ей об этом командой btrfs filesystem resize max /.

Удалённые файлы держащие место открытым дескриптором

Ситуация знакома каждому второму администратору: в /var/log лежал гигантский access.log, файл удалили, df показывает 100 процентов, du по каталогу - пару сотен мегабайт. Файл действительно удалён из каталога, но процесс держит его дескриптор, и блоки числятся занятыми до закрытия. Найти виновника:

lsof +L1

Флаг +L1 выводит файлы с количеством ссылок меньше одной, то есть удалённые, но открытые. Колонка SIZE/OFF подскажет размер, FD - номер дескриптора, COMMAND - кто держит. Чаще всего это rsyslog, контейнер или база, которых забыли перезапустить после ротации.

Безопасное решение - перезапустить службу, удерживающую дескриптор: systemctl restart rsyslog. Для несущественного процесса достаточно снять его. Экзотический путь - обнулить файл через дескриптор прямо в /proc:

: > /proc/12345/fd/4

Такой фокус работает только с обычными файлами и ровно с тем номером дескриптора, который показал lsof; ошибка в номере обнулит чужой файл. Корневая причина обычно в logrotate: если в правиле нет copytruncate, файл ротации переименовывается, а процесс продолжает писать в старый дескриптор, и место утекает бесшумно.

Отсюда же растёт миф о волшебной перезагрузке: при выключении все процессы завершаются, дескрипторы закрываются, место появляется, и администратор решает, что сервер "починился". Ничего не починилось: заполненный файл просто перестал существовать. Урок ложный, и он дорог - следующее переполнение случится на неделю позже, но с тем же финалом.

Для растущего лога, который держит процесс, правильнее не rm, а обрезка на месте:

truncate -s 0 /var/log/app/access.log

Каталог остаётся нетронутым, процесс продолжает писать в тот же дескриптор, а место возвращается сразу. Именно так стоит поступать с логами баз и веб-серверов в продакшене, где перезапуск сервиса дороже минуты уборки.

Поиск виновника переполнения через du ncdu и фильтры по размеру

Когда первые гигабайты освобождены, начинается следствие. Обход начинается сверху:

du -xh --max-depth=1 / | sort -h | tail -20

Флаг -x не даёт спускаться в смонтированные файловые системы, иначе счёт утащит вас в сетевые шары и внешние диски. Ходите по каталогам, пока не найдете виновника. Интерактивный вариант ncdu -x / показывает структуру с сортировкой и позволяет удалять файлы прямо из интерфейса.

Тонкий момент: если подозрительный каталог скрыт под чужой точкой монтирования, классический du его не увидит. Приём с bind-монтированием показывает корневую файловую систему в чистом виде:

mount --bind / /mnt/rootview
du -xh --max-depth=1 /mnt/rootview | sort -h | tail -20
umount /mnt/rootview

Крупные файлы ищутся напрямую:

find / -xdev -type f -size +200M -printf '%s %p\n' | sort -n | tail -20

Так находят забытые дампы баз, пятигигабайтные access-логи и выгруженные архивы, о которых никто не помнил. Обратите внимание на свежесть: файл, изменённый сегодня, и файл двухлетней давности требуют разного разговора с командой.

Для охоты на свежие файлы есть фильтр по времени:

find / -xdev -type f -size +100M -newermt "3 days ago" -printf '%s %p\n' | sort -n

Свежий гигантский файл почти всегда чей-то недавний эксперимент: разработка выгрузила дамп прямо в /root, аналитик положил архив в /tmp, скрипт резервного копирования начал писать копию на тот же диск, который копирует. Второй полезный приём - два прохода du по одному каталогу с интервалом в десять минут: разница между проходами показывает, кто растёт прямо сейчас, это быстрее, чем гадать по датам.

Типичные пожиратели места от журналов до слоёв Docker

У переполнения почти всегда конкретный автор. Журналы сервисов в /var/log растут из-за зацикленной ошибки, которая пишется тысячу раз в минуту: одна строка невинна, а за сутки накапливается гигабайт. Проверьте конфигурацию ротации в сухом режиме: logrotate -d /etc/logrotate.conf не меняет ничего и сразу показывает, что конфигурация не покрывает какой-то файл.

Базы данных копят собственные журналы: /var/lib/mysql забит binlog-файлами, которые обрезаются параметром expire_logs_days или командой PURGE BINARY LOGS, у PostgreSQL /var/lib/postgresql/pg_wal раздувается при остановившейся репликации. Docker добавляет образы с пометкой <none>, анонимные тома и json-логи контейнеров: каталог /var/lib/docker/containers хранит по файлу лога на каждый контейнера, и без настроенного лимита они растут без конца.

Вспомогательные системы тоже в списке подозреваемых: snap хранит по три старые ревизии каждого пакета (проверка snap list --all, снятие лишних через snap remove --revision, ограничение snap set system refresh.retain=2), flatpak копит ревизии до flatpak uninstall --unused, аварийные дампы ядер лежат в /var/lib/systemd/coredump и /var/crash. Кеш инструментов собирается незаметно: /root/.cache разрастается от сборок и загрузок пакетов, чистится быстро через pip cache purge или простым удалением подкаталогов. Очередь почты в /var/spool/postfix растёт, когда исходящие письма копятся без доставки, проверить её можно командой mailq, а не оценкой размера каталога.

Отдельная категория - человеческий фактор: бэкапы, положенные в /root или /var/tmp "на время", архивы от разработчиков, тестовые выгрузки. Такие файлы не подпадают ни под одну автоматическую чистку, и только регулярный проход du по крупным каталогам держит их в поле зрения.

Как не допустить нового переполнения корневого раздела

Горизонтальное правило: всё растущее уносится с корня. Журналы - под лимит SystemMaxUse, контейнерные логи - под max-size в /etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}

После правки нужен перезапуск Docker-службы, проверьте окно обслуживания заранее. Мониторинг должен сигнализировать на 80 процентах, а не на 99: у node_exporter и zabbix-agent стандартные метрики использования файловых систем, порог 85 процентов на корне - обычная практика.

Когда рост запланирован, корень расширяют без переустановки. Классическая схема на LVM:

pvcreate /dev/sdb
vgextend vg_root /dev/sdb
lvextend -r +50G /dev/vg_root/root

Флаг -r расширяет файловую систему следом за томом, для ext4 и XFS он работает сам. Раз в квартал полезно устраивать сухую проверку: logrotate -d, journalctl --disk-usage и один проход du -x по большим каталогам с записью результата в журнал администрирования. Пять минут плановой ревизии дешевле ночной тревоги, когда база остановилась, а место закончилось.

Структурное решение - вынести растущее за пределы корня: /var/log и /var/lib/docker заслуживают отдельных томов или разделов, тогда единичный сбой ротации не лишает систему права писать в /etc и pid-каталоги. Для /tmp хорошо работает tmpfs с фиксированным размером, это жёсткий предел по определению: когда tmpfs заполнен, туда просто ничего не пишет, а корень остаётся живым. И последнее: проверку по расписанию добавляйте и для дескрипторов, cron с короткой записью вывода df -hi в журнал администрирования закрывает слепую зону, в которую попадает большинство внезапных остановок.