Блокировки сессий в веб-проектах — выбираем эффективное оружие

    Всем привет!

    В последнее время, в связи с бурным ростом и усложнением фронт-эндов, аяксами и т.п. — все чаще проявляется проблема блокировки сессий во время эксплуатации сайтов на 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 содержат большой набор полезных инструментов, но, к сожалению, далеко не все ими умеют пользоваться. Надеюсь подобные практические статьи будут полезны не только системным администраторам, но и веб-разработчикам.

    1С-Битрикс

    73,00

    Компания

    Поделиться публикацией
    Комментарии 48
      +2
      Нет, я всё понимаю, отраслевое арго, все дела… Типа «у бульдозера клапанА, у оргАна клАпаны».

      Но, тем не менее…

      локировки блокировки
      лочит блокирует
        –7
        Извините, но некогда заниматься лингвистическими мастурбациями :-) Хочется поделиться полезной информацией с коллегами — они поймут из конктеста.
          +2
          Это Васе Пупкину на ЛОРе можно хоть лочить, хоть мастурбировать. А в корпоративном блоге приличной компании «лочить» == «чавкать на пресс-конференции» :)

            –4
            Я использую устоявшийся технологический сленг, понятный профессиональному сообществу. Может переписать все в стихах и завизировать тексты у вас? :-)
              +3
              [irony]Можно стать еще более понятным профессиональному сообществу, если использовать слова «пых», «ось», «таск» и т.д. вместо «лингво-мастурбационных» PHP, операционная система, задача...[/irony]
                –3
                Давайте говорить не дистрибуция, а распределение, запретим говорить слова дистрибутив и форк, а любой автор на хабре должен сидеть перед словарем иностранных слов неделю чтобы его работу оценили филологи, подрабатывающие разработчиками :-)
          –2
          По просьбе преподавателей русского языка и программистов с филологическим образованием заменил слово «локировка» на «блокировка».
          Не заменил следующие слова, гореть мне в аду теперь:
          аякс
          таб
          кастомный
          засыпающий файл
          файлик

          :-)
          +5
          Откройте для себя хранение сессий в MemCache/Redis и будут ваши «локеры» нежными и шелковистыми. И в русском есть достаточно устоявшаяся терминология на тему блокировок.
            0
            Открыл и что, там нет локировок? :-) Конечно есть, и больше. Тут в архитектуре и голове дело — что лочить и когда.
              0
              Есть, но в отличии от файловых операций они требуют меньшего времени и поэтому процессы друг другу мешают в гораздо меньшей степени.
                +2
                Меньшего времени?? Вы хотите сказать что системный вызов к закэшированному файловому дескриптору в памяти медленнее обращения по TCP/IP к memcached или еще более тяжелому слону Redis? Шутите? :-)
                  0
                  Нисколько не шучу.
                  Суммарное время при работе через localhost на блокировку-чтение-освобождение блокировки при работе с Редисом значительно меньше, чем при тех же самых операциях, но с сессиями в файле, коих в директории может быть пара-сотня тысяч.
                    –1
                    Без цифр сложно говорить. Пара тысяч файлов сессий не может тормозить, а когда их сотня тысяч — тут уже проблема с архитектурой, локальное хранилище может оказаться быстрее.

                    Но проблема то, согласитесь, остается — работа с сессией на веб-странице, при которой сессия лочится (сорри, уважаемые лингвисты) блокируется на все время работы скрипта (блин, можно ли писать слово скрипт? может нужно «текстовый файл, содержащий команды интерпретатора PHP»). Статья именно о проблеме работы с сайтом в рамках одной сессии — неужели никто не сталкивался?
                  –1
                  Также хочу отметить, что время ожидания локировки блокировки на несколько порядков выше самого выполнения вызова что к файлу что через локальный сокет к хранилищу типа memcached, redis, mysql. И чем это поможет, если маленький аякс занял сессию на 10 секунд и остальные элементы интерфейса ждут?
                0
                Не понял. А чем будет отличаться блокировка?

                <?php
                
                session_start(); //блокировка
                sleep(30);
                die; //разблокировка
                

                Следующий скрипт сможет выполнить session_start() не раньше, чем через 30 сек.
                  0
                  Даже если переопределить обработчики функций для работы с сессиями? Хмм, этот вопрос требует практической проверки — набросаю тест и посмотрим.
                    0
                    Так обработчик часто в проектах переопределяют и хранят сессии и в БД, и в memcached. Проблема паровозика никуда не уходит — один хит, захватывающий сессию эксклюзивно на 20 секунд и половина воркеров веб-сервера моментально забиваются. Тут нужно в каждом хите либо лочить разные куски объекта сессии :-), либо лочить только для чтения — ту тогда придется переделывать логику приложения…
                      +3
                      Просто, все кто плюсует Greendq, скорее всего, забыли реализовать блокировки в своем обработчике сессий. А без блокировок оно конечно быстро будет работать.
                        +1
                        Если сессию хранить как допустим строку в таблице БД, ее же нужно сначала открыть, затем залочить либо в SHRED (все читают, один пишет), либо в EXCLUSIVE LOCK (один читает и пишет, все ждут), затем записать измененные данные, затем закрыть. Если все будут писать без локировок — случиться каша, согласен.

                        Т.е. проблема даже не PHP и сессий, а глубже — разделение доступа к объекту, семафорами и мьютексами пахнет :-)
                          +2
                          "все читают, один пишет" — будут ждать не на стадии открытия, а на стадии записи, плюс будут читать устаревшие данные (т.к. они еще не записаны).

                          "один читает и пишет, все ждут" — так работает механизм по-умолчанию.

                          Самым распространенным решением, все же, является написание своего обработчика, который работает без блокировок. Таким образом, мы получаем проблемы атомарной работы с данными, но выигрываем скорость. Отсюда имеем:
                          • проблемы будут ограничиваться данными одного пользователя (чья сессия)
                          • их вероятность довольна низка (одновременное открытие многих табов, шустрые обильные аякс-запросы)
                          • цена ошибки, чаще всего, невелика (счетчик пропустит значение)

                          Так и живем.

                          Есть еще подход с куками, упомянутый Maxkn, я о нем задумывался, но на практике не реализовывал, хотя особенно подкупает «отвязка» от сервера.
                            0
                            Если Вам интересен подход с шифрованными/подписанными куками, то помедитировать можно над реализацией в фреймворке Slim и фреймворке MicroMVC. Это микрофреймворки, поэтому с кодом там все предельно просто.

                            P.S. Пользователи фреймворка Slim с опаской относятся к такому механизму хранения данных.
                            0
                            Всё, теперь понятно. Блокировки надо реализовывать самостоятельно (кстати — в документации PHP об этом вообще не упоминается). Но и без блокировок можно работать, если весь код будет следовать чётким правилам по записи и чтению данных из сессии.
                              +1
                              А как следовать этим четким правилам если процессы выполняются параллельно? Нужны примитивы синхронизации, здравствуйте мьютексы и дедушка Дейкстра.
                                +1
                                Ну есть же варианты логики, где читатели не блокируют писателей :) Но синхронизация в случае записи всё равно нужна, согласен.
                            0
                            То есть, ядро PHP внутри самостоятельно использует блокировки для всех переопределяемых функций работы с сессиями, вне зависомости от того, что в самой функции? Странно, я получал совсем другую картину при использовании редиса, надо бы посмотреть исходники PHP, чтобы быть уверенным наверняка, но чего-то меня они совсем не привлекают…
                          0
                          Идея в том, что один скрипт захватывает сессию на 30 секунд, а остальные просто ее пытаются открыть и не собираются спать по 30 секунд — они не смогут этого сделать, А если бы засыпали на 30 секунд, все бы выстроились паровозиком друг за другом — что ударит и по производительности и по загруженности воркеров веб-сервера.
                            0
                            Вас я понял. Ситуация такая:

                            <?php
                            //script1.php
                            session_start(); //блокировка
                            sleep(30);
                            die;
                            


                            <?php
                            //script2.php
                            session_start(); //ждем
                            


                            Все запущенные копии script2.php будут ждать. Верно?
                              0
                              верно. Можно die убрать, он не влияет на логику. Все скриты хотят взять объект сессии эксклюзивно для записи в него, вот и ждут друг друга.

                        +7
                        Читаю заголовок: «Локировки сессий в веб-проектах...» И в сознании откладывается, что речь пойдет о «лакировках», и возникает легкое удивление — а сессии можно еще и «лакировать»? И только потом понимаю, что на самом деле речь о «блокировках»

                        Конец заголовка "… выбираем эффективное оружие" вызывает ожидание, что будет дан некий совет/рецепт/методика о том, как избежать длительных блокировок сессии. Однако, встречаю лишь совет «отстреливать процессы веб-сервера».

                        В общем, упс, как говорят американцы. И, в отличие от автора, не разделяю надежды, что «подобные практические статьи будут полезны… и веб-разработчикам» (акцент на последнюю фразу)
                          –7
                          Уговорили, поправлю. Не ожидал бывших студентов филологов и преподавателей русского языка среди программистов и сисадминов :-)

                          Нет уж, я дал в статье инструмент выявления проблемы и пару способов борьбы. Достаточно, чтобы пойти к программистам с пивом :-)
                            0
                            Если ожидали увидеть исключительно тех, кто русский язык учит по англоязычной документации, то явно ошиблись. :)

                            Про пиво в статье тоже видел, но не подумал, что это тоже один из способов борьбы.

                            А если серьезно: понимаю, что проблема имеет место быть, и согласен, что далеко не всегда корни проблем ищутся, где нужно (поэтому статья, несмотря на придирки, весьма полезна), но в статье вижу методику изучения проблемы, вижу методику сбора статистики, а пару способов борьбы с ней — не вижу. Или пара методов — это «отстрел» и «рефакторинг»? Тогда первый — явно из категории «зловредных советов», а второй — слишком уж общий, типа «надо хорошо учиться и писать правильный код»
                              +1
                              Спасибо за вопрос. Если серьезно, то вижу целесообразным придерживаться при разработке следующих правил:

                              1) Захватывать сессию управляемо — не на все время выполнения скрипта (как работает по умолчанию), а тогда, когда это действительно нужно.
                              2) Не выполнять на хитах долгие операции, способные заблокировать сессию клиента — а быстро оформлять их как задачу (job) например в gearman (или проще через БД) и выполнять в фоне за пределами сессии. Затем браузер периодически проверяет готовность задачи и обновляет часть интерфейса.
                              3) Постараться вынести части веб-сайта в отдельные компоненты, собираемые не в рамках сессии, например через nginx ssi — тогда в сессии нужно будет выполнить минимум бизнес-операций.

                              И т.д. Но общий смысл рекомендаций — не полагаться на режим работы с сессией по умолчанию, а проявить армейскую смекалку :-)
                          0
                          Хабр стал площадкой для лингвистических баталий?
                            –1
                            Я сам в шоке, откуда они появились. Хоть бы один задал вопрос по логике кода, методике анализа проблемы. Один троллинг пока :-)
                            +4
                            Не проще, положить все что надо в куку, куку подписать и отдать пользователю и забыть про весь описанный геморрой? ну если вы конечно не храните в сессии пару мегабайт
                              0
                              Так сессию ж специально придумали, в частности, чтобы не гонять информацию туда-сюда через куки. В куках только ID сессии лежит.
                                +2
                                Когда сессию придумывали были несколько иные окружающие условия. Сейчас же вы ради хранения 1-2к информации о пользователе придумали адский костыль, который еще и не будет работать если веб серверов станет больше одного. в попретесь в сторону редиса, потом будете долго реплицировать, потом шардить и реплицировать. потом на вас обрушитьс еще пара напастей. зачем?
                                  0
                                  Соглашусь, что в некоторых довольно простых кейсах можно рассмотреть вопрос хранения только кук. Но ~4кБ для приложений со сложной бизнес-логикой станет потолком в объеме данных сессии, придется еще где-то хранить.
                                    0
                                    гкхм, извините, а что вы в сессии храните?
                                +1
                                Это не решает проблему гонок. Вот представьте, есть у вас скрипт, который должен увеличивать значение в «сессии» на единицу при каждом обращении. В случае с куками и асинхронными запросами со стороны клиента может получиться такая вот ситуация:

                                когда, несмотря на то, что запросов к серверу было 2, в итоге значение было увеличено лишь на единицу.
                                  0
                                  на мой взгляд, это слегка странное желание что-то увеличивать в сессии при каждом запросе. как только у вас в проекте фронтенд серверов станет больше 1 и разные запросы одного пользователя начнут выполнятся на разных серверах — сразу поплохеет. потом захотите патчить продакшен без потери текущих сессий — опять проблемы.

                                  сессии были придуманы очень давно. когда были иные задачи, иные возможности для их реализации и иные нагрузки. Они плохо масштабируются, висят мертвым грузом на сервере, пока пользователь наливает себе кофе и порождают непонятные пляски с бубнами типа описанных в топике. Если начинаются пляски вокруг сессий в почти 100% случаев это говорит о неправильной организации данных.

                                  Вместо сессии кука, если что-то нужно помнить на сервере — кладите в кеш ( при этот перед тем как положить что-то в кеш, нужно вспомнить, что на клиенте сейчас есть куча своего и достаточно емкого кеша, от локального стора до веб-дб и может имеет смысл хранить это там где за хранение платит клиент)
                                    0
                                    на мой взгляд, это слегка странное желание что-то увеличивать в сессии при каждом запросе.

                                    Замените «увеличить» на «добавить товар в корзину», если вам так будет проще представить суть проблемы. А если у вас нет вычислений, которые, основываются на данных из сессии и результат пишут туда же, то и с серверной стороны вам блокировки не нужны.
                                0
                                Что-то не уловил о каких блокировках идет речь. Очевидно sessionID разные для разных сессий, и выстраиваться в одну очередь будут скрипты лишь которые получили один sessionID из запроса. Т.е. 100500 аяксов одного пользователя может и выстроятся в очередь — так проблема в архитектуре бизнес-процессов а не в блокировках. В остальном ничего такого супер-страшного чтобы панику наводить. Хочется использовать воркеры — так уж можно и позаботиться о shared_memory и семафорах для разделяемых сессий. Вот автор, вы этим и займитесь, но за примеры мониторинга тоже спасибо!
                                  0
                                  А ведь на дворе уже 2013й год

                                  Почему ни одного упоминания про Web storage?
                                    0
                                    Уважаемому автору не встречались случаи зависания php-cgi в функции poll_schedule_timeout? Загрузка ЦПУ при этом падает до 0, и лечится только перезапуском php-cgi.
                                      0
                                      Можно попытаться войти в процесс через gdb и посмотреть чем он занимается в этот момент. strace — не покажет деталей тут.
                                        0
                                        strace показывает в цикле:

                                        poll([{fd=8, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}], 1, 0) = 0 (Timeout)
                                        poll([{fd=8, events=POLLIN|POLLPRI|POLLRDNORM|POLLRDBAND}], 1, 1000) = 0 (Timeout)
                                      0
                                      войти в процесс через gdb

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                      Самое читаемое