На днях я столкнулся с очень интересной проблемой. В системе, с которой я разбирался, использовался механизм ограничения времени жизни сессии. Валидация этого времени перекладывалась на плечи 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, то с легкостью восстановим порядок загрузки/обработки сессии:
- open
- read
- gc
- PROGRAM
- 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) говорит само за себя. Ну а главный вывод, конечно, заключается в том, что следует тщательно проверять, даже кажущиеся очевидными части системы. Ошибки системных функций или методов иногда являются их неверной трактовкой, а не ошибками как таковыми.
Всем спасибо за то, что дочитали до конца. Надеюсь, что эта статья оказалась для вас полезной.
ссылка на оригинал статьи http://habrahabr.ru/post/171151/
Добавить комментарий