- PVSM.RU - https://www.pvsm.ru -
Сервер лагает. Смотришь на диск - df -h говорит 95% занято. Запускаешь du -sh /* - в сумме набирается 20%. Куда делись остальные 75%? Файлы не найти, место не освободить, сервис падает.
Это не баг и не магия. Это фундаментальная особенность того как Linux работает с файлами. Разберём почему так происходит и как это чинить за две команды.
df и du смотрят на файловую систему с разных сторон.
df читает метаданные файловой системы напрямую - сколько блоков выделено, сколько свободно. Это данные суперблока, они обновляются мгновенно при любом изменении.
du обходит дерево каталогов и суммирует размеры файлов которые видит. Ключевое слово - видит. du считает только то что доступно через файловую систему прямо сейчас.
Вот тут и начинается расхождение.
В Linux файл - это не просто запись в каталоге. У каждого файла есть inode - структура в ядре которая хранит метаданные: права, владелец, размер, и главное - список блоков на диске где лежат данные.
Когда процесс открывает файл, ядро создаёт файловый дескриптор - ссылку на inode. Пока хотя бы один дескриптор на inode открыт, ядро не трогает блоки на диске.
Когда делаешь rm файл - удаляется только запись в каталоге (hardlink). Inode и блоки остаются нетронутыми пока счётчик ссылок не упадёт до нуля. Если процесс держит файл открытым - счётчик не упадёт.
Схема выглядит так:
каталог inode блоки на диске
/var/log/ --> app.log --> [блок1][блок2][блок3]
^
| открытый дескриптор процесса
nginx (pid 1234, fd 7)
после rm:
каталог inode блоки на диске
/var/log/ --> (удалено) --> [блок1][блок2][блок3]
^ ^
| дескриптор | всё ещё занято!
nginx (pid 1234, fd 7)
du обходит /var/log/ - файла там больше нет, не считает. df смотрит на блоки - они заняты, считает.
Отсюда расхождение.
Самый частый случай - ротация логов через rm.
Представь: logrotate настроен удалять старый лог и создавать новый. Он делает rm /var/log/app.log - запись из каталога пропала. Но nginx или java открыл этот файл на запись при старте и держит дескриптор. Процесс продолжает писать в удалённый файл, блоки заполняются, df видит рост, du ничего не находит.
Это может продолжаться часами и гигабайтами пока не кончится место или не перезапустят процесс.
$ lsof +L1
lsof - утилита которая показывает все открытые файлы в системе. Флаг +L1 означает "показать файлы у которых счётчик ссылок меньше 1" - то есть файл удалён из файловой системы, но процесс его ещё держит.
Вывод будет примерно такой:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
nginx 1234 www 7w REG 8,1 2147483648 0 12345 /var/log/app.log (deleted)
java 5678 app 3w REG 8,1 536870912 0 67890 /var/log/service.log (deleted)
Разбираем колонки:
COMMAND - имя процесса
PID - идентификатор процесса
FD - номер дескриптора и режим доступа. 7w - номер 7, режим w (write). Буква означает что делает процесс: r читает, w пишет, u читает и пишет
SIZE/OFF - размер файла. Вот они, потерянные гигабайты
NLINK - счётчик ссылок. 0 означает что файл удалён из всех каталогов
NAME - путь с пометкой (deleted)
Теперь ясно: nginx пишет в удалённый лог размером 2 ГБ.
Рестарт nginx освободит дескриптор и блоки. Но рестарт не всегда возможен - активные соединения, продакшен, согласования.
Есть способ освободить место не трогая процесс:
$ > /proc/1234/fd/7
Разбираем по частям:
> - оператор перенаправления в bash. Открывает файл на запись и сразу обнуляет его содержимое. Именно обнуляет - не удаляет, а делает размер ноль
/proc/ - это procfs, псевдофайловая система Linux. Не хранит данные на диске - представляет информацию о процессах и ядре в виде файлов и каталогов прямо в памяти
/proc/1234/ - каталог процесса с pid 1234
fd/ - подкаталог со всеми открытыми дескрипторами процесса в виде симлинков на реальные файлы
7 - номер дескриптора из вывода lsof
Команда идёт к файлу напрямую через дескриптор - минуя файловую систему где файл уже "удалён". Ядро обнуляет блоки, df сразу покажет освободившееся место. Процесс продолжает работать и писать в тот же дескриптор.
Проверить результат:
$ lsof +L1 | grep nginx
# пусто или SIZE/OFF = 0
Корень проблемы - logrotate использует rm + создание нового файла. Процесс продолжает писать в старый дескриптор.
Правильное решение - copytruncate в конфиге logrotate:
/var/log/app.log {
daily
rotate 7
compress
copytruncate
}
copytruncate работает иначе: копирует содержимое лога в архив, затем обнуляет оригинальный файл через тот же механизм что мы использовали вручную. Процесс продолжает писать в тот же файл, дескриптор не меняется, блоки освобождаются.
Минус: между копированием и обнулением есть небольшое окно где несколько строк лога могут потеряться. Для большинства случаев это приемлемо.
Если потеря строк недопустима - процесс должен поддерживать SIGHUP для переоткрытия лога. nginx умеет: nginx -s reopen. Тогда logrotate делает mv старого файла, создаёт новый, и посылает сигнал процессу.
Чтобы не ловить это в 3 ночи - добавь проверку в мониторинг:
# удалённые но открытые файлы больше 100 МБ
lsof +L1 -F s | awk '/^s/ && substr($0,2)+0 > 104857600 {count++} END {print count+0}'
-F s включает вывод размера в машиночитаемом формате - каждая строка выглядит как s2147483648. /^s/ фильтрует строки с размером. substr($0,2) убирает префикс s и оставляет число. +0 > 104857600 - больше 100 МБ (104857600 байт).
Или через Prometheus + node_exporter - метрика node_filefd_allocated показывает количество открытых дескрипторов. Резкий рост при падении свободного места - признак именно этой проблемы.
df и du расходятся когда файл удалён но процесс держит открытый дескриптор
rm не освобождает блоки пока счётчик ссылок не ноль
найти виновника: lsof +L1
починить без рестарта: > /proc/<pid>/fd/<fd>
не попасть снова: copytruncate в logrotate или SIGHUP после ротации
Механика одна и та же на любом Linux - от Raspberry Pi до серверов с терабайтными дисками.
Больше про Linux, DevOps и SRE - в Telegram-канале @b4shninja [1]
Автор: b4shninja
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/linux/448277
Ссылки в тексте:
[1] @b4shninja: https://t.me/b4shninja
[2] Источник: https://habr.com/ru/articles/1017290/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1017290
Нажмите здесь для печати.