| « Компания Agava (а конкретно - баннерная сеть tbn.ru) - это не только ценный мех, но и полнейший болт на поддержку пользователей | Первый роллердром 2010 » |
Распараллеливаем сайты: сессии PHP в БД - проблема и решение
Прочее IT, Сети и администрирование Add commentsДолго не писал в блог. К сожалению, времени, как всегда, крайне мало. Занимался ATLAC, правда сообщить пока нечего, кроме того, что ядро концентратора перешло из стадии beta в стадию RC.
В остальное время тихонько, но напряженно работаю над отказоустойчивой системой основных сервисов провайдера - RADIUS, личного кабинета, сайта компании. Развернул MySQL Cluster - как-нибудь напишу и о нем, но не в этот раз. RADIUS уже работает на кластере, и перестал зависеть от конкретного сервера. ATLAC тоже работает с двумя RADIUS-серверами вполне вменяемо, даже баги ловить не пришлось.
Встала задача распараллеливания сайтов. Собственно - поднять на фронтендах балансировщик и распараллелить на два бэкенда, а ротацию по фронтендам сделать в DNS - дело нехитрое. Но есть одно маленькое "но", которое обычно отбивает у начинающих желание связываться с кластеризацией HTTP.
Это "но" называется сессии PHP. Обычно PHP хранит сессии в файлах. Увы, если не использовать хитро-мудреные кластерные файловые системы на общем хранилище (которого нет, разве что DRBD лепить), NFS (у которого на такой мелочи, как сессии, явно будут проблемы с производительностью) и прочие извращения - проблема весьма нетривиальная.
Изучение и решение проблемы читайте под катом, по ссылке "Читать далее"
Скачать модуль Lockable MySQL sessions handler
Не забудьте прочитать о том, что это такое
Все по той же ссылке.
...
Первая проблема заключается в том, что серверам обязательно надо видеть сессии друг друга. Особенно при балансировке нагрузки. Иначе может получиться так, что создадите сессию на одном сервере, а при следующем запросе - попадете на другой, и там ее тю-тю. Вследствие чего авторизация работать не будет, к примеру, или начнут лезть вообще непредсказуемые глюки (особенно если данные будете хранить в сессиях) - разброс попаданий по серверам достаточно непредсказуем.
Перво-наперво на ум приходит самое очевидное решение. Хранить сессии в базе MySQL (благо под рукой уже есть NDB Cluster), или в каком-нибудь memcached. Последний отпал сразу, как только выяснилось, что при любом падении/рестарте его демона он теряет все данные, да и просто по велению левой задней ноги (по expire или нехватке памяти) может их терять. Сессии - штука тонкая, и обращаться с ними как попало не стоит.
А значит, остается MySQL. Написать для PHP модули сохранения сессий в базу, казалось бы - задача совсем нехитрая. Но есть в ней проблема номер два - эдакий "подводный камень", также зачастую отбивающий у новичков желание этим заниматься. Это проблема блокировки сессий.
Когда PHP сохраняет сессии в файл, многие могли заметить, что если открыть сессию и не сделать session_write_close, то пока один запрос конкретного пользователя отрабатывает (при долгих запросах очень заметно), то все последующие запросы "подтормаживают" до его завершения. Это связано с тем, что PHP блокирует файл сессии, и не отпускает его до момента закрытия сессии. С одной стороны - кажется недоразумением. С другой стороны - весьма важная вещь - механизм блокировки сессий осуществляет сериализацию - упорядочивание доступа к ним.
Представьте себе, что блокировки нет. Пользователь открыл два окна, и одновременно запустил два запроса. Оба запроса считали данные сессии, поправили в них что-то свое, и записали назад. Ой. В итоге сессия будет содержать только данные из того запроса, который выполнил запись последним. Понятное дело, что так жить нельзя - данные в сессии от первого запроса просто потеряны. В случае, если блокировка есть, запрос, стартовавший позже, не откроет сессию, и не прочитает ее данные, пока запрос, стартовавший раньше, не запишет, и не освободит данные сессии. Таким образом несмотря на явное "притормаживание" второго запроса, он считает сессию уже с данными первого, изменит, и сохранит все целиком.
В случае, когда мы начинаем хранить данные в базе MySQL, мы натыкаемся как раз на эти самые грабли. MySQL не блокирует запись при чтении, и повторно считать и изменить ее может кто угодно. Да, в InnoDB есть row-level locking при выполнении SELECT ... FOR UPDATE, но у нас может быть и не InnoDB. Конкретно в моем случае - NDBCLUSTER, у которого page-level locking, а это нам не подходит.
Решение нашлось почти сразу. MySQL (в т.ч. NDBCLUSTER) имеет механизм пользовательских блокировок через команды GET_LOCK и RELEASE_LOCK. Первая получает именованную блокировку, при этом может ожидать ее освобождения другим процессом. Вторая - снимает эту самую блокировку. Все блокировки в конкретном сеансе связи с MySQL снимаются при закрытии этого сеанса. Таким образом, в MySQL имеется механизм, который позволит нам упорядочить доступ к данным сессий.
После небольшого изучения предмета и тестирования этот механизм и был реализован в модуле, который вам предлагается скачать ниже. Этот модуль позволяет сохранять данные сессии в базе данных MySQL с корректной их блокировкой при доступе средствами именованных блокировок MySQL. Таким образом, работа с сессиями в БД при использовании данного модуля ничем не будет отличаться от работы с сессиями в файлах - в общем случае вы просто не заметите разницы.
Скачать модуль Lockable MySQL sessions handler
Дам кратенькое описание по использованию данного модуля. Код самодокументирующийся, параметры описаны (на английском, правда). Здесь только пример:
Создаем таблицу сессий в Вашей БД:
CREATE TABLE `sessions` (
`id` VARCHAR(255),
`value` BLOB,
`updated` TIMESTAMP,
PRIMARY KEY (`id`),
INDEX (`updated`)
)
В самом начале кода делаем следующее:
require_once('mysql_sessions.php'); # подключаем наш модуль
$mysql_sessions = new MYSQL_SESSIONS(); # создаем объект
$mysql_sessions->server = 'localhost'; # задаем сервер MySQL
$mysql_sessions->user = 'user'; # пользователь для доступа к БД
$mysql_sessions->password = 'password'; # пароль для доступа к БД
$mysql_sessions->base = 'mydb'; # имя БД, в которой лежит таблица сессий
$mysql_sessions->table = 'sessions'; # имя нашей таблицы сессий
Можно еще задать $mysql_sessions->lockname, но если не задать он будет равен 'mydb.sessions', т.е. <база>.<таблица> - это префикс названия блокировок сессий в GET_LOCK/RELEASE_LOCK.
Есть еще параметры compress, compress_level - они определяют, сжимать ли данные сессий с помощью gzcompress, и с каким уровнем сжатия. По умолчанию - TRUE и 9.
А еще есть lock_retry_sleep, lock_timeout, lock_fail_timeout - первые два, если не понимаете моего кода и механизмов работы PHP/MySQL, трогать настоятельно не рекомендуется, а последний указывает - сколько ждать освобождения блокировки сессии перед тем, как вывалиться с ошибкой, если сессия кем-то нагло заблокирована. По умолчанию - 60 секунд.
После чего делаем.
$mysql_sessions->init() # и наконец - устанавливаем обработчики сессий
Вуаля. Теперь можно использовать session_start и прочее - работать будет так же, как и с файлами. Ни в коем случае после начала работы не меняйте нигде параметр compress с TRUE на FALSE и наоборот - потеряете все данные сессий и получите вагон ошибок. Если меняете - не забудьте очистить таблицу сессий.
Собственно, вот и все. Надеюсь, модуль поможет вам в создании отказоустойчивых Web-приложений, и приложений, разнесенных на несколько серверов.
Почти обо всём было известно и всё использовалось, но всё равно написано хорошо - те, кто не в теме, по этому посту быстро вкурят, надеюсь. :)
"но если не задать он будет равен 'mydb.sessions'"
Т.е. по сути при запросе на конкретную сессию будут лочиться все? В таком случае проще именованного лока вероятно лучше было бы воспользоваться LOCK TABLES...
А почему имя лока нельзя связать с id сессии? тогда бы лочилась только конкретная сессия
'SELECT GET_LOCK('.
'"'.mysql_real_escape_string($this->lockname.'_'.$id, $this->dbid).'",'.
mysql_real_escape_string($this->lock_timeout, $this->dbid).
я просто в код не смотрел, а по тексту значит просто неправильно понял.
Тогда всё отлично :)
А можно чуть подробнее. Я реально не догоняю, ка, наример записать $data с помощью function _write($id, $data) ну и т.д.
Данный модуль представляет из себя замену стандартным обработчикам сессии в PHP, он должен использоваться строго так, как описано в тексте.