Медленный старт сессий при использовании Redis в Magento

При использовании Redis в качестве хранилища сессий, можно столкнуться с проблемой медленного старта и чтения сессии. В этой статье мы рассмотрим причины возникновения этих проблем на примере модуля Cm_RedisSession.

Проблема

Часто при мониторинге интернет магазинов на Magento, использующих Redis в качестве хранилища сессий, можно наблюдать такую картину в New Relic:

На этом скриншоте видно, что старт сессии занял почти 30 секунд.

Другой пример:

В этом примере чтение данных сессии заняло примерно 15 секунд.

Попробуем разобраться почему так происходит.

Redis и Magento

Краткая справка:

Redis is an open source (BSD licensed), in-memory data structure store, used as database, cache and message broker. It supports data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs and geospatial indexes with radius queries.
источник 1

Redis наряду с memcached может успешно использоваться для хранений сессий в Magento. Кроме того, Redis может использоваться в качестве хранилища кеша.

Подробную информацию по настройке и установке Redis можно найти в источнике 2.

Стандартным модулем Magento для хранения сессий в Redis является Cm_RedisSession (источник 3). Данный модуль уже включен в официальный дистрибутив Magento, и располагается в папке app/code/community/Cm/RedisSession.

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

В данном классе есть два основных метода для чтения и записи данных сессии, которые называются соответственно read и write. Обе эти функции имеют достаточно сложную логику, но больше всего внимания привлекает следующий фрагмент кода в функции read:

    public function read($sessionId)
    {
       ...
       while ($this->_useLocking) {
           ...
           // Timeout
           if ($tries >= $this->_breakAfter + $this->_failAfter) {
               $this->_hasLock = FALSE;
               if ($this->_logLevel >= Zend_Log::NOTICE) {
                   $this->_log(sprintf("Giving up on read lock for ID %s after %.5f seconds (%d attempts)", $sessionId, (microtime(true) - $timeStart), $tries), Zend_Log::NOTICE);
               }
               break;
           } else {
               if ($this->_logLevel >= Zend_Log::DEBUG) {
                   $this->_log(sprintf(
                       "Waiting %.2f seconds for lock on ID %s (%d tries, lock pid is %s, %.5f seconds elapsed)",
                       $sleepTime / 1000000, $sessionId, $tries, $lockPid, (microtime(true) - $timeStart)
                   ));
               }
               Varien_Profiler::start(__METHOD__ . '-wait');
               usleep($sleepTime);
               Varien_Profiler::stop(__METHOD__ . '-wait');
           }
       }
       ...
    }

Как можно понять из кода, модуль применяет механизм блокировок, для исключения конкурентного доступа к сессии. Это означает, что если клиент открывает медленную страницу и, не дожидаясь ее окончательной загрузки, пытается перейти на другую страницу, то второй запрос при обращении к сессии будет ожидать завершения первого запроса, делая паузы внутри цикла, ожидая разрешения на чтение данных. При этом в один момент времени только один процесс может быть «владельцем» сессии. ID процесса-владельца сохраняется в Redis и формируется следующим образом

public function _getPid()
{
return gethostname().'|'.getmypid();
}

Если после истечения определенного времени процесс не получил доступ на чтение данных, «владельцем» сессии становится текущий процесс.

За отключение механизма блокировок отвечает параметр disable_locking, в файле с настройками сайта app/etc/local.xml

<config>
    <global>
    ...
       <redis_session>
        ...
           <disable_locking>0</disable_locking>
<break_after_frontend>5</break_after_frontend>
<break_after_adminhtml>30</break_after_adminhtml>
        <max_concurrency>6</max_concurrency>
    ...
       </redis_session>
    ...
    </global>
</config>

Другие параметры, влияющие на механизм блокировки сессий:

max_concurrency - максимальное число процессов (запросов), которые могут ожидать блокировки для одной сессии. При превышении этого числа, пользователю будет показана страница с ошибкой «Error 503: Service Unavailable»;

break_after_frontend - сколько секунд процесс будет ожидать освобождения блокировки, после чего владельцем сессии считается текущий процесс;

break_after_adminhtml - настройка имеет тоже самое значение, что и break_after_frontend, но для административной области сайта.

Параметр disable_locking по умолчанию имеет значение 0 (блокировка включена) , что обеспечивает целостность и непротиворечивость данных при работе с сессиями. Выключать блокировки на “живом” магазине не рекомендуется.

Рекомендуемые значения параметров:

disable_locking = 0
break_after_frontend = 5
break_after_adminhtml = 30
max_concurrency = 6

Баг Cm_RedisSession

На момент написания статьи, в модуле Cm_RedisSession (v.0.2) присутствовал баг, в результате которого параметры break_after_* не читались из файла app/etc/local.xml , при этом использовалось значение по умолчанию 30 секунд, что можно считать слишком большим значением для клиентской области сайта.

Баг вызван следующей строчкой в классе Cm_RedisSession_Model_Session

$this->_breakAfter =          ((float) $config->descend('break_after_'.session_name()) ?: self::DEFAULT_BREAK_AFTER);

На момент выполнения этого кода, значение session_name() равно «PHPSESSID».

Исправить данную проблему можно следующим образом:

$currentArea = Mage::app()->getStore()->isAdmin() ? 'adminhtml' : 'frontend';
        $this->_breakAfter =          ((float) $config->descend('break_after_'.$currentArea) ?: self::DEFAULT_BREAK_AFTER);

При внесении изменении в файл не забудьте скопировать его в папку local с сохранением полного пути!

Заключение

Таким образом, становится понятно, чем были вызваны задержки скорости загрузки некоторых страниц сайта. В целях отладки и поиска “узких” мест в коде можно отключить блокировку и провести нагрузочное тестирование сайта, при этом из статистики будут исключены медленные запросы, связанные с ожиданием конкурентных запросов.

Источники
1. Сайт REDIS
2. Настройка Redis & Magento
3. Модуль Cm_RedisSession