К основному содержанию

Кейс

df показывает 27 из 30 ГБ занято (92%), а файлы занимают только 17 ГБ

На АТС кончалось место: df показывал 27 ГБ занято из 30, а подсчёт всех файлов давал только 17 ГБ. Разбираем, почему возникает такое расхождение, как найти виновника через lsof и как невидимая опечатка в настройке ротации логов на четыре месяца лишила сервер и места, и журналов.

10 ГБдержал файл, удалённый четыре месяца назад
Том 30 ГБ: что показывает df и что находит подсчёт файлов

Обе полосы отмерены от одного объёма в 30 ГБ.

Остаток до 30 ГБ приходится на резерв файловой системы, поэтому сегменты не складываются в полный объём тома.

Обычная телефонная станция на обслуживании: виртуальная машина, том на 30 ГБ, Asterisk, база звонков, записи разговоров. Занятость диска — 92 %, свободно 2,5 ГБ. Ещё немного, и станция перестанет писать и записи разговоров, и сам журнал звонков.

Первая гипотеза очевидна: записи разговоров. Их там почти 10 ГБ, они копятся ежедневно, и на такой машине это самый крупный потребитель. Считаем скорость роста: около 115 записей в день, в среднем 1,4 МБ — примерно 165 МБ в сутки. При 2,5 ГБ свободного это две недели до полной остановки.

Гипотеза оказалась неверной, и проверять её надо было до арифметики.

Ротация записей работала

В расписании нашлись две задачи, о которых стоило посмотреть раньше, чем считать прогноз:

0 *    * * *  root  find /var/spool/asterisk/monitor -type f -size -4  -delete
0 */6  * * *  root  find /var/spool/asterisk/monitor -type f -mtime +100 -delete

Первая ежечасно убирает обрезки меньше двух килобайт — пустышки от неотвеченных вызовов. Вторая каждые шесть часов удаляет всё старше ста дней. То есть хранение ограничено и объём записей давно вышел на стационар: самый старый файл был ровно столетней давности, старше сотни дней — ни одного лишнего.

Записи не росли неограниченно. Значит место занимало что-то другое.

df и du не сходятся

Простейшая проверка, которую стоит делать при любой нехватке места, — сравнить «занято» по файловой системе с суммой размеров всех файлов:

$ df -h /
/dev/vda1        30G   27G  2.5G  92% /

$ du -sxh /
17G     /

Двадцать семь против семнадцати. Десять гигабайт заняты, но ни одному файлу не принадлежат.

Такое расхождение почти всегда означает одно: файл удалён, но открыт работающим процессом. В Unix удаление — это удаление имени, а не данных. Пока хотя бы один процесс держит дескриптор, данные остаются на диске, и место не освобождается. Файла в каталоге уже нет — ни ls, ни du, ни find его не увидят, а место занято. Более того, процесс может продолжать в него писать, и «невидимка» будет расти.

Находится виновник одной командой — +L1 отбирает файлы с числом ссылок меньше одной:

$ lsof -nP +L1
COMMAND    PID  USER   FD   TYPE  SIZE/OFF NLINK   NAME
asterisk 28525  root    6w   REG  10856689100   0   /var/log/asterisk/messages-20260405 (deleted)

Десять целых одна десятая гигабайта. Файл с датой в имени — то есть уже переименованный ротацией — удалён, и Asterisk четыре месяца писал в него.

Почему ротация не сработала

Настройка ротации выглядела совершенно нормально:

/var/log/asterisk/messages /var/log/asterisk/*log {
    missingok
    rotate 5
    weekly
    postrotate
        /usr/sbin/asterisk -rx ‘logger reload’ > /dev/null 2> /dev/null
    endscript
}

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

Но команда не выполнялась. Присмотритесь к кавычкам:

$ grep 'logger reload' /etc/logrotate.d/asterisk | hexdump -C
00000010  73 6b 20 2d 72 78 20 e2  80 98 6c 6f 67 67 65 72  |sk -rx ...logger|
00000020  20 72 65 6c 6f 61 64 e2  80 99 20 3e 20 2f 64 65  | reload... > /de|

e2 80 98 и e2 80 99 — это U+2018 и U+2019, «умные» типографские кавычки, а не обычный апостроф 27. Такие подставляет текстовый редактор или веб-страница, откуда конфигурацию скопировали. Для оболочки они не кавычки, а обычные символы: команда получает аргумент ‘logger, чего не понимает, и завершается с ошибкой.

А ошибку никто не увидел, потому что в той же строке стоит 2> /dev/null. Четыре месяца ежедневного молчаливого отказа.

Что оказалось хуже нехватки места

Место — полбеды. Куда неприятнее второе следствие: раз приложение писало в удалённый файл, то все текущие журналы были пусты. Не «устарели», не «неполные» — нулевого размера, с апреля.

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

NOTICE chan_sip.c: Registration from '<sip:0000@...>' failed for '45.x.x.x' - Wrong password

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

Важно не преувеличить: невидимым перебор был для человека — открывать было нечего. Автоматическая блокировка, которая опирается на те же записи, за всё время закрыла около трёх тысяч адресов; сколько из потока после апреля она успела обработать, по её счётчикам уже не установить.

Починка

Место освобождается без перезапуска станции. Достаточно заставить приложение переоткрыть журналы — дескриптор закроется, и ядро освободит блоки:

$ asterisk -rx "logger rotate"
$ df -h /
/dev/vda1        30G   17G   13G  58% /

Десять гигабайт вернулись, журналы снова пишутся. Затем — первопричина: заменить кавычки на обычные и проверить, что postrotate теперь разбирается корректно.

Тот же конфиг с теми же кавычками нашёлся и на второй станции: настройки копируются между площадками вместе с опечатками. Там утечка ещё не успела развиться, но журналы тоже не писались.

Что забрать с собой

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

Не подавляйте stderr в postrotate. 2> /dev/null экономит несколько строк в почте администратора и обменивает их на четыре месяца необнаруженного отказа. Если шум мешает — заведите отдельный лог, но не выбрасывайте вывод целиком.

Проверяйте не факт ротации, а её результат. Файл переименовался, новый создался, logrotate отчитался успехом — и при этом приложение пишет в пустоту. Смотреть надо не на размер файла как таковой, а на то, растёт ли именно тот приёмник, который сервис действительно использует: он может писать и в системный журнал, и в другой файл, так что нулевой размер сам по себе ещё ни о чём не говорит. Такую проверку стоит поставить на мониторинг наравне с занятостью диска.

Опасайтесь копирования конфигураций из документации и переписки. Типографские кавычки, длинное тире вместо дефиса, неразрывный пробел — глазами не отличаются, hexdump показывает сразу. Если команда в конфиге «выглядит правильно», но не работает, посмотрите на неё в байтах.

И общее наблюдение, которое к этой истории только приложение: самые дорогие отказы — не те, что падают с ошибкой, а те, что отчитываются об успехе. Ротация работала, диск заполнялся, а мониторинг показывал процент без причины, защита была включена. Каждый элемент по отдельности выглядел исправным.

Все кейсы