Неожиданное поведение Garbage Collector'а сессий


    На днях я столкнулся с очень интересной проблемой. В системе, с которой я разбирался, использовался механизм ограничения времени жизни сессии. Валидация этого времени перекладывалась на плечи garbage collector'а, который почему-то её выполнял не совсем добросовестно, а то и вовсе не выполнял. Как оказалось, ошибки эти общераспространенных, по этому о тонкостях работы с GC я и хотел бы рассказать.

    В php за работу GC для сессий отвечают 3 параметра: session.gc_probability, session.gc_divisor и session.gc_maxlifetime.
    Эти параметры говорят о следующем: в gc_probability из gc_divisor запусков session_start запускается GC, который должен очистить сессии со временем последнего обращения больше, чем gc_maxlifetime.




    Делаем как все, или пример №1


    Попробуем протестировать работу GCна маленьком скрипте:
    <?php
    	ini_set("session.gc_maxlifetime", 1);
    
    	session_start();
    	if (isset($_SESSION['value'])) {
    		$_SESSION['value'] += 1;
    	} else {
    		$_SESSION['value'] = 0;
    	}
    
    	echo $_SESSION['value'];
    ?>
    
    

    Обновим этот файл 10 раз с промежутком секунд по 10-15(можно и больше, важно чтобы промежуток был выше чем 1 секунда). В результате мы получим «неожиданные ответы»:
    0
    1
    2
    3
    ...
    

    Причина довольно проста и, я бы сказал, очевидна:
    gc запустится только в 1 из 1000 запросов, а мы сделали всего 15.

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

    Обойти баг любой ценой, или пример №2


    Решение проблемы кажется простым — а что если запуск GC сделать принудительным?
    <?php
    	ini_set("session.gc_maxlifetime", 1);
    
    	ini_set("session.gc_divisor", 1);
    	ini_set("session.gc_probability", 1);
    
    	session_start();
    	if (isset($_SESSION['value'])) {
    		$_SESSION['value'] += 1;
    	} else {
    		$_SESSION['value'] = 0;
    	}
    
    	echo $_SESSION['value'];
    ?>

    Но поведение этого скрипта становится намного более неожиданным. Давайте попробуем повторить такие же действия, что и для примера №1:
    0
    1
    0
    1
    ...
    


    Разбор полетов, или почему так происходит


    Если мы повесим обработчики, с помощью session_set_save_handler, то с легкостью восстановим порядок загрузки/обработки сессии:
    1. open
    2. read
    3. gc
    4. PROGRAM
    5. close

    Т.е. garbage collector запустился уже после чтения сессии, а значит массив $_SESSION уже заполнен. Вот отсюда и возникает неожиданная единица во втором примере!

    Вернемся к 1ому примеру


    Как мы теперь видим, сборщик мусора может запустится на 3ем шаге, но что же произойдет если он не запустится? Ведь при стандартных настройках шанс на запуск всего 1 из 1000.
    Устаревшая сессия успешно откроется, прочитается, а в конце работы сохранится и время последнего обращения к файлу будет обновлено — в этом случае такая сессия становится почти бесконечной. Но, в тоже время, если наш скрипт использует 1000 разных пользователей, то о «бесконечности» сессии можно забыть, т.к. GC скорее всего запустится у кого либо из пользователей, время жизни начнет работать верно(точнее почти верно). Такое поведение системы неоднозначно и непредсказуемо, а это потенциально приведет к большому количеству трудно отлавливаемых проблем.

    И что теперь делать, или выходы из ситуации


    Самым верным решением, является использования своего механизма валидации сессии. В документации явно сказано что
    «session.gc_maxlifetime задает отсрочку времени в секундах, после которой данные будут рассматриваться как „мусор“ и потенциально будут удалены. Сбор мусора может произойти в течение старта сессии (в зависимости от значений session.gc_probability и session.gc_divisor).» Слова «потенциально» и «может», как раз и говорят о том, что gc не предназначен для ограничения времени жизни сессии. В тех местах, где время жизни сессии важно, а возникновение артефактов, как из примера №2 критично, используйте свою валидацию времени жизни.

    Выход №2, плохой и неправильный

    Мы знаем, что установленный «принудительный режим» работы gc отработает на шаге №3 старта сессии. Т.е. фактически после старта устаревшей сессии данные в массиве $_SESSION присутствуют, а файл уже удален. В таком случае логично попробовать пересоздать сессию, т.е фактически сделать запуск 2 запуска session_start:
    <?php
    	ini_set("session.gc_maxlifetime", 1);
    
    	ini_set("session.gc_divisor", 1);
    	ini_set("session.gc_probability", 1);
    
    	session_start();
    	if (isset($_SESSION['value'])) {
    		$_SESSION['value'] += 1;
    	} else {
    		$_SESSION['value'] = 0;
    	}
    
    	echo $_SESSION['value'];
    	session_commit();
    	session_start();
    	echo ' '.$_SESSION['value'];
    ?>

    Результаты работы скрипта будут:
    0 0
    1
    0 0
    1
    ...
    


    Это поведение ясно из порядка обработки сессии, но(вспомним документацию, да и вообще взглянем адекватно) делать так не стоит.

    Ура, разобрались — вывод


    Меня удивило, что большинство, даже опытных, разработчиков ни разу не задумывались о поведении GC, беззаботно доверяя ему ограничение времени жизни сессии. При том что в документации явно указано, что делать этого не стоит, а название Garbage Collector(не Session Validator, или Session Expire) говорит само за себя. Ну а главный вывод, конечно, заключается в том, что следует тщательно проверять, даже кажущиеся очевидными части системы. Ошибки системных функций или методов иногда являются их неверной трактовкой, а не ошибками как таковыми.

    Всем спасибо за то, что дочитали до конца. Надеюсь, что эта статья оказалась для вас полезной.
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 12
    • +5
      А где баг?
      • –1
        В том и дело, что бага нет.
        Но почему-то люди привыкли, что gc_maxlifetime — это время жизни сессии, хотя это далеко не так
        • +4
          гарантированное время жизни, уточню. И это так.
          • 0
            session_destroy() так не думает.
            Этот параметр отвечает за время, после которого приложение гарантированно не использует сессию с таким идентификатором, а значит она потенциально может быть удалена.(см. документацию)
            • 0
              gc_maxlifetime отвечает за время, спустя которое сессия будет считаться мусором, а за чистку мусора отвечает gc_divisor и gc_probability.

              session.gc_maxlifetime specifies the number of seconds after which data will be seen as 'garbage' and potentially cleaned up.

              Документацию пишут не просто так.
              • 0
                «гарантированно не использует сессию с таким идентификатором» эквивалентно «will be seen as 'garbage'»
                «она потенциально может быть удалена» тоже, что и «potentially cleaned up»

                У меня складывается такое ощущение, что мы с вами говорим об одном и том же, но никак не можем согласиться друг с другом
                • +1
                  Скорей «будет рассматриваться как 'мусор'»
      • +1
        GC запускается каждый раз при вызове session_start(). На больших проектах много сессий и соответственно это дополнительные потери времени, которые нельзя игнорировать т.к. влияют на время ответа.
        У меня GC отключен и принудительно запускается отдельным скриптом по cron. К сожалению в PHP много таких старых проблем.
        • –1
          Первый раз вижу такое применение GC. O_o
          Документация же описывает, что обьекты будут помечены, как мусор и возможно произойдёт освобождение памяти.
          • +1
            Самое смешное, что нет гарантии что gc уберёт сессию, в дебианах был отдельный баш скрипт, который чистил сессии, а gc вовсе их не чистил. Не копал из-за сухосина это или ещё от чего-то, опытные разработчики знают, что если хочешь быть уверенным в результате, то нужно это делать самому.
            • +1
              GC был специально отключен, так как не мог работать из-за принятых мер по повышению безопасности. На /var/lib/php5 выставлены права, не позволяющие получить список файлов, и тем самым узнать доступные идентификаторы сессий. Плюс гарантия, что сессия будет уничтожена, даже если запросов со стороны клиентов не поступало.
              • +1
                Собственно фокус в том, что любой нормальный разработчик сессии складывает не в дефолтную папку-кучу, а в свою. и сторонний уборщик мусора тогда совсем не может чистить сессии, попка засирается и получаем ошибки при создании сессии.

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