kai.kzLab

Закончилось место на диске в Linux: как найти, что его заняло, и освободить без потерь

Команда KAI Lab · · 8 мин чтения

Когда в Linux закончилось место на диске, сначала найдите заполненную файловую систему командой df -h, а потом каталог, который её занял, — через du -xh -d1 или ncdu -x. Если df показывает 100%, а du насчитывает на гигабайты меньше, ищите удалённые, но всё ещё открытые файлы (lsof +L1); если место свободно, а ошибка No space left on device не уходит, проверьте inode командой df -i.

Ниже порядок, в котором это удобно проверять на живом сервере: сначала факты, потом действие. Главное правило — ничего не удалять, пока не понятно, кто и зачем пишет на диск. Лог, удалённый наугад, часто не освобождает ни байта, а каталог базы, удалённый наугад, превращает мелкий инцидент в большой.

Симптом → команда → что смотреть

СимптомКомандаЧто смотреть
No space left on device, сервис не может писатьdf -hкакая файловая система заполнена: столбцы Use% и Mounted on
Непонятно, что заняло местоdu -xh -d1 /var или ncdu -x /самый большой каталог, дальше спускаться в него
Нужны самые большие файлыfind / -xdev -type f -size +1Gлоги, дампы, забытые архивы
df и du расходятся на гигабайтыlsof +L1файлы с пометкой (deleted) и процесс, который их держит
Место есть, а ошибка естьdf -iIUse% 100% — закончились inode
Растёт /var/log/journaljournalctl --disk-usageсколько занимает журнал systemd
Растёт /var/lib/dockerdocker system dfобразы, остановленные контейнеры, тома, кэш сборки
Лог растёт без ограниченийlogrotate -d /etc/logrotate.d/appчто сделает ротация, без реальных изменений

Шаг 1. Какая файловая система заполнена

$ df -h
Filesystem      Size  Used Avail Use% Mounted on
tmpfs           392M  1.1M  391M   1% /run
/dev/vda1        49G   47G     0 100% /
tmpfs           2.0G     0  2.0G   0% /dev/shm
tmpfs           5.0M     0  5.0M   0% /run/lock
/dev/vda15      105M  6.1M   99M   6% /boot/efi
tmpfs           392M  4.0K  392M   1% /run/user/1000

Заполнен корень /. Строки tmpfs — файловые системы в памяти, их можно пропустить: df -h -x tmpfs уберёт их из вывода.

Обратите внимание: Size 49G, Used 47G, а Avail уже 0. Разница — резерв ext4. По умолчанию mke2fs оставляет 5% блоков для root, чтобы системные демоны продолжали работать, когда обычным процессам писать уже некуда. Поэтому приложение от своего пользователя падает с No space left on device, а процессы root ещё какое-то время пишут. Размер резерва показывает tune2fs:

$ sudo tune2fs -l /dev/vda1 | grep -i 'reserved block count'
Reserved block count:     655360

655 360 блоков по 4 КБ — это 2,5 ГБ. Уменьшать резерв через tune2fs -m посреди инцидента — плохая идея: место кончится снова, только уже без запаса для системы.

Шаг 2. Что заняло место: du, ncdu, find

du считает, сколько занимают каталоги. Флаг -x не даёт уходить в другие файловые системы — иначе в подсчёт попадут смонтированные диски и сетевые шары.

$ sudo du -xh -d1 / 2>/dev/null | sort -h | tail -5
1.3G    /opt
2.8G    /usr
4.9G    /home
22G     /var
31G     /
$ sudo du -xh -d1 /var 2>/dev/null | sort -h | tail -4
312M    /var/cache
2.4G    /var/lib
19G     /var/log
22G     /var

Спускайтесь по самому большому каталогу, пока не станет ясно, кто виноват. Интерактивно то же делает ncdu -x /: он один раз обходит дерево, а дальше по каталогам можно ходить стрелками и сортировать по размеру. Клавиша d в ncdu удаляет выбранный файл, поэтому на проде удобнее ncdu -r — режим только для чтения.

Самые большие файлы целиком находит find:

$ sudo find / -xdev -type f -size +1G -exec ls -lh {} + 2>/dev/null
-rw-r----- 1 app app 5.1G Sep 23 00:00 /var/log/app/app.log.1
-rw-r----- 1 app app 9.8G Sep 24 03:12 /var/log/app/app.log

Небольшое расхождение между du и df нормально: метаданные файловой системы и её журнал du не видит. Но здесь du насчитал 31 ГБ, а df — 47 ГБ. Шестнадцать гигабайт лежат где-то ещё.

Шаг 3. df и du не сходятся: удалённые, но открытые файлы

Файл в Linux существует, пока на него есть ссылки: имя в каталоге или открытый дескриптор в каком-нибудь процессе. rm убирает только имя. Если процесс продолжает писать в удалённый лог, место не освобождается, а du этот файл уже не видит — по имени его больше не найти.

