Дата последнего изменения: 23.09.2021
В связи с бурным ростом и усложнением Front-End аяксами, табами в браузере и тому подобным, все чаще проявляется проблема блокировки сессий во время эксплуатации сайтов на PHP. PHP по умолчанию создает для сессии файл и процесс эксклюзивно его блокирует. Остальные процессы, пытающиеся открыть сессию - выстраиваются в очередь. Не всегда логика приложения, особенно если она сложная, позволяет эффективно ограничить время блокировки конкурирующих за сессию процессов. Ситуация усугубляется еще тем, что 3-5 подобных клиентов способны быстро забить зависшими и простаивающими в ожидании процессами PHP-worker'ы и сайт "повисает".
К сожалению, разработчики/сисадмины не всегда могут сразу понять, что дело в блокировке сессии — и ищут проблемы в других частях проекта, теряя время.
Рассмотрим, что происходит внутри операционной системы, если одновременно попытаться открыть в браузере (можно в разных вкладках) один засыпающий файл и несколько просто стартующих сессию скриптов:
<?php session_start(); sleep(30);// только для одного скрипта ?>
Страницы будут дожидаться освобождения сессии (30 секунд) и займет это много времени, при этом будут забиты слоты веб-сервера. Примерно то же самое случается, когда AJAX запускает в сессии веб-клиента тяжелую задачу и остальные AJAX и другие элементы интерфейса зависают в ожидании (либо когда открывается несколько вкладок под одной авторизацией).
Процессы веб-сервера, в данном случае httpd, но то же самое происходит и с PHP-FPM — пытаются эксклюзивно заблокировать файл сессии, что видим с помощью lsof:
lsof -n | awk '/sess_/' httpd 7079 nobody 52uW REG 8,1 2216 809832 /tmp/sess_f629a13b4b0920a21042c86d17f4a6a6 httpd 10406 nobody 52u REG 8,1 2216 809832 /tmp/sess_f629a13b4b0920a21042c86d17f4a6a6 httpd 10477 nobody 52u REG 8,1 2216 809832 /tmp/sess_f629a13b4b0920a21042c86d17f4a6a6 httpd 10552 nobody 52u REG 8,1 2216 809832 /tmp/sess_f629a13b4b0920a21042c86d17f4a6a6 httpd 11550 nobody 52u REG 8,1 2216 809832 /tmp/sess_f629a13b4b0920a21042c86d17f4a6a6 httpd 11576 nobody 52u REG 8,1 2216 809832 /tmp/sess_f629a13b4b0920a21042c86d17f4a6a6
Обратите внимание на 4 колонку. Число - это номер дескриптора файла в процессе, а дальше - тип блокировки. "uW" - веб-сервер заблокировал файл эксклюзивно для записи. Остальные - в ожидании. Как только процесс 7079 закончит свою работу, блокировку "uW" возьмет другой процесс. В это время, понятно, выстраивается очередь и веб-интерфейс заметно тормозит. Если несколько процессов заблокируют сессию на единицы секунды - интерфейс вообще "замрёт".
Посмотрим теперь с другой стороны, чем занимаются процессы:
ps -e -o pid,comm,wchan=WIDE-WCHAN-COLUMN | grep httpd 7079 httpd - 10406 httpd flock_lock_file_wait 10477 httpd flock_lock_file_wait 10552 httpd flock_lock_file_wait 11550 httpd flock_lock_file_wait 11576 httpd flock_lock_file_wait
Во второй колонке видим, что все, кроме одного, заняты в функции flock_lock_file_wait.
strace -p 10406 Process 10406 attached - interrupt to quit flock(52, LOCK_EX)
Они заняты в системном вызове c запросом эксклюзивной блокировки.
LOCK_EX Place an exclusive lock. Only one process may hold an exclusive lock for a given file at a given time.
Чтобы постоянно отслеживать на веб-серверах появление такого "паровозика", забивающего PHP-воркеры, можно использовать скрипт, написанный на AWK:
/sess_/ { load_sessions[$9]++; if (load_sessions[$9]>max_sess_link_count){ max_sess_link_count = load_sessions[$9]; max_sess_link_name = $9; }; if ($4 ~ /.*uW$/ ){ locked_id[$9]=$2 }; } END { print max_sess_link_count, max_sess_link_name,locked_id[max_sess_link_name]; if (locked_id[max_sess_link_name] && max_sess_link_count>3) { # r=system("kill "locked_id[max_sess_link_name]); # if (!r) print "Locking process "locked_id[max_sess_link_name]" killed" system("ls -al "max_sess_link_name); } }
Запускается так:
lsof -n | awk -f sess_view.awk 5 /tmp/sess_f629a13b4b0920a21042c86d17f4a6a6 24830
Скрипт отображает длину "паровозика" и процесс — создающий затор.
Понятно, что на реальном сайте не должно быть подобных проблем - нужно сделать все (переделать логику работы с сессией, написать кастомные хандлеры PHP, принять другие меры), чтобы у клиента по возможности ничего не тормозило, а вы как системный администратор - спали крепко и долго.
Если по каким-либо причинам переделка логики и прочие меры предпринять сложно (невозможно), то можно раскомментировать kill
и уничтожать процессы веб-сервера, создающие коллапс. Но правильнее с собранной подобным образом через cron в файлик статистикой обратиться к разработчикам и договориться о рефакторинге.