Пользователь
0,0
рейтинг
1 марта 2013 в 02:28

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

Всем спасибо за то, что дочитали до конца. Надеюсь, что эта статья оказалась для вас полезной.
Бакин Влад @mixkorshun
карма
10,0
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (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
        Собственно фокус в том, что любой нормальный разработчик сессии складывает не в дефолтную папку-кучу, а в свою. и сторонний уборщик мусора тогда совсем не может чистить сессии, попка засирается и получаем ошибки при создании сессии.

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