Кейс
df показывает 27 из 30 ГБ занято (92%), а файлы занимают только 17 ГБ
На АТС кончалось место: df показывал 27 ГБ занято из 30, а подсчёт всех файлов давал только 17 ГБ. Разбираем, почему возникает такое расхождение, как найти виновника через lsof и как невидимая опечатка в настройке ротации логов на четыре месяца лишила сервер и места, и журналов.
Обе полосы отмерены от одного объёма в 30 ГБ.
- Файлы на месте
- Удалённый файл, удерживаемый процессом
- Свободно
файлы — 17 ГБудалённый файл — 10 ГБсвободно — 2,5 ГБ
файлы — 17 ГБсвободно — 13 ГБ
Остаток до 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 показывает сразу. Если команда в конфиге «выглядит правильно», но не
работает, посмотрите на неё в байтах.
И общее наблюдение, которое к этой истории только приложение: самые дорогие отказы — не те, что падают с ошибкой, а те, что отчитываются об успехе. Ротация работала, диск заполнялся, а мониторинг показывал процент без причины, защита была включена. Каждый элемент по отдельности выглядел исправным.