Симптомы проблемы

Сервер приложений перестал создавать временные сокеты и писать PID-файлы. Команды падали с ошибкой:

BASH
touch /tmp/test.txt
touch: cannot touch '/tmp/test.txt': No space left on device

При этом df -h рапортовал о более чем 420 ГБ абсолютно свободного места на корневом разделе /dev/sda2.

Проверка таблицы inode

Запуск df -i моментально раскрыл тайну:

TEXT
Filesystem      Inodes     IUsed    IFree  IUse% Mounted on
/dev/sda2     32768000  32768000        0   100% /

Таблица индексных дескрипторов (inodes) заполнена ровно до последнего слота. Любая попытка создать даже нулевой файл блокируется ядром на уровне VFS.

Поиск виновника

Обычный ls /tmp намертво подвесил терминал — директория содержала миллионы записей, и чтение каталога через getdents64 аллоцировало сотни мегабайт в ядре.

Используем быстрый поиск на Perl:

BASH
perl -e 'opendir(D, "/tmp") or die; while($f=readdir(D)){ $n++ } print "$n\n";'

Счетчик показал: 10 412 890 файлов с префиксом sess_.

Сломался встроенный крон очистки сессий (/etc/cron.d/php), который не запускался из-за синтаксической ошибки в одном из соседних файлов крона после недавнего обновления.

Быстрое удаление без fork-бомбы

Команда rm -rf /tmp/sess_* выбросила бы ошибку Argument list too long (лимит execve ARG_MAX).

Использование find /tmp -type f -delete на таком объеме создавало чудовищный sync overhead.

Оптимальное решение — потоковое удаление через Perl или rsync с пустой директорией:

BASH
mkdir /empty_dir
rsync -a --delete /empty_dir/ /tmp/sessions/

Либо через быстрый однострочник на Си/Perl:

BASH
perl -e 'opendir(D, "/tmp") or die; while($f=readdir(D)){ unlink "/tmp/$f" if $f=~/^sess_/; }'

После очистки освободилось более 10 миллионов inode за 4 минуты.

Предотвращение рецидивов

  • Перенос сессий в Redis с автоматическим TTL.
  • Настройка tmpfs для /tmp с фиксированным лимитом nr_inodes=500k.
  • Алерт в Prometheus: предупреждение при достижении node_filesystem_files_free / node_filesystem_files < 0.2.