Симптомы проблемы
Сервер приложений перестал создавать временные сокеты и писать PID-файлы. Команды падали с ошибкой:
touch /tmp/test.txt
touch: cannot touch '/tmp/test.txt': No space left on device
При этом df -h рапортовал о более чем 420 ГБ абсолютно свободного места на корневом разделе /dev/sda2.
Проверка таблицы inode
Запуск df -i моментально раскрыл тайну:
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sda2 32768000 32768000 0 100% /
Таблица индексных дескрипторов (inodes) заполнена ровно до последнего слота. Любая попытка создать даже нулевой файл блокируется ядром на уровне VFS.
Поиск виновника
Обычный ls /tmp намертво подвесил терминал — директория содержала миллионы записей, и чтение каталога через getdents64 аллоцировало сотни мегабайт в ядре.
Используем быстрый поиск на Perl:
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 с пустой директорией:
mkdir /empty_dir
rsync -a --delete /empty_dir/ /tmp/sessions/
Либо через быстрый однострочник на Си/Perl:
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.