Запись на диск падает с ошибкой "No space left on device", а df -h показывает 20 свободных гигабайт. Для production-сервера это штатная ловушка: утилита читает суперблок файловой системы и не проверяет, может ли процесс реально создать файл.
Свободные блоки не равны доступному месту. Запись не пройдёт по шести причинам: закончились inode, место держат удалённые, но всё ещё открытые файлы, ext4 отдала 5% блоков в резерв root, путь записи ведёт на другой раздел через bind mount или overlay, журнал systemd вырос до десятков гигабайт, слои Docker overlay2 заняли раздел целиком. У каждой причины свой набор команд и свой порядок проверки.
Двигайтесь от быстрого к глубокому. df -i и lsof +L1 отвечают за секунды, docker system df и journalctl --disk-usage за пару секунд, а полный обход дерева через du --inodes на сервере с миллионами файлов может идти десятки минут. Начинать с обхода - значит потерять время и не найти причину.
Почему df показывает свободные гигабайты, а запись на диск невозможна
Ядро возвращает ENOSPC не только при нехватке блоков данных. Тот же код ошибки приходит при исчерпании inode, при попытке непривилегированного процесса занять зарезервированные блоки, при записи в файловую систему, которая смонтирована только для чтения после ошибки ввода-вывода. Диагностика обязана различать эти состояния, иначе очистка диска превращается в угадывание.
Первые три проверки дают ответ в большинстве инцидентов: df -i, lsof +L1 и findmnt -T для конкретного пути записи. Остальные шаги нужны, когда эти три чистые.
Чем df отличается от du и почему это важно при диагностике
df читает суперблок: сколько блоков и inode выделено, сколько занято, сколько свободно. Обход каталогов не выполняется, поэтому результат приходит мгновенно. du рекурсивно проходит дерево и суммирует занятые блоками данные каждого файла. Разные механики дают разные цифры, и расхождение несёт информацию.
Типовой пример: df -h показывает 20 ГБ свободных на корневом разделе, а du -xsh / возвращает 5 ГБ. Разницу в 15 ГБ объясняет один из факторов:
- удалённые файлы, которые удерживают открытые дескрипторы процессов (du их не видит вообще);
- снапшоты ZFS, LVM или btrfs, ссылающиеся на старые блоки;
- слои Docker overlay2 в /var/lib/docker;
- зарезервированные блоки ext4, недоступные непривилегированным процессам;
- жёсткие ссылки: du считает файл один раз, df учитывает все занятые блоки;
- sparse-файлы: логический размер больше числа реально выделенных блоков.
Расхождение df и du это сигнал, а не баг. Сопоставление показаний и готовые рецепты поиска крупных файлов разобраны в материале шпаргалка по df и du: диагностика дискового пространства в Linux.
Быстрый чек-лист: с чего начать диагностику за 60 секунд
Шесть команд, каждая выполняется секунды и сужает круг причин.
- df -h - свободное место по всем точкам монтирования.
- df -i - занятость inode. Колонка IUse% важнее, чем кажется на первый взгляд.
- findmnt -T /path/to/write - в какую файловую систему и точку монтирования реально ведёт путь.
- lsof +L1 2>/dev/null | head -50 - удалённые, но открытые файлы с указанием PID и размера.
- docker system df - размеры образов, контейнеров, томов и кэша сборки.
- journalctl --disk-usage - сколько занимает журнал systemd.
Интерпретация: IUse% = 100 при свободных блоках означает исчерпание inode. Непустой вывод lsof +L1 объясняет, почему удаление файлов не вернуло место. Большое значение RECLAIMABLE в docker system df показывает объём, который освободит очистка. Если все шесть проверок чистые, проверяйте reserved blocks и монтирование по конкретному пути записи.
Исчерпание inode: когда место есть, а файлы не создаются
inode хранит метаданные файла: тип, права, владельца, временные метки, счётчик ссылок, указатели на блоки данных. Имя файла лежит в каталоге, а не в inode. На ext4 число inode фиксируется при создании файловой системы: mkfs.ext4 -N 1000000 задаёт общее количество, mkfs.ext4 -i 16384 - один inode на каждые 16 КБ. Колонка IFree в df -i показывает остаток этого лимита.
XFS создаёт inode динамически и держит под них часть файловой системы, параметр imaxpct по умолчанию равен 25% для крупных разделов. Упереться в лимит там сложнее, но при миллионах мелких файлов это реально.
При IUse% = 100 ядро возвращает ENOSPC на любой вызов, создающий новый объект: open с флагом O_CREAT, mkdir, mknod, symlink. Запись в уже существующий файл при этом работает, поэтому сервис может продолжать писать логи и создавать видимость нормальной работы.
Типовые виновники: очереди почты в /var/spool/postfix, кэш пакетных менеджеров в /var/cache, сессии PHP в /var/lib/php/sessions, каталог /var/lib/docker с тысячами мелких файлов слоёв, node_modules в каталогах сборки, каталоги .cache в домашних директориях, ротационные логи в /var/log, временные файлы в /tmp.
Как найти каталог-виновник по количеству inode
Начните с корня и спускайтесь по самому тяжёлому каталогу. Флаг -xdev не даёт перейти на другие файловые системы, иначе вывод засорят proc, sys и tmpfs.
du --inodes -x --max-depth=1 / | sort -n
Дальше подставьте найденный каталог вместо корня и повторите. Если нужно увидеть распределение файлов по каталогам напрямую, считайте их через find:
find / -xdev -type f -printf '%h\n' 2>/dev/null | sort | uniq -c | sort -rn | head -20
Второй вариант даёт точные числа, но на диске с десятками миллионов файлов может идти долго. Запускайте его под ionice, чтобы не забить дисковую очередь.
Как безопасно удалить миллионы мелких файлов
rm -rf на дереве с миллионами файлов работает часами, создаёт лавину операций ввода-вывода и роняет отзывчивость сервера. Практичнее точечное удаление через find, которое не держит в памяти список файлов:
find /path -xdev -type f -delete ionice -c2 -n7 nice -n19 find /path -xdev -type f -delete
Альтернатива без перебора каждого файла: синхронизация с пустым каталогом. rsync строит список и удаляет лишнее пакетами, нагрузка распределяется ровнее.
mkdir -p /tmp/empty && rsync -a --delete /tmp/empty/ /path/
Для частичной очистки по возрасту подходит find с -mtime или -atime. После любой очистки повторите df -i. Если IFree не вырос, файлы всё ещё держат открытые дескрипторы, и это уже следующий сценарий.
Предупреждение: удаление запускайте под nice и ionice, не с -9 и не на боевом пике нагрузки.
Удалённые, но всё ещё открытые файлы: невидимый пожиратель места
unlink убирает имя файла из каталога и уменьшает счётчик ссылок. Блоки данных ядро освобождает только после закрытия последнего дескриптора. Пока процесс держит файл открытым, место остаётся занятым, а du его не находит, потому что обходит дерево каталогов, где файла уже нет.
Классический сценарий: администратор удаляет access.log на 5 ГБ, чтобы вернуть место. Сервис продолжает писать в тот же дескриптор, файл растёт дальше, df по-прежнему показывает занятый раздел. Место не вернулось, а лог потерян.
Как найти процесс, удерживающий удалённый файл
Первый инструмент - lsof с фильтром по счётчику ссылок меньше единицы. Вывод содержит команду, PID, пользователя, размер и имя файла с пометкой (deleted).
lsof +L1 2>/dev/null | head -50 lsof 2>/dev/null | grep -i deleted
Если lsof не установлен, работает обход /proc напрямую. Ядро показывает символические ссылки дескрипторов, у удалённых файлов в цели стоит " (deleted)".
find /proc/*/fd -lname '* (deleted)' 2>/dev/null
Колонка SIZE/OFF покажет объём, который удерживается. Чаще всего в списке оказываются nginx, postgres, java, docker daemon, rsyslogd и приложения, которые пишут собственные логи без ротации.
Как освободить место без перезапуска сервиса
Самый безопасный путь - перезапуск или посыл сигнала, после которого сервис переоткрывает файлы: nginx -s reopen, kill -USR1 для ряда сервисов, systemctl reload. Дескриптор закрывается, место освобождается, downtime минимален.
Когда перезапуск недопустим, остаётся обнуление файла через дескриптор. Место возвращается сразу, процесс продолжает писать в тот же файл.
truncate -s 0 /proc/PID/fd/N
Ограничения важны. Если приложение хранит собственный указатель записи, после обнуления появится разреженная дыра: логический размер останется большим, а занятые блоки будут маленькими. Для append-only логов это обычно безопасно. Для файлов баз данных, журналов транзакций и структур с фиксированными смещениями обнуление может закончиться порчей данных. Для БД выбирайте перезапуск или штатное переключение WAL.
Третий вариант - logrotate с директивой copytruncate: содержимое копируется, исходный файл обнуляется, сервису не нужно переоткрывать дескриптор. Подходит для приложений, которые не умеют обрабатывать сигнал переоткрытия.
Не применяйте kill -9 к процессам баз данных ради быстрого возврата места.
Docker overlay2 и журналы systemd: скрытые потребители места
Контейнерные окружения и systemd дают два источника, которые легко пропустить: слои overlay2 в /var/lib/docker и журнал journald. Оба растут постепенно и оба могут занять больше сотни гигабайт на активном хосте.
Как оценить и очистить слои Docker overlay2
Начните с общей картины. df -h и docker system df при обычном переносе строк выдают проблему сразу:
df -h /var/lib/docker docker system df docker system df -v
Таблица показывает размеры и колонку RECLAIMABLE по четырём категориям: Images, Containers, Local Volumes, Build Cache. Раздувается одна из них. Диагностика и очистка образов с сохранением тегов, которые нужны для откатов, разобраны в статье управление Docker-образами в проде: команды очистки, хранение слоёв и обновление Ubuntu/Alpine.
Дальше по возрастанию риска:
- docker container prune - удаляет остановленные контейнеры;
- docker image prune - удаляет образы без тегов (dangling);
- docker builder prune - очищает кэш сборки;
- docker system prune - контейнеры, сети, dangling-образы и кэш сборки;
- docker image prune -a - все образы, не используемые запущенными контейнерами;
- docker system prune -a --volumes - то же плюс неиспользуемые тома.
Два предупреждения. docker image prune -a снесёт базовые образы, и при следующей сборке их придётся тянуть из реестра заново. docker volume prune удаляет данные томов безвозвратно, а для stateful-сервисов это потеря состояния. Перед очисткой томов проверьте, какие из них подключены к остановленным контейнерам. Полный порядок действий с проверками описан в материале полная очистка Docker: удаление образов, контейнеров и томов.
Не удаляйте /var/lib/docker вручную через rm -rf. Метаданные демона разойдутся с содержимым каталогов, и Docker Engine придётся восстанавливать переустановкой. После очистки проверьте результат: docker system df и df -h /var/lib/docker.
Как ограничить рост журналов systemd
journalctl --disk-usage показывает текущий объём. Очистка выполняется вакуумом, без удаления файлов вручную:
journalctl --vacuum-size=500M journalctl --vacuum-time=7d journalctl --vacuum-files=5
Разовое удаление вернёт место, но проблема вернётся через неделю. Ограничения задаются в /etc/systemd/journald.conf: SystemMaxUse=500M, SystemMaxFileSize=50M, MaxRetentionSec=1week, SystemKeepFree=1G. После правки нужен systemctl restart systemd-journald.
Учитывайте два хранилища. Постоянное лежит в /var/log/journal и переживает перезагрузку, временное в /run/log/journal живёт в памяти. Если каталог /var/log/journal отсутствует, журнал не сохраняется между запусками, и место освобождается само.
Зарезервированные блоки и неправильно смонтированные разделы
ext4 по умолчанию держит 5% блоков в резерве для процессов с правами root. На разделе 2 ТБ это около 100 ГБ, недоступных обычному пользователю. df -h показывает эти блоки как занятые, и цифра свободного места для сервиса под непривилегированной учётной записью будет меньше, чем в выводе утилиты.
Как проверить и изменить reserved blocks на ext4
Сначала посмотрите текущие значения и посчитайте процент вручную.
tune2fs -l /dev/sda1 | grep -iE 'Reserved block count|Block count'
Снижение резерва до 1% освобождает почти всё зарезервированное место:
tune2fs -m 1 /dev/sda1
Для корневого раздела в проде оставьте резерв. Он нужен, чтобы root и системные сервисы могли писать при заполнении диска, а также чтобы фрагментация не съела рабочий запас. Для разделов с данными, логами, бэкапами и каталогом /var/lib/docker снижение до 1% оправдано. XFS резервирование блоков для root не использует, там этот параметр проверять бессмысленно.
Как убедиться, что запись идёт в тот раздел, который вы проверяете
Приложение пишет в /var/lib/app/data, вы смотрите df -h /, место есть, а запись падает. Причина в том, что путь ведёт на другой раздел через bind mount, overlay или отдельную точку монтирования.
findmnt -T /var/lib/app/data df -h /var/lib/app/data mount | grep -E 'bind|overlay'
findmnt -T показывает файловую систему, точку монтирования и опции для конкретного пути, поэтому именно эта команда закрывает вопрос. Отдельно проверяйте tmpfs: df -h -t tmpfs выведет разделы в оперативной памяти, которые переполняются независимо от свободного места на диске. В /tmp, /run и /dev/shm они встречаются чаще всего.
Вторая ловушка - перекрытый каталог. Если поверх непустого каталога смонтирован другой раздел, файлы под ним не видны в ls и не учитываются в du, но место на исходном разделе продолжают занимать.
Пошаговый runbook: как освободить место и не потерять данные
Порядок шагов важнее отдельных команд. Начните с фиксации исходного состояния, дальше двигайтесь от безопасных операций к рискованным и после каждого шага проверяйте результат.
df -h df -i
Запишите оба вывода до начала работ. Без точки отсчёта невозможно понять, освободилось место или файлы создались заново.
Порядок очистки: от безопасного к рискованному
Уровень 1, безопасно. Эти операции не удаляют данные приложений.
- journalctl --vacuum-size=500M - срезает журнал systemd до лимита;
- docker system prune -f - остановленные контейнеры, dangling-образы, кэш сборки, неиспользуемые сети;
- logrotate -f /etc/logrotate.conf - принудительная ротация настроенных логов;
- find /tmp -xdev -type f -atime +7 -delete - старые временные файлы;
- apt-get clean, dnf clean all или yum clean all - скачанные пакеты в кэше пакетного менеджера.
Уровень 2, средний риск. Требует понимания, какие образы и снапшоты понадобятся.
- docker image prune -a и docker builder prune --filter "until=168h";
- apt autoremove --purge - старые версии ядра и зависимости;
- удаление снапшотов ZFS, LVM или btrfs после проверки политики хранения.
Уровень 3, высокий риск. Эти действия могут закончиться потерей данных.
- truncate -s 0 /proc/PID/fd/N - обнуление удалённого открытого файла;
- docker volume prune - удаление неиспользуемых томов вместе с данными;
- tune2fs -m - изменение резерва блоков;
- удаление файлов из /var/lib вручную.
Для каждого действия держите в голове, что оно освобождает и что может сломать. Очистка журнала не влияет на приложение, удаление тома стирает состояние базы.
Как проверить результат и убедиться, что место действительно освободилось
После каждого шага повторяйте df -h и df -i. Если место не изменилось, возможны четыре объяснения: файлы всё ещё открыты (повторите lsof +L1), сервис создал файлы заново (проверьте du по рабочему каталогу), освободилось меньше ожидаемого из-за reserved blocks, место держат снапшоты. Для контейнерных хостов добавьте контроль docker system df.
Удаление файла и возврат места это разные события. Проверяйте именно цифры в df, а не факт выполнения команды.
Профилактика: как не наступить на эти грабли снова
Разовая очистка возвращает сервер в строй на несколько недель. Дальше нужны лимиты и мониторинг, иначе инцидент повторится под нагрузкой и в неудобное время.
Настройка лимитов journald и logrotate
Для journald достаточно четырёх строк в /etc/systemd/journald.conf: SystemMaxUse=500M, SystemMaxFileSize=50M, MaxRetentionSec=1week, SystemKeepFree=1G. После правки: systemctl restart systemd-journald.
Для приложений настройте /etc/logrotate.d/ с директивами daily, rotate 7, compress, delaycompress, missingok, notifempty и maxsize 100M. Директиву copytruncate применяйте только к сервисам, которые не переоткрывают дескриптор по сигналу: копирование больших файлов создаёт лишнюю нагрузку на диск. Для nginx, postgres и syslog правильнее create вместе с postrotate и сигналом переоткрытия. Проверка конфигурации без выполнения: logrotate -d /etc/logrotate.conf.
Пример конфигурации logrotate, cron-задания для регулярной очистки и схемы мониторинга собраны в руководстве борьба с утечками дискового пространства: логи, снапшоты, Docker-образы.
Ограничение роста Docker и мониторинг inode
Логи контейнеров по умолчанию пишутся драйвером json-file без ограничения размера и лежат в /var/lib/docker/containers. Одна болтливая служба съедает десятки гигабайт. Ограничение задаётся в /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" }
}
После правки нужен systemctl restart docker, но параметры применяются только к новым контейнерам. Существующие придётся пересоздать, иначе они продолжат писать без лимита. Регулярная уборка ставится в cron: docker system prune -f --filter "until=168h" раз в неделю.
Метрики для алертов: node_filesystem_avail_bytes (свободное место), node_filesystem_files_free и node_filesystem_files (inode), плюс размер /var/lib/docker. Пороги, которые ловят проблему заранее: свободное место меньше 10-15%, IUse% выше 80%. Для ZFS добавьте контроль zfs list -o space и политику хранения снапшотов с автоматической очисткой.
Чек-лист для нового сервера: проверить reserved blocks, задать лимиты journald, настроить logrotate для всех сервисов, ограничить логи Docker, подключить мониторинг места и inode. Если конфигурацию нужно проверить на чистой машине и не рисковать боевым хостом, разверните тестовую ВМ в облаке, например Timeweb Cloud с почасовой оплатой и гибким изменением ресурсов.
Крайние случаи: когда ничего из перечисленного не подошло
Если все проверки чистые, а запись всё равно падает, остаются аппаратные проблемы, повреждения файловой системы и скрытые потребители места на уровне подсистем хранения.
Проверка здоровья диска и файловой системы
Начните с ядра. Ошибки ввода-вывода и сообщения о переводе файловой системы в режим только для чтения объясняют ENOSPC лучше любых утилит.
dmesg -T | grep -iE 'I/O error|EXT4-fs error|XFS.*error|remount' smartctl -a /dev/sda | grep -iE 'Reallocated_Sector_Ct|Pending_Sector|Uncorrectable|Power_On_Hours' fsck -n /dev/sda1
Проверка fsck выполняется только на размонтированном разделе, для корневого это rescue-режим или live-носитель. Флаг -n означает проверку без изменений. Для XFS аналог: xfs_repair -n /dev/sda1. Для NVMe вместо smartctl используется nvme smart-log /dev/nvme0.
Файловая система, перешедшая в read-only после ошибок ядра, это отказ диска или повреждение структур, а не нехватка места. Порядок восстановления тома с данными в контейнерах описан в статье ремонт файловых систем в контейнерах: восстановление persistent storage в Docker и Kubernetes.
Когда дампов и журналов накопились сотни мегабайт и нужно быстро вытащить повторяющиеся ошибки, срезы удобно отправлять в языковую модель через единый API, например AiTunnel, который даёт доступ к GPT, Gemini и Claude по одному ключу.
Снапшоты, квоты и immutable-файлы
Снапшоты занимают место, но не видны в du и часто не отражаются корректно в df по конкретной точке монтирования.
- ZFS: zfs list -t snapshot -o name,used,refer, zfs list -o space, колонка USEDSNAP;
- LVM: lvs -a -o +lv_size,lv_attr, снапшот растёт по мере изменения исходного тома;
- btrfs: btrfs subvolume list /, btrfs filesystem usage /, btrfs qgroup show.
Квоты объясняют ситуации, когда место на разделе есть, а конкретный пользователь или том его не получает: repquota -a, xfs_quota -x -c 'report -h' /. Неизменяемые файлы мешают очистке молча: lsattr -R /path | grep i покажет атрибут, chattr -i снимет его.
Дополните проверку списком смонтированных loop-устройств (losetup -a) и разделов в памяти (df -h -t tmpfs). Если после всего этого картина остаётся неясной, соберите единый пакет данных: df -h, df -i, findmnt, lsof +L1, docker system df, journalctl --disk-usage, du -x --max-depth=1 /, и передайте его вендору или в сообщество профильных администраторов.