Tips & Tricks: PHP, Rails, Nginx, Apache, Passanger, MySQL, PgSQL, RabbitMQ, Memcached, …

Как известно, двумя основными проблемами программирования на данный момент являются именование и инвалидация кэша. И если первая проблема до сих пор отравляет жизнь мне и моим коллегам на нашем проекте sophia.org, то для решения второй мы кое-что предприняли.

Sophia.org – проект, написанный на ruby с использованием фреймворка Rails. Для организации backend-кэширования на sophia.org мы используем Memcached. Memcached – прекрасное решение разряда key-value, обеспечивающее быстрый доступ к данным и превосходную кластеризацию “из коробки”. Вместе с тем одной из его проблем является массовая инвалидация.

В Rails для организации доступа к cache имеется объект Rails.cache (являющийся сам по себе экземпляром класса ActiveSupport::Cache::Store). Он работает поверх любого cache storage. Наше решение действует на уровне Rails.cache, следовательно, будет работать не только для MemCacheStore, но и для других кэш-хранилищ, которые могут быть использованы с Rails.

Итак, для обеспечения массовой инвалидации мы использовали механизм тэгирования. Идея заключается в инвалидации не самих записей, а некоей метаинформации прикрепленной к ним – т.н. тэгов. По сути, мы обеспечили возможность протэгировать любую запись, сохраненную в memcached, набором тэгов:

Rails.cache.write('key-name', 'value-string', :tags => %w(user1 group2))

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

{key: 'key-name', value: 'value-string', tags: [{name: user1, version: 1}, {name: group2, version: 1}]}

Кроме того, были созданы записи:

{key: 'tag-user1',  value: 1}
{key: 'tag-group2', value: 1}

Теперь, когда мы будем доставать из кэша эту запись, мы будем извлекать из нее версии тэгов и проверять их актуальность. В случае если сохраненная версия тэга в записи и актуальная версия тэга не равны, – мы признаем запись устаревшей. Т.е. когда нам понадобится инвалидировать запись с ключом ‘key-name’, мы будем не удалять ее из кэша – мы просто увеличим версию тэга на 1.

{key: 'tag-group2', value: 2}

Все, запись ‘key-name’ уже устарела, т.к. запись ссылается на версию 1 тэга group2.

На практике в нашем проекте мы часто кэшируем фрагменты views. Эти фрагменты зависят от текущего пользователя, смотрящего на них, самого объекта, его версии, каких-то сторонних объектов. Следовательно, если мы изменяем объект – возможно, мы изменяем и право какого-то пользователя посмотреть на фрагмент. Чтобы не перечислять все названия фрагментов кэша, мы инкрементируем версию тэга, связанного с этим объектом, инвалидируя таким образом все записи, связанные с ним.

Конечно, более стандартным подходом, используемым для этих целей, является хранение значения updated_at объекта в имени ключа (это происходит в стандартном методе ActiveRecord::Base#cache_key) . Но этот подход лично для нас не работает, т.к. зачастую изменяется не сам объект, а какие-то записи, связанные с ним по ассоциациям, из-за чего какой-нибудь счетчик на странице принимает другое значение. Согласитесь, изменять timestamp объекта из-за появившейся связи в таблице связи с другим объектом не очень культурно.

Но и здесь, конечно, есть обратные стороны. Мы получили увеличившуюся нагрузку на Memcached. Теперь ему надо сделать два запроса при чтении записи вместо одного: на саму запись и на все связанные с ней тэги (этот запрос реализован через read_multi). Впрочем, с подобным ростом нагрузки он у нас отлично справляется. Также нужно скептически относиться к отказоустойчивости решения и понимать, что если запись тэга вылетит из кэша из-за редкого использования, это автоматически инвалидирует все помеченные этим тэгом записи (хотя, вероятно, эти записи должны были вылететь из кэша еще раньше).

Описанное выше решение выложено на github, и с ним можно ознакомиться и начать использовать.

P.S. Если кто-нибудь решил проблему именования, пожалуйста, сообщите нам. Спасибо.