28 августа 2008 в 13:52

Особенности хранения сессий PHP в memcached

PHP*
Данная статья рассматривает одну из проблем хранения PHP-сессий в memcached: отсутствие их блокировки.

Введение

Ни для кого не секрет, что одним из самых популярных способов повышения производительности сайта является использование memcached. Об этом неоднократно говорили и приводили многочисленные примеры. Самый простой способ сделать это — использовать memcached для хранения сессий PHP. Для этого нет необходимости переписывать весь код, достаточно нескольких простых действий. Я не буду рассказывать, почему надо хранить сессии в memcached. Я расскажу о том, почему хранение сессий в memcached опасно.

Счётчик запросов или «Кто виноват?»

Предположим, нам необходимо подсчитать количество переходов пользователя по сайту (на практике это может быть всё, что угодно: от хранения истории перемещения пользователя по сайту до покупок в корзине интернет-магазина). Рассмотрим пример, состоящий из 2 файлов: counter.php и frameset.php:

counter.php
<?php

//ini_set('session.save_handler', 'memcache');
//ini_set('session.save_path', '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://foldo.ru/developer/habrahabr/standard-session/frameset.php

Открываем frameset.php в браузере и видим: каждый запрос к counter.php увеличивает счётчик в сессии на единицу и счётчик работает правильно. Теперь давайте рассмотрим тот же самый пример, только с сессиями в memcached. Для этого раскомментируем 2 строки в начале скрипта.


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         ||                     |                     ||
+--+-------------------+---------------------++---------------------+---------------------++

С вопросом «Кто виноват?» мы разобрались. Подведём краткие итоги:
  1. Есть вероятность того, что при активном взаимодействии клиента и сервера часть данных будет безвозвратно потеряна;
  2. Переход на хранение сессий в memcached может оказаться просто невозможным;
  3. Memcached позволяет сократить время обработки запроса; 
  4. В сессиях желательно хранить только данные, которые редко изменяются (например, профиль пользователя);
  5. При увеличении количества серверов 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, и потому надо либо обходить этот момент, либо реализовывать блокировку самостоятельно.
Алексей Еремихин @alexxz
карма
67,4
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

