Дайджесты новостей
Инфографический чертёж-рентген файловой системы Linux: системный вызов unlink, открытый дескриптор в proc и сброс размера через truncate.

Диск забит на 100%, а du показывает свободное место: спасаем сервер

Картина маслом: три часа ночи, дежурный мониторинг взрывается авариями. СУБД экстренно падает в аварийный read-only, веб-сервер сыплет ошибками No space left on device (ENOSPC). Ты подключаешься по SSH, запускаешь df -h и видишь: корневой раздел забит ровно на 100%.

Казалось бы, решение очевидно: найти самый толстый лог-файл в /var/log/ и снести его через rm -f. Ты радостно удаляешь сорокагигабайтный файл, снова вводишь df -h... и замираешь в ступоре. Занятое пространство не уменьшилось ни на один килобайт. Ты запускаешь du -xsh /, и утилита рапортует, что видимые файлы занимают всего 50 ГБ из 100 доступных. Куда делись остальные 50 гигабайт?

Мы уже подробно разбирали архитектуру ядра и файловых систем в материале Большой справочник по Linux и Bash: от архитектуры ядра до отладки. Давай разберём физику этого инцидента на уровне системных вызовов VFS.

Физика файловых дескрипторов: почему rm не освобождает блоки

В операционных системах Linux файл физически состоит из двух частей:

  1. Запись в каталоге (имя файла и ссылка на номер inode).
  2. Сам индексный дескриптор inode и связанные с ним блоки данных на накопителе.

Команда rm — это всего лишь системный вызов unlink(). Она стирает запись об имени файла из директории и уменьшает счётчик жестких ссылок (nlink). Однако ядро освобождает фактические блоки на диске только тогда, когда выполнены два условия:

  • счётчик ссылок nlink равен нулю;
  • все процессы закрыли открытый файловый дескриптор этого файла.

Если твой сервис (Java, Nginx, PostgreSQL или ClickHouse) в момент удаления продолжал писать в лог, дескриптор остаётся открытым. Файл превращается в невидимого «призрака» (unlinked deleted file). Утилита du обходит дерево каталогов и просто не видит этот файл. А утилита df опрашивает суперблок файловой системы (statvfs), который честно учитывает все занятые блоки. Хуже того: процесс продолжает писать в открытый дескриптор, и диск заполняется дальше!

Поиск виновника и спасение сервера без перезагрузки

Самый грубый способ — перезагрузить сервер или перезапустить упавший сервис. Но если это мастер-узел распределённой базы данных или центральный балансировщик, перезагрузка вызовет недопустимый простой бизнеса.

Мы пойдём чисто инженерным путём через аудит открытых дескрипторов утилитой lsof:

# 1. Проверяем процент занятого дискового пространства
df -h /

# 2. Ищем процессы, удерживающие удаленные файлы (+L1 = Link count < 1)
sudo lsof -nP +L1 / | grep -i deleted

# Пример вывода:
# COMMAND   PID USER   FD   TYPE DEVICE        SIZE/OFF   NODE NAME
# java     1420 root    7w   REG  253,0  42949672960 134212 /var/log/app.log (deleted)

Из вывода мы видим: процесс с идентификатором PID 1420 удерживает файл через дескриптор номер 7.

Теперь самое интересное. Виртуальная файловая система ядра Linux экспортирует все дескрипторы работающих процессов в псевдокаталог /proc/<PID>/fd/. Мы можем сбросить размер удерживаемого файла в ноль прямо на лету, не останавливая приложение:

# Обнуляем размер файла через системный дескриптор в /proc
sudo truncate -s 0 /proc/1420/fd/7

# Альтернативный вариант через перенаправление пустого потока шелла:
# sudo sh -c ": > /proc/1420/fd/7"

# Проверяем мгновенное освобождение блоков:
df -h /
# Результат: занятое место сразу падает с 100% до безопасных 55%!

А если закончились не гигабайты, а inode?

Бывает обратная ситуация: df -h показывает 30 ГБ свободного места, но любая попытка создать файл возвращает ту же ошибку No space left on device. В этом случае проверь исчерпание индексных дескрипторов:

# Проверка пула индексных дескрипторов
df -i /

# Если IUse% = 100%, ищем каталоги с миллионами крошечных файлов (кеш, сессии):
sudo find / -xdev -type d -size +1M 2>/dev/null
Опасность race condition при использовании copytruncate

Чтобы логи не переполняли диск, настраивают системный демон logrotate. Многие по привычке ставят директиву copytruncate. Помни: она работает в два этапа — сначала копирует файл целиком, а затем обнуляет исходный. На высоконагруженных системах строки логов, записанные приложением между окончанием копирования и вызовом очистки, безвозвратно теряются! Для критичных сервисов безопаснее использовать метод переоткрытия дескрипторов по сигналу: блок postrotate с вызовом systemctl reload <service>.

Запомни эту связку lsof -nP +L1 и truncate /proc/<PID>/fd/<FD>: в критический момент она спасёт тебя от ночной паники и сохранит непрерывность работы продакшена.