Как стать автором
Обновить

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

Время на прочтение 4 мин
Количество просмотров 15K

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

Всем спасибо за то, что дочитали до конца. Надеюсь, что эта статья оказалась для вас полезной.
Теги:
Хабы:
+4
Комментарии 12
Комментарии Комментарии 12

Публикации

Истории

Работа

PHP программист
171 вакансия

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн