Ни для кого не секрет, что одним из самых популярных способов повышения производительности сайта является использование memcached. Об этом неоднократно говорили и приводили многочисленные примеры. Самый простой способ сделать это — использовать memcached для хранения сессий PHP. Для этого нет необходимости переписывать весь код, достаточно нескольких простых действий. Я не буду рассказывать, почему надо хранить сессии в memcached. Я расскажу о том, почему хранение сессий в memcached опасно.
Счётчик запросов или «Кто виноват?»
Предположим, нам необходимо подсчитать количество переходов пользователя по сайту (на практике это может быть всё, что угодно: от хранения истории перемещения пользователя по сайту до покупок в корзине интернет-магазина). Рассмотрим пример, состоящий из 2 файлов: counter.php и frameset.php:
Открываем frameset.php в браузере и видим: каждый запрос к counter.php увеличивает счётчик в сессии на единицу и счётчик работает правильно. Теперь давайте рассмотрим тот же самый пример, только с сессиями в memcached. Для этого раскомментируем 2 строки в начале скрипта.
Что мы видим? Счётчик работает неправильно. Почему? Давайте разберёмся в этом. Рассмотрим, что происходит в действительности. Если сессия хранится в файле, при вызове session_start файл открывается, блокируется, читается, производится работа с $_SESSION, после чего новое значение записывается поверх старого, снимается блокировка с файла и файл закрывается. При этом параллельный поток честно дожидается снятия блокировки и только после этого работает. К сожалению, в настоящий момент в memcached нет блокировки переменных, потому получается, что оба потока считывают одинаковые исходные данные, обрабатывают их и записывают, при этом все изменения первого потока безвозвратно затираются. В таблице приведена примерная схема работы для этих двух случаев.
С вопросом «Кто виноват?» мы разобрались. Подведём краткие итоги:
Есть вероятность того, что при активном взаимодействии клиента и сервера часть данных будет безвозвратно потеряна;
Переход на хранение сессий в memcached может оказаться просто невозможным;
Memcached позволяет сократить время обработки запроса;
В сессиях желательно хранить только данные, которые редко изменяются (например, профиль пользователя);
При увеличении количества серверов memcahed может выступать как единое хранилище сессий.
Как видим, у хранения сессий в memcached есть не только недостатки.
«Что делать?»
У нас остался только один вопрос — «Что делать?». Скажу сразу, что готового решения у меня нет, однако есть две зарисовки на этот счёт. Обе зарисовки основываются на том, что в memcached всё-таки есть способ организации блокировки. Блокировка базируется на методе add класса Memcache. Про него в документации написано:
Returns TRUE on success or FALSE on failure. Returns FALSE if such key already exist.
Значит, мы можем организовать собственную блокировку вида:
function lock($session_id, $memcache) { $max_iterations = 15; $iteration = 0;
function unlock($session_id, $memcache) { return $memcache->del( `lock_`. $ession_id ); }
Используя эти две функции, мы можем написать свой session save handler и использовать его, однако это повлечёт за собой дополнительную нагрузку на сервер и дополнительного выигрыша в производительности мы не получим.
Я же подошёл к вопросу с другой стороны. Проанализировав свои потребности, я пришёл к выводу, что в действительности мне требуется хранить всего 2–3 группы активно изменяющихся данных. При этом, данные чаще необходимо считывать, а не записывать. Потому я ввёл для себя понятие субсессии. Субсессия (subsession) — виртуальный объект, который физически располагается вне сессии. Субсессия предназначена для хранения часто изменяющихся данных. Если необходимо изменение данных, субсессия блокируется, считывается, изменяется, записывается и разблокируется. Вот как это выглядит со стороны:
Если требуется просто получение данных из субсессии, то можно не блокировать. Итак, что же мне даёт субсессия? Большая часть кода выполняется в неблокированном режиме, потому существенных задержек не происходит. Чтение данных из субсессии тоже происходит в неблокированном виде, так что блокировка действует не в течение всей работы скрипта, а только на коротких её участках. Да, это несколько усложняет код, но, на мой взгляд, преимущества очевидны.
Выводы
Хранение сессий в memcached прекрасно подходит для многосерверных систем. Кроме явного прироста в производительности есть и дополнительный прирост (за счёт отсутствия блокировки). Переход на хранение сессий в memcached очень прост, однако содержит в себе подводные камни. На этапе проектирования системы необходимо учитывать отсутствие блокировки в memcached, и потому надо либо обходить этот момент, либо реализовывать блокировку самостоятельно.