На Linux закончилось место, хотя df показывает свободные гигабайты: диагностика inode, удалённых файлов и Docker overlay2 | AdminWiki

На Linux закончилось место, хотя df показывает свободные гигабайты: диагностика inode, удалённых файлов и Docker overlay2

11 сентября 2026 15 мин. чтения
Содержание статьи

Запись на диск падает с ошибкой "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 /, и передайте его вендору или в сообщество профильных администраторов.

Поделиться:
Сохранить гайд? В закладки браузера