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

Всем спасибо за то, что дочитали до конца. Надеюсь, что эта статья оказалась для вас полезной.

ссылка на оригинал статьи http://habrahabr.ru/post/171151/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *