Иногда возникает необходимость скрыть графическую информацию от ботов при парсинге страницы.
Был найден простой но интересный способ на страницах lifehacker.ru.
Как известно, двумя основными проблемами программирования на данный момент являются именование и инвалидация кэша. И если первая проблема до сих пор отравляет жизнь мне и моим коллегам на нашем проекте 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. Если кто-нибудь решил проблему именования, пожалуйста, сообщите нам. Спасибо.
$ irb
1.9.3-p0-perf :001 > puts a if a = 4
(irb):1: warning: found = in conditional, should be ==
NameError: undefined local variable or method `a' for main:Object
from (irb):1
from /home/saks/.rvm/rubies/ruby-1.9.3-p0-perf/bin/irb:16:in `<main>'
Выглядит это так, как будто выражение под if-ом, выполняется не в той же области видимости, в которой выполняется проверка условия.
Еще одна неожиданная упячка в ruby:
vitaly@way:~/projects$ irb
1.9.3p0 :001 > a
NameError: undefined local variable or method `a' for main:Object
from (irb):1
from /home/vitaly/.rvm/rubies/ruby-1.9.3-p0-perf/bin/irb:16:in `<main>'
1.9.3p0 :002 > a = a
=> nil
1.9.3p0 :003 > a
=> nil
1.9.3p0 :004 > a = b
NameError: undefined local variable or method `b’ for main:Object
from (irb):4
from /home/vitaly/.rvm/rubies/ruby-1.9.3-p0-perf/bin/irb:16:in `<main>’
Wtf?
Здесь со временем будут появляться странности или неожиданности синтаксиса или просто то, что вызывает недоумение.
В один прекрасный момент приложение на нашем production-сервере (написанное на Ruby On Rails), начало докучать нам не страшными, но противными Exception’ами. Выглядело это так:
CollectionsController# (ActionView::MissingTemplate) “Missing template collections/show, application/show with {:handlers=>[:erb, :builder, :haml], :formats=>[\"*/*;q=0.9\"], :locale=>[:en, :en]}.
На начальном этапе разработки стартапа многие разработчики сталкиваются с проблемой создания достаточно гибкой и в будущем масштабируемой архитектуры приложения, оптимизированной для большой нагрузки. Мы хотели бы поделиться опытом в этой области. В качестве примера рассмотрим методы построения ресурса, базирующегося на Ruby On Rails. Тем не менее примеры, которые будут приведены ниже, с легкостью могут быть использованы для LAMP решений.
Серверы можно разделить на несколько типов по ролям:
- Application
- Database
- Load Balancer
- Utils
- Tools
Application – это front-end сервер. Возможно использование как одного, так и нескольких Application серверов. В случае использования одного Application сервера надобности в балансировке нагрузки нет.
- Application сервер выполняет следующие функции:
- обработка запросов от пользователей (получает запросы от балансировщика нагрузки),
- взаимодействие с базой данных (Database server),
- отправка сообщений в очередь сообщений (Utils server),
- взаимодействие с CDN,
- взаимодействие с search engines (Util server),
- инвалидация кэша,
- работа с кэшем (Database server).