Сервер лагает. Смотришь на диск - df -h говорит 95% занято. Запускаешь du -sh /* - в сумме набирается 20%. Куда делись остальные 75%? Файлы не найти, место не освободить, сервис падает.

Это не баг и не магия. Это фундаментальная особенность того как Linux работает с файлами. Разберём почему так происходит и как это чинить за две команды.

Почему df и du показывают разное

df и du смотрят на файловую систему с разных сторон.

df читает метаданные файловой системы напрямую - сколько блоков выделено, сколько свободно. Это данные суперблока, они обновляются мгновенно при любом изменении.

du обходит дерево каталогов и суммирует размеры блоков которые видит. Ключевое слово - видит. du считает только то что доступно через файловую систему прямо сейчас.

Вот тут и начинается расхождение.

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

В 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

Комментарии (24)


  1. RedEyedAnonymous
    31.03.2026 07:18

    Он делает rm /var/log/app.log - запись из каталога пропала. Но nginx или java открыл этот файл на запись при старте и держит дескриптор.

    Звучит как откровенное вредительство.


    1. FSA
      31.03.2026 07:18

      Это не вредительство, это обычное поведение файловой системы linux. Оно в корне отличается от того, что происходит в Windows. Пока что-то держит файл, файл после rm физически не удаляется и продолжает работать. При этом для тех, кто файл не держал, файл фактически считается удалённым. В Windows же при попытке удалить файл, пока его кто-то держит, вываливается ошибка.

      Всех этих мучений, что описаны в статье, можно было бы избежать, если бы сервис не писал файлы логов самостоятельно, а доверил это какому-либо сервису по ведению логов. Там все особенности файловой системы linux уже учтены.


      1. NickDoom
        31.03.2026 07:18

        …со стороны «nginx или java» таки вредительство, потому что надо или понимать особенности, или «доверить это какому-либо сервису по ведению логов».

        А то перекомпилировали, завелось — и радуются. А эта особенность даже домохозяйкам-на-убунте уже совершенно понятна и во многом кажется логичнее (особенно если они начинали с убунты и не выработали синдром утёнка к мастдаю).


        1. FSA
          31.03.2026 07:18

          Могу сказать только в защиту nginx. Он по умолчанию пишет сообщения самостоятельно. Но это легко меняется в настройках и он отлично пишет в journald. Особенно удобно, когда логи nginx можно просматривать совместно с логами php-fpm через journalctl. Сразу видно какой запрос пришёл, какую отладочную информацию выдал php-fpm и какие проблемы возникли при обработке запроса PHP. В java, наверно, тоже можно сделать что-то подобное.


          1. NickDoom
            31.03.2026 07:18

            На фоне питона они вообще все ангелы… вот уж кто считает, что не он для системы, а система для него. А равно и юзеры, и разрабы…

            Особенно юзерам «весело». Каждый первый проект с гитхаба требует или полностью разобраться в использованном стеке просто для того, чтобы это всё завелось под конкретной версией, или взять отдельный ноут и циклически форматировать диск, ставя с нуля систему и под ней перебирая версии питона — авось заведётся. Где он так откладывает личинки, что даже sudo apt purge не помогает — хз.

            Ну да, люди же писали, трудились, а ты хочешь, чтобы без труда всё у тебя заработало, ага. Научись из речного песка свой процессор делать, тогда удостоишься /s


      1. Anselm_nn
        31.03.2026 07:18

        Я бы скорее вредительством назвал поведение в windows, которое не дает удалить файл, даже от админа, если кто-то его держит. В древние времена была тулза Unlocker, которая могла сказать "УДАЛЯЙ" и удалялось. Давно не сижу на windows и было как-то не очень важно, вот сейчас решил спросить у gemini, оказывается Unlocker и аналоги еще живы (правда сначала он впаривал какие-то инструменты посмотреть кем открыто и его закрывать)

        Скрытый текст

        TL;DR: Вы абсолютно правы. Официальные современные утилиты слишком «вежливые» — они заставляют вас разбираться, кто держит файл, и вручную убивать процессы. Если вам нужен подход старого Unlocker (нажал правой кнопкой $\rightarrow$ «Удалить любой ценой» $\rightarrow$ файла нет), вам нужны сторонние утилиты, использующие драйверы уровня ядра для принудительного отрыва дескрипторов. Лучшие из них сейчас — IObit Unlocker, DeadLock и сам оригинальный Unlocker 1.9.2.

        Ниже приведен подробный разбор инструментов, которые работают по принципу «вижу цель — не вижу препятствий», и объяснение того, как именно они ломают защиту Windows.

        1. Программы для бескомпромиссного удаления (Аналоги старого Unlocker)

        Эти инструменты не предлагают вам «подумать» или «завершить задачу». Они делают грязную работу сами: либо жестко убивают процесс-владелец, либо вырывают у него дескриптор файла на уровне ядра.

        • IObit Unlocker

          • Как работает: Добавляет пункт в контекстное меню. При выборе открывается окно, где есть кнопка-стрелка рядом с «Unlock». Выбираете «Unlock & Delete» (Разблокировать и удалить).

          • Результат: Программа автоматически сбрасывает дескрипторы и удаляет файл. Никаких вопросов.

          • Нюанс: Это коммерческий продукт, но базовая функция разблокировки бесплатна. Как и со всем софтом от IObit, при установке нужно внимательно снимать галочки, чтобы не поставить лишний рекламный мусор.

        • DeadLock

          • Как работает: Бесплатная утилита, созданная именно для агрессивного снятия блокировок. Также встраивается в Проводник.

          • Результат: Позволяет в пару кликов сменить владельца файла (Take Ownership) и принудительно удалить его (Unlock and Delete), даже если система сопротивляется.

        • LockHunter (Режим быстрого удаления)

          • Хотя я упоминал его ранее, стоит уточнить: в нём не обязательно смотреть на список процессов. Там есть большая кнопка «Delete It!».

          • Результат: Нажатие этой кнопки автоматически закроет хэндлы или убьет процессы, мешающие удалению, и отправит файл в корзину (или удалит навсегда, если зажать Shift).

        • Оригинальный Unlocker (от Cedrick Collomb)

          • Реальность: Тот самый легендарный Unlocker (версия 1.9.2) всё ещё работает даже на Windows 10 и 11.

          • Нюанс: Проект заброшен много лет назад. Его официальный сайт мертв. Скачивать приходится со сторонних зеркал (например, MajorGeeks).

          • Предупреждение: Последние версии оригинального инсталлятора содержали рекламное ПО (Delta Toolbar). Браузеры и Защитник Windows почти гарантированно пометят его как PUA/PUP (потенциально нежелательное ПО). Если решите ставить — ищите портативную версию (Portable) или внимательно снимайте галочки при установке.

        2. Deep Dive: Как работает "Магия" агрессивного удаления и почему её нет в Windows

        Ваше возмущение («раз я удаляю файл, значит работа с ним полностью неактуальна») логично с точки зрения пользователя. Но архитектура Windows (NTFS) выстроена иначе.

        Чтобы программа могла просто «взять и удалить» заблокированный файл, она должна применить один из двух методов глубокого системного вмешательства:

        Метод А: Принудительное закрытие дескриптора (Forced Handle Closure)

        Когда программа (например, Word или фоновый системный процесс) открывает файл, ядро Windows выдает ей «дескриптор» (handle) — уникальный номер. В таблице объектов ядра ставится отметка, что файл заблокирован.

        Утилиты вроде Unlocker устанавливают свой собственный драйвер режима ядра (ring-0). Этот драйвер:

        1. Находит в оперативной памяти таблицу дескрипторов чужого процесса.

        2. Вызывает недокументированные системные функции (например, NtClose) от имени системы.

        3. Жестко удаляет запись о том, что файл занят.

        Риски: Программа-владелец не знает, что у неё отобрали файл. В следующую миллисекунду она пытается записать данные по старому адресу (дескриптору), который теперь указывает в никуда или на другой системный объект. Это вызывает исключение (Exception). Если файл держал системный драйвер или антивирус, это мгновенно приводит к синему экрану (BSOD) IRQL_NOT_LESS_OR_EQUAL.

        Метод Б: Отложенное удаление при загрузке (Boot-time Deletion)

        Если дескриптор оторвать нельзя (например, файл занят самим ядром ОС), агрессивные утилиты используют API-функцию MoveFileEx с флагом MOVEFILE_DELAY_UNTIL_REBOOT.

        Она записывает путь к файлу в специальный ключ реестра PendingFileRenameOperations. При следующей перезагрузке, еще до того, как загрузятся сервисы и графический интерфейс (до появления экрана блокировки), встроенный менеджер сеансов Windows (smss.exe) читает этот ключ и физически удаляет файл с диска.

        Итог: Microsoft никогда не добавит кнопку «Удалить без вопросов» в саму Windows, потому что Метод А нарушает фундаментальную стабильность ОС и гарантии целостности данных, которые дает файловая система NTFS. Однако, как технически грамотный пользователь, вы можете использовать инструменты с драйверами уровня ядра (IObit Unlocker или старый Unlocker), принимая риск того, что программа, державшая файл, в этот момент крашнется.


        1. ReadOnlySadUser
          31.03.2026 07:18

          Microsoft нынче разрабатывает PowerToys. Это эдакий швейцарский нож из ништяков, без которых мне уже и винда - не винда. Там есть аналог unlocker


          1. Anselm_nn
            31.03.2026 07:18

            Не аналог, а жалкая породия, именно это сначала гемини и предложил. Там показывается, какие процессы обращаются к файлу, типо или и сам их ищи закрывай. Это и близко не удалить принудительно


  1. FSA
    31.03.2026 07:18

    А я как-то привык писать логи в journald. Он, и логи в сжатом виде хранит (с поиском по ним), и о ротации заботится, и чтобы диск не забивался логами (ограничение на максимальный размер). А ещё du себя очень странно ведёт на дисках с btrfs. У самой утилиты обслуживания btrfs есть свой btrfs filesystem du.


    1. b4shninja Автор
      31.03.2026 07:18

      journald - да, хороший вариант, приложение может просто писать в поток и не знать вообще ни про какой файл = не держать fd, вся ротация и прочее будет разруливаться на уровне journald.
      btrfs - да, есть такой момент у btrfs, поэтому лучше отдельной тулзой для btrfs - вы правы.


    1. andreymal
      31.03.2026 07:18

      логи в сжатом виде хранит

      Как заставить их сжиматься? У меня они занимают в 3 раза больше места чем текстовые (включенные по умолчанию Compress=yes и compact mode видимо бесполезны)


      1. FSA
        31.03.2026 07:18

        Скорее всего где-то вы ошиблись. Внимательно посмотрите на то, что там пишется. Может быть какой-то сервис сильно много чего-то пишет. В любом случае, если у вас journald, то там есть ограничение на максимальный объём файлов журнала. В случае переполнения, старые логи просто начнут удаляться и дополнительного места на диске уже не потребуется. Я бы просто запустил journalctl -f и посмотрел чего у меня там летит такого, что постоянно забивает логи. Возможно у какого-то сервиса не выключена отладка или что-то сбоит.


        1. andreymal
          31.03.2026 07:18

          Это не имеет никакого значения, если тот же самый много пишущий сервис, пишущий в обычный текстовый файл вместо journald, станет занимать в 3 раза меньше места. Вышеупомянутый перенос логов nginx из текстовых файлов в journald для меня выглядит самоубийством (и это я ещё даже не вспоминал про то, что logrotate сожмёт текстовые логи после ротации)


          1. FSA
            31.03.2026 07:18

            Вышеупомянутый перенос логов nginx из текстовых файлов в journald для меня выглядит самоубийством

            Рекомендую всё-таки ознакомиться с возможностями journalctl. Там есть и свой grep, и фильтр по датам, и поиск по метаданным, и аналог tail -f.... Лично мне больше понравилось. Плюс не нужно быть root, чтобы логи смотреть. Достаточно дать пользователю необходимые права. А также вместо путей /var/log… можно пользоваться просто тегами или именами сервисов (опять таки, надо смотреть что удобнее в каждом конкретном случае). Мне тоже, по началу, казалось сложно, но сейчас наоборот - ведение логов в текстовых файлах кажется дичью.


            1. andreymal
              31.03.2026 07:18

              Возможности это конечно прикольно, но вот лично я не готов платить за них трёхкратным увеличением занятого места

              аналог tail -f...

              С лагом в одну секунду, что при реалтаймовой отладке чего-нибудь раздражает

              Плюс не нужно быть root, чтобы логи смотреть.

              Достаточно быть в группе adm


              1. FSA
                31.03.2026 07:18

                С лагом в одну секунду, что при реалтаймовой отладке чего-нибудь раздражает

                Ради интереса открыл journalctl -f на своём компьютере и попробовал обратиться к веб-серверу nginx локальному. За одно туда много пишет отладочной информации php-fpm, всё-таки у меня сервер стоит для разработки. Время от нажатия кнопки обновить в браузере и вывод логов занимает доли секунды. Точнее, чтобы узнать сколько, нужно что-то аппаратное, потому что глазом видно, что это происходит почти мгновенно.

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


          1. andreymal
            31.03.2026 07:18

            А впрочем чего я теоретизирую, если могу провести эксперимент в виртуалке: настроил nginx access_log одновременно на файл и на syslog (чтобы ушло в journald), налил 100 тысяч запросов — /var/log/journal занял 96МБ (логи заранее почистил, чтобы в них был только nginx), текстовый access.log занял 27МБ (и может быть сжат gzip'ом до 4МБ)


            1. b4shninja Автор
              31.03.2026 07:18

              верное замечание, access.log действительно лучше не держать в journald, как раз таки из за оверхеда, сам journald добавляет еще кучу полей к записи.


  1. andy_p
    31.03.2026 07:18

    du обходит дерево каталогов и суммирует размеры файлов

    du суммирует размер не файлов, а занятых блоков. Поэтому если у вас много мелких файлов, то du насчитает намного больше, чем сумма их длин.


    1. vesper-bot
      31.03.2026 07:18

      но всё равно не больше чем df, так же?


      1. b4shninja Автор
        31.03.2026 07:18

        Верно.
        df - сколько блоков занято на устройстве с точки зрения файловой системы, включая всё что du никогда не увидит (метаданные ФС, зарезервированные блоки, удалённые но открытые файлы)
        du - сколько блоков числится за файлами которые он обошёл


    1. b4shninja Автор
      31.03.2026 07:18

      Абсолютно верное и важное уточнение, спасибо.


  1. astenix
    31.03.2026 07:18

    Когда процесс открывает файл, ядро создаёт… ссылку на inode

    Открывает в смысле «создает» новый файл?


    1. b4shninja Автор
      31.03.2026 07:18

      Когда процесс открывает файл ядро делает три вещи:

      1. Находит inode - для существующего файла идёт по пути через directory entries. Для нового (O_CREAT) - сначала создаёт inode, потом directory entry на него.

      2. Создаёт struct file - это объект в памяти ядра: указатель на inode, текущая позиция в файле (offset), флаги (O_RDONLY и т.д.). Это и есть "ссылка на inode".

      3. Кладёт fd в таблицу процесса - файловый дескриптор (число 3, 4, 5...) это просто индекс в таблице который указывает на struct file.