Рабочее тегирования для кэша в памяти
Недавно встала, казалось бы, типичная задача – орагнизовать блочное кэширование веб-приложения. Имеется в виду кэширование HTML-фрагментов. Однако в процессе решения задачи всплыл один момент.
Задача
Необходимо сделать кэширование HTML фрагментов, называемых блоками. Блок генерируется “тяжело” с использованием SQL запросов через ORM слой. Это означает, что надо минимизировать количество обращений к БД для увеличения производительности. Кроме собственно кэширования встаёт задача инвалидации кэша после обновления оригинальных данных. Для того, чтобы связать процесс генерации данных с процессом инвалидации кэша чаше всего используются т.н. “теги”. Тег – ключ описывающий связь многие-ко-многим генерирующих процедур и сохранённых блоков в кэше.
Проблема
Главная проблема в том, что используемые хранилища для кэша в оперативнйо памяти (т.е. самые быстрые) имеют чрезвычайно простое API, это хранилища типа ключ-значение. В качестве примера приведу Memcached, Apc. Именно последний бекэнд использовался в текущем проекте. Если мы заглянем в исходники функции clean() в Zend_Cache_Backend_Apc/Memcached, то увидим там вот что:
1 2 3 4 5 6 7 8 9 10 |
public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) { ... case Zend_Cache::CLEANING_MODE_MATCHING_TAG: case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG: case Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG: $this->_log(self::TAGS_UNSUPPORTED_BY_CLEAN_OF_APC_BACKEND); break; ... } |
А это значит, что нужно самому придумывать очистку кэша по тегам.
Возможные решения
1. Хранить связи в том же кэше. Основаня идея: ранение всех ключей кеширования для конкретного тега в массиве — в самом кеше, с использованием тега как ключа кеширования для этого массива. Подробнее.
2. Использовать модификация для тегирования memcached-tags и класс Dklab_Cache.
3. Использовать версионность для тегов. Подробнее вот тут.
4. Хранение связей тегов и ключей в другом (но тоже быстром хранилище), например в MongoDB. Вкратце — при каждом добавлении новой записи в кеш составные части ключа записываются в монго, давая тем самым возможность полностью воссоздать ключ кеша, зная только его часть. Пример вот тут.
Вариант 1 и 3 мне не понравился своей трудоёмкостью, вариант 2 тем, что надо ставить доп.софт и ещё скорее всего настраивать и тюнить его. Вариант 4… хм, вот тут я призадумался. А почему бы просто не хранить блочный кэш в Mongo. Т.е. не только теги, но и сам кэш рядышком.
Свой вариант
Итак, решение на мой взгляд очень неплохое. Будем хранить блочный кэш в Монго вместе с тегами. Индексы обеспечат быстрый поиск, хранение в памяти – производительность, и в качестве бонуса – кэш может шариться между app-инстансами. Сначала я решил сам писать бекэнд кэша, но потом нашел замечательную штуку Zend_Cache_Backend_Mongo. А вот и мануал по её использованию. После небольшой доработки напильником бекэнд встал, как влитой (люблю за это ZF).
Бенчмарки
Предлагаю вам взглянуть на бенчмарки от автора решения.
Memcache
1 2 3 4 |
inserted 1000 items in 201ms (items/second: 4986) loaded 50000 random items in 5455ms (items/second: 9165) 1000 cache miss requests 109ms (items/second: 9153) Memory peak usage: 6029312 Byte (5.75 MB) |
MongoDB
1 2 3 4 |
inserted 1000 items in 339ms (items/second: 2946) loaded 50000 random items in 5747ms (items/second: 8700) 1000 cache miss requests 115ms (items/second: 8660) Memory peak usage: 6029312 Byte (5.75 MB) |
Монго чуть медленее на записи, но по скорости аналогично memcached при чтении. Думаю теги того стоят. Теперь можно не использовать TwoLevel cache, и юзать на всю катушку все функции MongoDB по работе с документами. Имхо, это лучшее решение.
Ссылки
http://www.smira.ru/2008/10/29/web-caching-memcached-5/
http://habrahabr.ru/post/57142/
http://blog.stoeckl.de/post/14618646779/my-mongodb-cache-backend-adapter-for-zend-framework-is
Приличные показатели. Проверьте, происходят ли вставки в режиме SaveAndFire (без ожидания результата операции) и, если приемлемо, используйте bulkInserts (пример вставки 10-20 к записей в секунду тут http://lenta.iadlab.ru/2012/01/23/mongodb-bulkbatch-insert/ )
Спасибо за ценное замечание. Обязательно поюзаю!
Интересное решение!
Приму к сведению )))
Для минимизации обращений к БД достаточно кешировать результаты запросов с этой самой БД и чистить кеш при добавлении новых данных в БД. В качестве хранилища кеша советую обратить внимание на Redis в замен скудному функционалу Memcached. В котором более богатый функционал для выборок и поддержка большего количества типов хранилищ, не только ключ – значение.
Ага, их есть у меня. Doctrine 2 results cache. Для первой Доктрины даже был класс для автоматической инвалидации. Как кэша результатов, так и кэша SQL->DQL пробразований.
Doctrine2 results cache, конечно, прикольный, но я так и не понял, как его заставить работать непосредственно с сущностями. Ну когда бывает что-то типа: $blog->getPosts()
Эм, ну никак. Это же Results cache. Он кэширует сырой ответ от БД. А для кэширования сущностей можно юзать например Zend_Cache (Manager+Core+Backend+Frontend).
>> Это же Results cache
И что? В BasicEntityPersister метод load содержит такую строку:
$stmt = $this->_conn->executeQuery($sql, $params, $types);
При этом executeQuery принимает 4 параметра и четвертый, как раз – QueryCacheProfile, регулирует использовать cache или нет.
В общем, копаясь сам ответил на свой вопрос – нужно переопределять BasicEntityPersister::load
Отлично! Не знал об этом.
Ха, у них еще и персистеры захардкодены в UnitOfWork
http://www.doctrine-project.org/jira/browse/DDC-391
Ну, намудрили