Всем привет!
В последнее время, в связи с бурным ростом и усложнением фронт-эндов, аяксами и т.п. — все чаще проявляется проблема блокировки сессий во время эксплуатации сайтов на PHP. PHP по умолчанию создает для сессии файл и процесс эксклюзивно его блокирует. Остальные процессы, пытающиеся открыть сессию (аяксы, табы в браузере) — выстраиваются в очередь. Не всегда логика приложения, особенно если она сложная, позволяет эффективно ограничить время блокировки конкурирующих за сессию процессов.
Ситуация усугубляется еще тем, что 3-5 подобных клиентов способны быстро забить зависшими и простаивающими в ожидании процессами PHP-воркеры и сайту становится плохо, если не сказать очень.
К сожалению, разработчики/сисадмины не всегда могут сразу понять, что дело в блокировке сессии — и ищут проблемы в других частях проекта, теряя время.
В статье расскажу какие инструменты позволяют быстро диагностировать проблему, приведу работающий код и дам несколько боевых рекомендаций по выживанию :-)
Я сознательно не усложняю статью и не рассказываю о теории и практике написания кастомных обработчиков сессии PHP — это отдельная интересная тема. Сосредоточимся на конкретной задаче и попытаемся ее решить.
Диагностика
Рассмотрим, что происходит внутри операционной системы, если одновременно попытаться открыть в браузере (можно в разных вкладках) один засыпающий файл и несколько просто стартующих сессию скриптов:
<?php
session_start();
sleep(30);// только для одного скрипта
?>
Страницы будут дожидаться освобождения сессии (30 секунд) и займет это ой ой ой времени, при этом будут забиты слоты веб-сервера. Примерно то же самое случается, когда аякс запускает в сессии веб-клиента тяжелую задачу и остальные аяксы и другие элементы интерфейса зависают в ожидании (либо когда открывается несколько вкладок под одной авторизацией).
Процессы веб-сервера, в данном случае 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» и отстреливать процессы веб-сервера, создающие коллапс и наслаждаться реакцией клиентов и менеджеров технической поддержки :-) Но правильнее конечно, купить 2-3 баночки пива и сходить в гости к разработчикам — с собранной подобным образом через cron в файлик статистикой и договориться о рефакторинге :-)
Всем удачи и успехов!
P.S.
По просьбе преподавателей русского языка и программистов с филологическим образованием заменил слово «локировка» на «блокировка».
Поддерживая большие проекты нашей компании, нам приходится постоянно создавать инструменты и методики для быстрого анализа проблем с производительностью и их решения. GNU/Linux содержат большой набор полезных инструментов, но, к сожалению, далеко не все ими умеют пользоваться. Надеюсь подобные практические статьи будут полезны не только системным администраторам, но и веб-разработчикам.
Автор: AlexSerbul