Особенности хранения сессий PHP в memcached
Раздел:
Programming /
PHP
@
28.08.2008 |
Ключевые слова: php memcached
Автор: Алексей Источник: habrahabr
ВведениеНи для кого не секрет, что одним из самых популярных способов повышения производительности сайта является использование memcached. Об этом неоднократно говорили и приводили многочисленные примеры. Самый простой способ сделать это — использовать memcached для хранения сессий PHP. Для этого нет необходимости переписывать весь код, достаточно нескольких простых действий. Я не буду рассказывать, почему надо хранить сессии в memcached. Я расскажу о том, почему хранение сессий в memcached опасно.
Счётчик запросов или «Кто виноват?»Предположим, нам необходимо подсчитать количество переходов пользователя по сайту (на практике это может быть всё, что угодно: от хранения истории перемещения пользователя по сайту до покупок в корзине интернет-магазина). Рассмотрим пример, состоящий из 2 файлов: counter.php и frameset.php:
counter.php<?php
//ini_set(`session.save_handler`, `memcache`); //ini_set(`session.save_path`, `http://www.rusdoc.ru/go.php?tcp://localhost:11211`);
session_start();
$_SESSION[`habra_counter`] = isset($_SESSION[`habra_counter`])? $_SESSION[`habra_counter`]: 0;
usleep(1000000); // Полезная работа
$_SESSION[`habra_counter`] ++; // Счётчик
usleep(1000000); // Полезная работа
echo `Page count `. $_SESSION[`habra_counter`];
?>
frameset.php<?php session_start(); // это чтоб кука встала ?> <form action="" method="post" onsubmit="work(); return false;" > <input type="submit" name="submit" value="Work" /> </form>
<iframe src="" name="iframe1" id="idframe1"></iframe> <iframe src="" name="iframe2" id="idframe2"></iframe>
<script> function work (){ document.getElementById(`idframe1`).src = `counter.php? f=1` + Math.random(); document.getElementById(`idframe2`).src = `counter.php? f=1` + Math.random(); } </script>
http://www.rusdoc.ru/go.php?http://foldo.ru/developer/habrahabr/standard-session/frameset.php
Открываем frameset.php в браузере и видим: каждый запрос к counter.php увеличивает счётчик в сессии на единицу и счётчик работает правильно. Теперь давайте рассмотрим тот же самый пример, только с сессиями в memcached. Для этого раскомментируем 2 строки в начале скрипта.
http://www.rusdoc.ru/go.php?http://foldo.ru/developer/habrahabr/memcache-session/frameset.php
Что мы видим? Счётчик работает неправильно. Почему? Давайте разберёмся в этом. Рассмотрим, что происходит в действительности. Если сессия хранится в файле, при вызове session_start файл открывается, блокируется, читается, производится работа с $_SESSION, после чего новое значение записывается поверх старого, снимается блокировка с файла и файл закрывается. При этом параллельный поток честно дожидается снятия блокировки и только после этого работает. К сожалению, в настоящий момент в memcached нет блокировки переменных, потому получается, что оба потока считывают одинаковые исходные данные, обрабатывают их и записывают, при этом все изменения первого потока безвозвратно затираются. В таблице приведена примерная схема работы для этих двух случаев.
+--+-----------------------------------------++-------------------------------------------++
| | Сессии на жёстком диске || Сессии в memcache ||
+--+-------------------+---------------------++---------------------+---------------------++
| | Поток 1 | Поток 2 || Поток 1 | Поток 2 ||
+--+-------------------+---------------------++---------------------+---------------------++
|1 | open file | || connect memcache | ||
|2 | lock file | open file || read memcache 5 | connect memcache ||
|3 | read file 5 | lock file || work 5+1| read memcache 5 ||
|4 | work 5+1 | lock || write memcache 6 | work 5+1||
|5 | write file 6 | lock || close memcache | write memcache 6 ||
|6 | unlock file | lock || | close memcache ||
|7 | close file | read file 6 || | ||
|8 | | work 6+1 || | ||
|9 | | write file 7 || | ||
|10| | unlock file || | ||
|11| | close file || | ||
+--+-------------------+---------------------++---------------------+---------------------++ С вопросом «Кто виноват?» мы разобрались. Подведём краткие итоги:
- Есть вероятность того, что при активном взаимодействии клиента и сервера часть данных будет безвозвратно потеряна;
- Переход на хранение сессий в 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;
while( !$memcache->add( `lock_`. $session_id, ...) ) { $iteration++; if( $iteration > $max_iterations) { return false; } usleep(1000); } return true; } function unlock($session_id, $memcache) { return $memcache->del( `lock_`. $ession_id ); }
Используя эти две функции, мы можем написать свой session save handler и использовать его, однако это повлечёт за собой дополнительную нагрузку на сервер и дополнительного выигрыша в производительности мы не получим.
Я же подошёл к вопросу с другой стороны. Проанализировав свои потребности, я пришёл к выводу, что в действительности мне требуется хранить всего 2–3 группы активно изменяющихся данных. При этом, данные чаще необходимо считывать, а не записывать. Потому я ввёл для себя понятие субсессии. Субсессия (subsession) — виртуальный объект, который физически располагается вне сессии. Субсессия предназначена для хранения часто изменяющихся данных. Если необходимо изменение данных, субсессия блокируется, считывается, изменяется, записывается и разблокируется. Вот как это выглядит со стороны:
$this->session->init_subsession(`fupload`, $this->memc); // инициализация субсессии /* lock */ $this->session->fupload->lock(); // блокируем субсессию $fupload = $this->session->fupload->get(); // получаем субсессию $fupload = is_array($fupload)? $fupload: array(); // проверяем на корректность $fupload[] = $new_data; // добавляем данные $this->session->fupload->set($fupload); // записываем сессию /*unlock*/ $this->session->fupload->unlock(); // снимаем блокировку
Если требуется просто получение данных из субсессии, то можно не блокировать. Итак, что же мне даёт субсессия? Большая часть кода выполняется в неблокированном режиме, потому существенных задержек не происходит. Чтение данных из субсессии тоже происходит в неблокированном виде, так что блокировка действует не в течение всей работы скрипта, а только на коротких её участках. Да, это несколько усложняет код, но, на мой взгляд, преимущества очевидны.
ВыводыХранение сессий в memcached прекрасно подходит для многосерверных систем. Кроме явного прироста в производительности есть и дополнительный прирост (за счёт отсутствия блокировки). Переход на хранение сессий в memcached очень прост, однако содержит в себе подводные камни. На этапе проектирования системы необходимо учитывать отсутствие блокировки в memcached, и потому надо либо обходить этот момент, либо реализовывать блокировку самостоятельно.
Вернуться в раздел:
Programming /
PHP
|