Назад в блог

Файловые сессии PHP убили наш сервер в 23:00. Вот что мы сделали

Наш сервер лёг в 23:00. Прямо во время маркетинговой акции.

CPU — 40%. Память — в норме. Запросов — примерно на 30% больше обычного. Nginx отвечает. PHP-FPM молчит. Новые запросы просто висят.

Мы смотрели в мониторинг 20 минут и не понимали, что происходит. Потом открыли FPM status page.

Что показал PHP-FPM

pm.status_path в Bitrix включается одной строкой в nginx.conf. Когда открыли — все 128 воркеров в статусе Reading headers или Running, очередь из 340 ожидающих запросов.

Это значит: у FPM закончились воркеры. Каждый занят, новые запросы ждут.

Но почему? Нагрузка не запредельная. Мы такое видели раньше — сервер справлялся.

Запустил strace на нескольких застрявших процессах:

flock(14, LOCK_EX)  = 0
write(14, "...")
flock(14, LOCK_UN)

Файловый lock. На файлах сессий.

Как PHP блокирует сессии по умолчанию

Дефолтный session.save_handler = files — это значит, каждая пользовательская сессия хранится в отдельном файле в /tmp/sess_<session_id>.

При вызове session_start() PHP берёт на этот файл exclusive lockLOCK_EX. Пока запрос не закроет сессию (session_write_close() или конец скрипта), файл заблокирован.

Для одного запроса на одну вкладку — нормально. Но в реальности:

  • Пользователь открывает страницу каталога → это 1 основной запрос + 3-5 AJAX-запросов (виджеты, корзина, рекомендации).
  • Все эти запросы идут от одного пользователя → один session_id → один файл.
  • PHP-FPM запускает их параллельно → все пытаются взять LOCK_EX на один файл.
  • Первый взял. Остальные стоят в очереди.

Тогда было 340 одновременных пользователей. У каждого — современный фронт с параллельными AJAX-запросами. Итого 340 × 4 = ~1300 «параллельных» запросов, половина из которых стояла на file lock.

Именно это и убило FPM. Не нагрузка — очереди на блокировку.

Почему это не ловится в спокойный день

До 100-150 одновременных пользователей сервер справлялся. Запросы успевали отработать быстрее, чем очередь успевала расти.

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

Я проверил lsof | grep sess_ и увидел несколько тысяч открытых файловых дескрипторов на файлы сессий. Вот тогда стало понятно.

Переход на Redis-сессии

Мы уже использовали Redis для кеша Bitrix. Добавить Redis как session handler — 20 минут работы.

В php.ini (или в php-fpm.d/*.conf):

session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379?weight=1&timeout=2"

Для Bitrix нужно убедиться, что сессии Bitrix тоже пойдут через PHP-сессию. В большинстве установок это уже так — Bitrix использует стандартный PHP session_start(). Проверить можно в bitrix/.settings.php: если 'session' не переопределён, значит всё идёт через php.ini.

Если у вас Redis для кеша и Redis для сессий — разнесите их по разным базам (параметр database в save_path). Иначе случайный FLUSHDB выкинет всех пользователей из авторизации.

; для сессий — database 1, для кеша — database 0
session.save_path = "tcp://127.0.0.1:6379?weight=1&timeout=2&database=1"

Подводные камни, которые стоили нам часа

Redis не имеет file lock — там lock реализован через SET NX с TTL. Это убирает очереди, но добавляет нюанс: если запрос долго держит сессию открытой, lock может истечь раньше завершения. Параметр redis.session.lock_expire решает это — мы поставили 30 секунд, для наших самых тяжёлых запросов хватает.

TTL сессий нужно синхронизировать явно. Дефолт gc_maxlifetime = 1440 в Redis сам по себе не применяется:

session.gc_maxlifetime = 1440
redis.session.lock_expire = 30

Ещё одно: Redis persistence. Если Redis перезапустится без appendonly yes — все сессии умрут, пользователей выкинет. В dev это терпимо, в prod — нет.

Что изменилось в цифрах

До переключения:

  • p95 latency на страницах каталога: 3.8s
  • Пиковое количество занятых FPM-воркеров: 127/128
  • Количество ошибок 504 в час акции: ~1100

После:

  • p95: 0.7s
  • Занятые воркеры в том же трафике: 38-42/128
  • Ошибок 504: 0

Сервер справился с пиком на следующий день без изменений в железе.

Что теперь делаю на каждом проекте

session.save_handler — первая строчка в списке проверок при заходе на новый PHP-проект. Вместе с OPcache hit ratio и N+1 в iblock-запросах. Это три вещи, которые стабильно убивают production Bitrix без каких-либо очевидных симптомов в спокойный день.

Подробнее про OPcache — в отдельной статье про мисконфигурацию OPcache в production. Там схожий паттерн: дефолтные настройки, которые нормально работают на малом трафике и ломаются в пик.

Файловые сессии — не баг. Это разумный дефолт для сайта, где 20 человек одновременно. Когда вы перешли порог 200+ параллельных пользователей с современным SPA-фронтом — file lock становится тем же, чем OPcache без revalidate_freq: тихой миной.

Redis — не серебряная пуля. Но для этой конкретной задачи он убирает проблему полностью.