Такие файлы показывает lsof +L1 — открытые файлы, у которых не осталось ни одного имени:

$ sudo lsof +L1
COMMAND  PID USER   FD   TYPE DEVICE    SIZE/OFF NLINK    NODE NAME
java    2143  app    1w   REG  252,1 16734789632     0 1835123 /var/log/app/app-2026-09-20.log (deleted)
java    2143  app    2w   REG  252,1 16734789632     0 1835123 /var/log/app/app-2026-09-20.log (deleted)

Классическая история: ночью кто-то удалил «старый» лог, чтобы освободить место, а Java-процесс писал в этот файл stdout и stderr и продолжил писать. 15,6 ГБ держат дескрипторы 1 и 2 процесса 2143.

Правильное решение — перезапустить сервис, чтобы он закрыл файл: sudo systemctl restart app. Если перезапуск прямо сейчас невозможен, файл можно обрезать до нуля через /proc:

$ sudo ls -l /proc/2143/fd/1
l-wx------ 1 app app 64 Sep 24 03:20 /proc/2143/fd/1 -> '/var/log/app/app-2026-09-20.log (deleted)'
$ sudo truncate -s 0 /proc/2143/fd/1
$ df -h /
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        49G   32G   16G  68% /

Вариант sudo : > /proc/2143/fd/1 не сработает: перенаправление выполняет ваша оболочка, а не sudo. Поэтому truncate. Перезапуск всё равно понадобится, чтобы сервис писал в нормальный файл, а не в удалённый.

Вывод на будущее: активный лог не удаляют, а обрезают — sudo truncate -s 0 /var/log/app/app.log. А лучше настраивают ротацию, о ней ниже.

Шаг 4. Место есть, а No space left on device: inode

На ext4 число inode задаётся при создании файловой системы. Каждый файл и каталог занимает один inode, и если приложение наплодило миллионы мелких файлов — сессий, кэша, писем в очереди, — inode закончатся раньше байтов.

$ df -h /
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        49G   28G   19G  60% /
$ df -i /
Filesystem      Inodes   IUsed IFree IUse% Mounted on
/dev/vda1      3276800 3276800     0  100% /

Где именно лежат файлы, покажет du --inodes:

$ sudo du --inodes -x -d1 /var/lib 2>/dev/null | sort -n | tail -3
14022   /var/lib/dpkg
2903114 /var/lib/app
2981455 /var/lib