Комментарии (75)

  • +4
    Мысль интересная, но лично я бы подошел к проблеме счётчика по другому. При инициализации сессии я бы создал переменную, а потом использовал бы такое:
    $memcache->increment('user_visit_counter'.$userid);
    • +6
      Счетчик был взят для примера, чтобы наглядно воспроизвести потерю данных.
  • +2
    тоже заморачивался с сессиями в memcache, при интенсивной работе данные теряются. Писал свою реализацию, вводил отдельную переменную в которой был флаг блокировки сесии, но это всё равно не помогало. Решил отказаться пока от сессий на memcache
  • +2
    Memcache подходит больше для readonly сессий и счётчиков, т.к. у него есть increment/decrement, которые гарантировано без потери данных увеличат/уменьшат счётчик.

    С часто меняющимися данными вообще стандартные сессии плохо использовать, т.к. блокировка файла заставляет остальные процессы ждать, что может привести к скоплению большой очереди, особенно если скрипты тяжеловесные — при большом количестве запросов у вас возникнет проблема. Такие данные нужно блокировать непосредственно перед чтением, после которого в минимальное время идёт запись данных, и разблокировать.

    Систему нужно строить изначально так, что бы сессия была больше readonly, с редко обновляемыми данными. Хранить, к примеру, корзину товаров в сессии, которая в memcache — явно плохая идея, такое нужно хранить в базе.
    • 0
      вы живете в прошлом веке
      ru2.php.net/manual/ru/function.session-write-close.php
      • 0
        Это не решает проблему уничтожения данных, или вы хотите в ручную перебирать данные, сравнивая версию сессии из скрипта и версию сессии из memcache и пробовать решать конфликты в коде?

        Тогда уж надо примерно так:
        Имеем объект, который имеет API для обновления данных в сессии и запоминает все изменения (он хранит оригинальные данные) и при закрытии сессии считывает то, что находиться в memcached на текущий момент и сравнивает с оригинальными значениями. Если данные расходятся — обновляет, а потом уже применяет к ним изменения из скрипта и всё это сохраняет. По крайней мере таким извращённым способом мы избежим долгих блокировок, хотя лично мне такой вариант не очень нравиться.

        Моё имхо остаётся таким-же, как и было — если нам нужно что-то обновить в memcache — то блокируем, читаем, обновляем, разблокируем в том месте, где это надо, а не глобально на весь скрипт (механизм блокировки каждый придумывает сам).

        Данная тема весьма непроста и подойти к ней можно кучей способов и каждый из них имеет недостатки. Идеального решения просто нет.
        • 0
          я это написал касательно файловых сессий, чтобы другим потокам скрипта не пришось долго ждать, можно закрыть сессию по завершении работы с ней.
  • +3
    Искажена идея memcached — хранение кэша, а не данных.
    • –2
      Идея memcached в том что бы хранить в памяти что угодно. В контексте PHP и JAVA удобно хранить объекты. Но это могут быть и бинарные данные, и все что угодно если это выгоднее хранить в памяти чем на диске.
      • +7
        > memcached is a high-performance, distributed memory object caching system

        Memcached не гарантирует сохранность данных, это не система хранения данных, а система кэширования. Почуствуй разницу, username.

        Хранить сессии, на сколько бы они временными не были, в memcached — ошибка проектирования.

        • –1
          … кэширования чего? Не данных ли?
          Кэш тоже данные, продублированные и положенные в доступное место. И там и там храняться данные, насколько я понимаю. Вопрос сохранности и целостности а так же как именно вы их собираетесь использовать вопрос скорее реализации чем дизайна. А данным в сессии быть, поскольку memcached is a high-performance, DISTRIBUTED memory object caching system
          • +1
            Кэш — не дублирование данных. Учите матчасть, в общем. Разговор пустой.
            • 0
              Определите понятие кэш в вашем понимании
              • 0
                ru.wikipedia.org/wiki/Кэш

                Кэш (англ. cache[1]) — промежуточный буфер с быстрым доступом, содержащий копию той информации, которая хранится в памяти с менее быстрым доступом, но с наибольшей вероятностью может быть оттуда запрошена. Доступ к данным в кэше идёт быстрее, чем выборка исходных данных из медленной памяти или их перевычисление, что делает среднее время доступа короче.

                • +1
                  Доступ к чему идет быстрее? Да как они посмели не выучить матчасть… А еще словари пишут
                  • 0
                    К кэшу доступ быстрее. Для того и кэш. Но кэш — не хранилище, еще раз повторюсь.
                    • –17
                      Цитирую
                      Доступ к данным в кэше идёт быстрее, чем выборка исходных данных из медленной памяти или их перевычисление, что делает среднее время доступа короче.
                      Конец цитаты
                    • +3
                      Ему уже нечего доказывать, по нику понятно что он в танке :)
                      • +1
                        Справедливо, мудро, коструктивно.
                        • +1
                          Вот смотрел я на этот диалог и думал. На мой взгляд, оба случая разделяет ответ на один деликатный вопрос. Что делает memcached, когда у него заканчивается память — удаляет старые данные, или всётаки не принимает новых?

                          Если не принимает новых, то можно использовать Memcached как хранилище. Если же удаляет старые данные, то только как кеш.
                          • 0
                            Удаляет, я пытаюсь донести что в кэше тоже храняться данные как и в базе данных как и на диске, как и в файловой системе. А КЭШ это не сущность это механизм хранения. Данных.
                          • +3
                            memcache — это кеш. если память кончается, удаляются самые старые записи:

                            FAQ по мемкешу:
                            «If the server is out of memory, expired slabs are replaced first, then the oldest unused slabs go next.»

                            еще раз: приложение обязано корректно работать в том случае, если нода ляжет.

                            «How is memcached redundant?

                            It's not! Surprise! Memcached is a caching layer for your application. It is not designed to have any data redundancy. If a node loses all of its data, you should still be able to fetch it all again from the source. Especially be careful that your application can survive losing memcached instances. Don't write awful queries and expect memcached to be a fix-all! If you're worried about having too much of a spike in database usage during failure, you have some options. You can add more nodes (lessen impact of losing one), hotspares (take over IP address when down), etc.»

                            www.socialtext.net/memcached/index.cgi
                        • 0
                          Весь этот пост пронизан мудростью :) Начиная с сессий в мемкэшед, заканчивая реализацией слиплока. А здесь ещё вы убеждаете что 'идея мемкэшед в том чтобы хранить в памяти что угодно'.
                          • 0
                            Да именно, если нужно :)
                      • 0
                        Да, не заметил :)

                        Прекращаю кормить тролля…
                    • 0
                      Парень в танке.
  • +2
    Такие вещи решаются помощью инкремента и декримента. Хранить в сессии такую информацию как-то не логично. Лучше мемкеш-хендлера пока ничего не придумали. Как вы обойдетесь без него на высокой нагрузке в ситуации, когда серверов больше, чем один. Save handler реализованный через БД не прокатит, так как база загнется.
    • +1
      Чем то напоминает присказку о микроскопе и гвоздях…
  • +3
    один из больших плюсов использования memcache в качестве хендлера для сессий — отсутствие блокировки. а тут некоторые предлагают от этого плюса отказаться, введя ручную блокировку. это не есть гут.
    • 0
      + есть session_commit().
  • 0
    Использую memcached для хранения cache в symfony. Многое спорно, очень часто функция set возвращает false и ни привета не ответа, что происходит не ясно. Как-то странно работает. Наверное на 1000 memcached серверов оно не так заметно но на 3 как у меня что-то не рабоатет как надо.

    В реализации PECL memcache класса нет никаких способов идентификации ошибок. Щас сижу и не могу понять почему половина кэша не сохраняется :-(

    Может кто что подскажет. У меня 3 сервера Memcache и на доном из них крутится symfony. Под Memcache выделено 2Gb памяти но используется только по 200Mb. В чем может быть проблема с сохранением кэша? И как ошибки то выловить?

    Кто как кстати мониторит состояние memcache? Я юзаю www.cacti.net/
    • 0
      А можно поподробнее о настройке мониторинга?
      • +1
        Тут находится инструкция как мониторить memcache через cacti.

        dealnews.com/developers/cacti/memcached.html

        ставишь cacti потом эти шаблоны и все.
        • 0
          да, я нашел инфу в инете, но к сожалению по этой ссылке битые линки на шаблоны :(
        • 0
          Только не совсем понятно, как с указанным выше темплейтом мониторить несколько демонов memcached на одной машине (висящих на разных портах).
  • –5
    а зачем отдельным процессом считать такие данные?

    у меня все проходит через index.php и подобных проблем нету.
    • 0
      А здесь тоже все данные проходят через один файл counter.php, однако, при наличии двух конкурирующих запросов возможны проблемы. В примере я постарался добиться максимальной воспроизводимости. Если уменьшить время в usleep, то поймать мемент потери данных гораздо сложнее.
      • –5
        зачем вообще разделять?

        єсть запрос на index.php сгенерировал страницу + в базу закинул информацию где был, обновил сессию конец роботы.
      • +1
        я кажется понял — ето просто пример чтоб продемонстрировать как можно потерять данные.

        хотя данную схему приєма статистических данных я б не использовал.
    • –6
      А ещё пхпшники удивляются почему их называют быдлокодерами :)
      — слиплок! ещё и на пхп! ещё и с мемкэшд!
      — мемкэшд для хранения данных
      — каждый php файл — отдельный процесс
      • 0
        Кажется Вы вопроса так и не поняли, меня интересуєт именно почему нужно было процесс сбора таких данных распараллеливать?

        ведь можно все сделать через один поток и я б не сказал что будь большая потеря производительности.

        Попрошу также без таких слов я не из тех програмеров что пишут тока на одном языке.
        • 0
          Если вы про два ифрэйма, то видимо чтобы продемонстрировать проблему когда у человека заела кнопка f5 :) Просто я ваш коммент так прочитал, что связались слова index.php и отдельный процесс.
          А про быдлокодинг — это больше даже не к пхп, а к данной страничке на хабре :) Я даже удивился что столько людей отплюсовало подобный пост и подумалось, что либо они не читали что там внутри, либо — а про быдлокодинг :)
  • 0
    а что если хранить сессии в /dev/shm? блокировка файлов вроде бы поддеживается
    • 0
      На сколько я понял, /dev/shm (tmpfs) сама по себе доступна только на одной машине. Для многосерверной разработки придётся поднимать, например, nfs, которая почти не поддерживает блокировки. Либо другую сетевую файловую систему.
      • 0
        очень даже поддерживает, у нас как раз сессии на nfs
        • 0
          и как работает? насколько нагруженный проект?
          • 0
            работает нормально, уже несколько лет.

            ну и проект довольно нагруженный — бесплатный хостинг, примерно 9 млн. пользователей (на nfs хранятся пользовательские сессии)
            • 0
              дело в том что мы наоборот активно боремся с NFS… когда сервер начинает тормозить, клиенты намертво подвисают, и ничего не поделать.

              Когда я спрашивал про нагруженность, имел в виду скорее интенсивность записи сессий
              • 0
                да, я понимаю что Вы имели в виду, просто другого показателя интенсивности записи сессий привести не могу.

                Может я неясно выразился, но все сессии 9 миллионов пользовательских сайтов хранятся в одном месте. Не знаю правда какой процент сайтов использует сессии.
    • 0
      там ограничена память, вроде в РНР можно тольл 32 Мб, но я не уверен.
  • +1
    Жжоте
  • 0
    статья ни о чём. храню данные (в том числе сессии) в memcache. уже как год использую такую реализацию, проблем нет.
    На счёт быдлокодеров не согласен, есть и умные люди. Но их мало, что огорчает :(
  • 0
    мне кажется тут нарушена сама идея мемкешд. мемкешд для промежуточных либо статических данных.
    Сама по себе возможность модификации этих данных в параллельно происходящих рассчетах есть извращение над идеей.
    • 0
      ** псевдо-статических
  • 0
    Для односерверной конфигурации даже заморачиваться с memcached не стоит. Работать с файлами быстрее чем с memcached через сокеты. И гораздо быстрее будет хранить файлы сессий в диск в памяти (/dev/shm)

    Memcached преднозначен для распределенных систем.
  • 0
    м, если в сессии не очень много данных, можно бы юзать таблицы типа MEMORY =)
  • 0
    приведите, пожалуйста, реальную ситуацию, в которой при выполнении скрипта происходит дважды одновременное (!!!) обращение к значению, хранящемуся в сессии. Просто не укладывается в голове, как же вообще такое может произойти?? А потому и проблема кажется надуманной.
    Также нужно понимать, что последний «писатель» заносит актуальную информацию, и его не волнует, что кто-то из «читателей» получил ее раньше.
    • 0
      Вот как лично я столкнулся с параллельным выполнением.

      Со страницы запускается загрузка большого файла и отображается прогресс бар загрузки. Обновляются данные примерно раз в секунду. Процесс идёт около 20 минут. На обработку аякс запроса в проекте необходимо было запускать сессию. Параллельно с этим открывается новое окно, в котором пользователь продолжает работать и часть данных должна оседать в сессии, если происходили одновременные запросы к серверу, то данные могли не сохраниться. Вот такая рабочая ситуация.
      PS Сейчас за приёмку файлов и аплоад прогресс отвечает самописный сервер, который не затрагивает сессии.
      PPS Не забывайте про медленные каналы, там очень интересно могут запросы приходить.
    • 0
      Юзер открывает страницу на которой Х ссылок. Просмотрев список быстро кликает средней кнопкой Y ссылок которые начинают открываться в фоне. Как отразится на сессии зависит от множества факторов, но шанс потери данных возрастает.
  • 0
    Хочу заметить, для всех читающих этот пост, что на данный момент эта проблема уже не актуальна, в memсache 3.0.4 реализована поддержка локов сессий. pecl.php.net/package-changelog.php?package=memcache
    • 0
      Здорово. Правда еще не в stable версии да и проблемы вытеснения из кэша не решит.
      • 0
        Москва не сразу строилась :)
  • 0
    конечно статья отличная и исследовательская работа произведена качественно
    но можно сессии хранить в shm — не пробовали?
    и доступ быстрее и блокировка предусмотрена.
    • 0
      преимущество мемкеша — его можно посадить на отдельный сервер, и нет проблем масштабируемости
      а shm все-таки живет на том же сервере что и РНР и ограничен физическим размером.
      • 0
        Вы сами ответили на свой вопрос. Могу добавить, что у проекта 8 машин, которым нужны общие сессии.
        • 0
          а про другое хранилище не думали?
          я тут начал тарантула осваивать.
          Для сессий то что надо: и сохраняемость есть как у редиса и скорость по утверждениям разработчиков выше редиса
          • +1
            Проект маленький. Прикручивать к нему монстров типа редиса смысла нет. Мемеш мал и достаточен. Нет смысла переделывать то, что работает и прекрасно справляется с задачей.
            • 0
              Могу добавить, что у проекта 8 машин. Проект маленький.

              Я бы сказал не такой уж и маленький.
              интересные масштабы 8 машин и маленький проект.

              Участвовал в одном проекте, социалка среднего уровня, там было 4 сервера, они позиционируют себя как  HiLoad, 120-130К хостов. Так там использовали и мемкеш и редис и даже монгоДБ собирались использовать (не знаю как все закончилось...) Так что, на мой взгляд кол-во компонент не зависит от масштаба проекта.
            • 0
              Прикручивать к нему монстров типа редиса смысла нет

              редис то по сравнению с мускулем — таракан.
            • 0
              Нет смысла переделывать то, что работает и прекрасно справляется с задачей.

              это верно.
              Но если не справляется надо искать альтернативы.
  • 0
    лично я использую для хранения сессий мемкеш
    мой стартап маленький и меня это устраивает.
    потери сессий не наблюдал.

    знаю что в некоторых проектах для хранения сессий используют редис;
    в чем преимущество не знаю, но мемкеш все же немного пошустрее.

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