Удалить миллионы файлов командой rm /var/lib/app/cache/* не получится: оболочка раскроет звёздочку в список, который не влезет в лимит аргументов, и вернёт Argument list too long. Надёжнее find с условием по возрасту файлов:

$ sudo find /var/lib/app/cache -type f -mmin +1440 -delete

И сразу выясните, почему приложение не чистит за собой. Без этого через неделю будет повтор.

Журнал systemd занимает гигабайты

$ journalctl --disk-usage
Archived and active journals take up 3.9G in the file system.
$ sudo journalctl --vacuum-size=500M
Deleted archived journal /var/log/journal/0f3c2a7d9b8e4c61a5d2e7f8c9b0a1d2/system@8d1f2e3a4b5c46d7a8b9c0d1e2f3a4b5-0000000000000001-0005f9a1b2c3d4e5.journal (128.0M).
# ... и ещё два десятка таких строк
Vacuuming done, freed 3.4G of archived journals from /var/log/journal/0f3c2a7d9b8e4c61a5d2e7f8c9b0a1d2.

--vacuum-size, --vacuum-time (например, --vacuum-time=14d) и --vacuum-files удаляют только архивные файлы журнала. Активные остаются; если нужно почистить и их, добавьте --rotate — journald сначала закроет текущие файлы, потом выполнит очистку.

Постоянный лимит задаётся в конфигурации journald. По умолчанию журнал может занять до 10% файловой системы, но не больше 4 ГБ. Для небольшого сервера это много:

$ sudo mkdir -p /etc/systemd/journald.conf.d
$ printf '[Journal]\nSystemMaxUse=1G\n' | sudo tee /etc/systemd/journald.conf.d/size.conf
[Journal]
SystemMaxUse=1G
$ sudo systemctl restart systemd-journald

Файлы журнала руками через rm не удаляют: для этого и есть vacuum.

logrotate: чтобы место не кончилось снова

logrotate переименовывает лог, сжимает старые копии и удаляет лишние. Пример для приложения, которое пишет в /var/log/app/:

$ cat /etc/logrotate.d/app
/var/log/app/*.log {
    daily
    rotate 14
    maxsize 1G
    compress
    delaycompress
    missingok
    notifempty
    copytruncate
}
$ sudo logrotate -d /etc/logrotate.d/app

Что здесь важно:

  • rotate 14 — хранить 14 старых копий, более старые удалять;
  • maxsize 1G — ротировать раньше срока, если файл вырос больше гигабайта;
  • copytruncate — для приложений, которые не умеют переоткрывать файл: logrotate копирует лог и обрезает оригинал. Документация честно предупреждает, что между копированием и обрезкой часть строк может потеряться. Если приложение умеет переоткрывать лог по сигналу или при reload, лучше create и команда в блоке postrotate;
  • logrotate -d — пробный прогон: покажет, что будет сделано, и ничего не изменит.

Ещё одна ловушка: logrotate обычно запускается раз в сутки — задачей cron или через logrotate.timer. maxsize проверяется только в момент запуска. Если лог растёт на 20 ГБ в час, суточный запуск не спасёт: нужен запуск чаще или меньше логов на уровне приложения.

Если это Docker или Kubernetes

На хостах с Docker место чаще всего уходит в /var/lib/docker: старые образы, остановленные контейнеры, тома, кэш сборки и json-логи контейнеров без ограничения размера. Этот случай разобран отдельно — No space left on device в Docker.

В Kubernetes заполненный диск ноды — это не только ошибки записи. По умолчанию kubelet начинает выселять поды, когда на файловой системе ноды свободно меньше 10% места или меньше 5% inode, и ставит ноде условие DiskPressure. Как это выглядит со стороны подов — в разборе Pod Evicted.

Как не доводить до 100%

Алерт «диск заполнен на 95%» ночью срабатывает слишком поздно, а на огромном разделе — слишком рано. Надёжнее прогноз: успеет ли диск заполниться за ближайшие часы при текущей скорости записи. В Prometheus с node_exporter это predict_linear:

# при текущем темпе корень заполнится меньше чем за 4 часа
predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 4 * 3600) < 0

Добавьте отдельный алерт на inode по метрике node_filesystem_files_free — он ловит случай из шага 4. И заведите короткий runbook: какие каталоги на этом сервере можно чистить, а какие не трогать никогда — данные баз, pg_wal у PostgreSQL, тома Docker. Если такой алерт придёт на вашем первом дежурстве, runbook сэкономит полчаса.

Полный диск ещё и маскируется под другие проблемы: сервис не стартует, потому что не может создать pid-файл или временный файл, — см. systemd-сервис не запускается.

В симуляторе DevOps KAI Lab с диска начинается второй месяц: тикет «Диск на проде заполнен на 97%», а на первом дежурстве приходит алерт о том, что диск заполнится через четыре часа. Консоль там отвечает реальными выводами команд, так что порядок из этой статьи можно отработать на сломанном сервере, а не на своём проде.

Частые вопросы

Почему после rm место на диске не освободилось? Файл ещё открыт процессом. rm удалил имя, но данные останутся на диске, пока процесс не закроет дескриптор. Найдите такой файл через sudo lsof +L1, затем перезапустите сервис или обрежьте файл через /proc/PID/fd/N.

Почему df и du показывают разное? du считает файлы, которые видит по именам, а df — занятые блоки файловой системы. Разницу в гигабайты чаще всего дают удалённые, но открытые файлы. Второй вариант — данные под точкой монтирования: если в /data что-то записали до того, как туда смонтировали диск, эти файлы скрыты, но место на корне занимают.

Что такое inode и почему они заканчиваются? Inode — служебная запись о файле: владелец, права, где лежат данные. На ext4 их число фиксируется при создании файловой системы. Миллионы мелких файлов исчерпывают inode при свободных гигабайтах; проверка — df -i.

Можно ли просто удалить /var/log/journal? Руками — не стоит. Используйте journalctl --vacuum-size= или --vacuum-time=, а постоянный лимит задайте через SystemMaxUse= в конфигурации journald.

Что чистить нельзя, даже если очень хочется? Каталоги данных баз, WAL PostgreSQL, тома Docker и всё, назначение чего вы не знаете. Удаление там не освобождает место «безопасно» — оно ломает данные. Сначала найдите процесс-виновника, потом решайте.

Источники

  • man7.org: df(1), du(1) — опции -x, -d, --inodes.
  • man7.org: lsof(8) — +L1 выбирает открытые файлы, у которых удалены все имена.
  • man7.org: unlink(2) — файл существует, пока его держит открытый дескриптор.
  • man7.org: mke2fs(8) — резерв 5% блоков для root по умолчанию.
  • ncdu. Manual — -x, -r и клавиши интерфейса.
  • man7.org: journalctl(1) и journald.conf(5) — vacuum, SystemMaxUse=, лимиты по умолчанию.
  • man7.org: logrotate(8) — copytruncate, maxsize, -d.
  • Kubernetes. Node-pressure Eviction — пороги выселения по умолчанию и DiskPressure.
  • Prometheus. Query functions: predict_linear.

Читать дальше