Pull to refresh

Comments 47

Например кеширование данных для каждого пользователя скорее всего будет неэффективно при большом количестве пользователей. Если кешировать данные для всех пользователей разом, то возникнут проблемы с устареванием данных и когерентностью кеша.


Еще с правами доступа можно получить массу удовольствия. Если кэш общий для всех — пользователь может получить «закрытые» для него данные. Но даже если кэш отдельный для каждого пользователя, то можно нарваться, например, на получение данных из кэша, к которым доступ был закрыт позже (если, например, права доступа администрируются отдельно, и на «актуальность» собственно данных влияния не оказывают).
Статья получилась для тех, кто все эти схемы попробовал и знает их плюсы и минусы. Чтобы охватить более широкий круг пользователей, хорошо бы добавить простые примеры реализации с описанием получающихся граблей.
Опять же, наверное стоило упомянуть о проблеме конкурентной генерации кэша…
К сожалению простой пример синхронизированного кеша по объему равен всей статье. Позже напишу про кеширование в asp.net mvc и sharepoint.
Спасибо за интересную статью!
Как мне кажется, для начинающих хорошее подспорье.

Кстати, если уж говорить о кеше, то можно упомянуть, что не только в сетевых технологиях он акутален. Для тех же игр кеширование текстур — нормальное явление, для программ со сложным и навороченным UI — тоже. Если программа занимается обработкой большого количества данных, хранящихся в файловой системе (ну допустим около терабайта), без кеша опять же не обойтись.
адский псевдокод:

get_something
    if cached
        return cached
    else
        cached = ...
        return cached

set_something:
    ...
    drop cached


Такая логика, по-моему, работает в 95+ процентах случаев, когда чтений данных больше, чем изменений.
Когда не так — уже приходится думать.

А что сложного в кешах для каждого юзера? Почему не сделать для его закешированного профиля ключ в кеше, включающий его идентификатор?
но не работает в случае если данные были изменены 3й стороной
Ок. Мне всегда сопутствовала роскошь работать на проектах, где с данными общалась только одна codebase. Как бы это по-русски? :)
Одна codebase еще не означает автоматической синхронизации кешей между разными серверами.
Так для третей стороны есть API.
Нечего ей прямой доступ к данным давать.
мы говорим о разных вещах.
представьте, у нас горизонтальный кластер из 10 серверов приложений. пользователь имеет сессию на одном из серверов и соответственно на этом же сервере у него есть кеш.
3й стороной в данном случае может быть даже сам пользователь, открывший сессию с другим сервером (например с другого компьютера). я уже молчу про администратора и апи.
Ну, кеш на сервере с одной стороны это хорошо, близко.

С другой стороны, разумно сделать кластер memcached-ов на этих 10 серверах и не париться.

Если становятся важны единицы миллисекунд — то ок, можно перенести кеши на каждый сервер в отдельности; но тогда надо придумывать способы, чтобы пользователь всегда приходил на один сервер.
Этот аццкий псевдокод реализует lazy кеш. Зачастую его даже писать не надо, он во многие фреймворки встроен. Но, по причинам описанным в посте, мало полезен.

А в кешах для каждого юзера сложности нет, но процент попадай в кеш получается низкий, когда юзеров много.
Так кеш генерится только тогда, когда он каким-то юзером запрошен. Нормальный будет процент)
Можно посчитать. Предположим 1000 пользователей, каждый держит в кеше по 100кб. Объем кеша 50МБ — то есть примерно 50% пользовательских кешей будет влезать. Если все 1000 пользователей запрашивают равновероятно, то матожидание попадания в кеш будет 50%. Это довольно низкий процент. При увеличении количества пользователей эффективность падает. При всплесках нагрузки кеш окажется почти бесполезным.
Решение — сделать 100 МБ. И следить за наполняемостью кеша, не стоит доводить выше, чем до 80% (в случае memcache).

Потом, 100кБ на пользователя — это очень много.
Ну сделаешь 100МБ, а потом придет 10,000 пользователей…

100кб может быть много для одного запроса, но для нескольких — очень даже мало.
Если у вас одновременно работают десять тысяч пользователей, то, наверно, популярность вашего приложения позволит вам и (целый!) гигабайт выделить на кеш.
Насчет «устаревших данных» и «затрат на реализацию» я категорически не согласен — таких проблем у приведенного псевдокода не будет, и написать его, ей-богу, не трудно. Даже когда он не «псевдо».
Какой-то expire добавить, конечно, надо — но это на случай, если кеш перестал быть кому-то нужен, а не чтобы он по нему обновлялся.
По псевдокоду:
про «адский» увидел, но не смог удержаться, простите.

def get_something(id):
    if not self.__cached:
        self.__cached = self._get_something()
    return self.__cached


def set_something(id):
    self.__cached = None
    ....


Тут хорошо подходит идеология обращения через property

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

При изменениях, id записей накапливаются.
Есть один или несколько (нужен механизм распределения записей по обработчикам) обработчиков.
На cron вешать эти обработчики, чтоб они пачками обновляли кэш не по каждому объекту, а по списку накопившихся.

Конкретика:
Использовал в поддержании актуальности (задержки 10 минут) таблицы по сущности, которая собирается из множества других таблиц в oracle,
когда запрос с join-ами этих таблиц для web-интерфейса отчетности не давал приемлимых результатов.
На все эти таблицы вешались триггеры, которые вычисляли и записывали в таблицу-лог id объекта и время изменения, то есть, накапливали объекты, по которым кэш устарел
Была процедура, которая запускалась раз в 10 минут.
— Сделать view, в которой используются таблицы, по набору колонок равную таблице
— Запомнить время старта
— Выбрать все объекты, которые имеют запись в логе до времени старта
— Накатить все данные из view, которые есть в логе (merge into table using(select * from view where id in (select id from log))… )
— Удалить записи из лога, которые до времени старта (чтобы удалить только гарантированно отработанные, так как при обновлении, успевают налететь еще данные)

На выходе получали таблицу с 50-60 столбцами (все нужные параметры сущности), по которой очень удобно и быстро строить отчеты почти online.
Время инкрементного обновления кэша 40 сек. Если выбирать все записи из представления, то 3 часа.
Ну да, такой get, конечно, лучше :)
Почему не будет? Как приложение узнает что cached пора обновить? Иначе получится что cached заполнится один раз на все время жизни приложения и не будет меняться. Такого, конечно, не бывает.

«Какой-то expire добавить» выливается в объем кода, сравнимый с объемом кода логики приложения.
Ну обратите внимание на set_something — когда мы закешированную сущность меняем в базе, сбрасываем её кеш.

В случае memcached «добавить expire» выливается в добавление ", $expire".

Если в вашей системе кеширования нет встроенного expire, то я надеюсь, что там есть хотя бы вытеснение старых данных новыми — сойдет и это.
Одна и та же сущность может быть закеширована много раз, например в персональных кешах пользователей или просто для разных представлений. Как выяснить какие кеши нужно экспайрить?
Надо немного конкретизировать, чтобы мы оставались в общем контексте.
Я так понимаю, вы имеете в виду ситуацию, когда, например, у нас есть некоторая сводная страница, где надо показать не одну сущность — пользователь, сайт, etc — а некую их группу или группы. Вероятно, в каком-то определенном порядке.
При этом информация о каждом отдельном элементе каждой группы в кеше есть.

Как бы я поступал в таком случае.

Во-первых, надо определиться, допустимы ли тут устаревшие данные и если да, то насколько.

Для начала, пусть допустимы.

Тогда я бы получал список групп для показа, в нужном порядке, и кешировал бы эти группы — в виде id сущностей. На какое-то время, определяемое задачей. Скажем, 5 минут.
Потом на каждый запрос я бы обращался к этому кешу, собирал все id, и забирал отдельный кеш каждой сущности.
В memcached есть get_multi, кстати — экономия на накладных расходах существенная. Да и запросы в базу для cache miss тоже можно группировать, так что, конечно, групповые lookup-ы надо для каждой сущности реализовать.
Рендерил страницу, отдавал.

Теперь пусть неактуальность недопустима.

Снова развилка — как часто потенциально может обновляться информация на сводной странице?

Скажем, редко. Это маловероятная ситуация, сейчас станет понятно, почему.
Тогда можно дропать сводный кеш каждый раз, когда происходит что-то, что может повлиять на сводную страницу — то есть на каждый апдейт каждой сущности, которая теоретически может там присутствовать.
Скорее всего, так ключ будет дропаться чуть ли не чаще, чем запрашиваться.

Поэтому переходим к случаю часто обновляемой сводной страницы, которая всегда должна быть актуальна.
А тут рецепт прост — учиться получать список id сущностей, которые вы будете показывать — быстро.

Либо это грамотно огранизованная таблица/таблицы в РСУБД с правильными индексами, либо это, скажем, Redis или что-то ещё, работающее в памяти.

В итоге мы все равно получим список id сущностей, которые будут лукапиться вторым шагом.

В общем, весь этот очень длинный текст сводится к тому, что не надо дважды одну информацию класть в два места, если вам важна когерентность этих мест. Просто вот не надо и всё. Придумывайте по-другому.
Сводную страницу из разных данных можно разбить на несколько блоков, каждый из которых кешируется и обновляется отдельно. И собирать это всё при отдаче клиенту.
Давно придумали тэгирование. И очистку кеша по тэгу.
Это скорее ответ не Вам, а gandjustas.

Пример, кеширование запросов в БД через ORM. В большей части ORM есть модели, к модели можно привязать определенные префикс. Модель выполняет разнообразные запросы с различными параметрами и все отправляет в кэш со своим префиксом.

Когда наступает момент изменения данных в БД, касающихся определенной модели, мы точно не знаем, какие именно кеши эти данные затронули и сбрасываем кеши объединенные префиксом или тэгом. Многие системы позволяют искать по префиксу (главное, чтобы он был уникальный и не затрагивал лишних данных).

Но, к примеру Memcached не позволяет, поэтому я лично знаю костыль, когда дополнительно используется noSQL решение в добавок Memcached для хранения ключей по тегам.

Реализацию с БД привел только как абстрактный пример.
Я такое делал когда еще Velocity Cache не стал Windows Server App Fabric. Там это довольно легко сделать через паттерн publish\subscribe, ну и кеш когерентный получается. Но изобретать такое с нуля — проблематично.
То есть, например, когда в таблице пользователей появляется новое поле, надо все старые кеши инвалидировать?
Ну или просто по какой-то причине мы знаем, что у нас половина кеша сломана?

Искать по префиксу в мемкеше вроде и не надо, просто меняем префикс в коде и оп, все пошли за свежим кешем.

А если вот так разом сбрасывать опасно — можно написать скриптик, который будет проходить по базе и дропать кеши постепенно, опционально — вызывать ф-цию, которая проставит правильный кеш.

Вообще ключи я привык формировать так: NAMESPACE-PREFIX-id, где NAMESPACE отделяет друг от друга виды сущностей, а PREFIX отдельные кеши внутри сущности.

А именно про теги в мемкеше я нагуглил, но у меня сразу возникли вопросы «а как это работает, когда у меня ключи на десяти серверах» и «а где они хранят информацию о тегах, ведь memcached не персистентное хранилище и может выкинуть из кеша что угодно когда угодно».
Тут уже встает вопрос насколько такой кэш подходит Вашему приложению. Если select/total > 90%, я считаю эту стратегию верной и самой простой.

Поясните, чем плох дроп кеша, даже массовый (в пределах тега)?

Менять префиксы не самый лучший способ. Их необходимо где-то хранить. Также некоторые системы при перезаполнении пула, могут просто отказаться работать (старые версии APC, к примеру).

Другой вариант, это бэкграундое перестроение кеша. Когда Вы заранее знаете, что Вы храните и перестраиваете по затронутым тэгам, а пока кеш не перестроен, отдаете старый контент.
Массовый дроп может быть плох, если у Вас после этого приложение ломанется в базу так активно, что ляжет :)
Речь только о массовом, теги так вряд ли используются, с ними должно быть ок.

Менять префиксы не самый лучший способ.

Ну, я все это рассказываю в применении к memcached. Там старые данные рано или поздно уплывут, риска переполнения кеша вообще нет.
Synchronized cache, синхронизированный кеш – клиент вместе с данными получается метку последнего изменения и может спросить у поставщика не изменились ли данные, чтобы повторно из не запрашивать. Такой тип кеширования позволяет всегда иметь свежие данные, но очень сложен в реализации.

Не понимаю. Мы про бекенд говорим или про фронтенд?
Если про бекенд, то не вижу никакой проблемы в вводе CacheDependency.
Если про фронтенд, то опять же нет никакой проблемы — опрашиваем сервер по таймеру или через постоянное соединение отправляем диффы/обьекты клиенту.
CacheDependency можно повесить на произвольную СУБД\веб-сервис? CacheDependency не везде существует.
Если говорим про фронтэнд на HTTP, то там уже все есть. А если нет, то реализация очень нетривиальная. Мне, к сожалению, пришлось прикручивать такой кеш к SOAP вебсервису. Код кеширования (клиент+сервер) в итоге оказался больше, чем код всего веб-сервиса.
Есть запрос к источнику данных, есть зависимость, которая определяет актуальность этих данных.
CacheDependency в моем понимании может быть чем угодно и реализуется программистом.
Например:
Storage.cache(dependency).get(criteria)

Если dependency не обновился с момента последнего сохранения результата в кеш, то кеш и возвращаем.
Может вы про случай, когда зависимость нетривиальна и определение актуальности информации действительно сложная задача? Приведите утрированный пример, пожалуйста.
У нас на проекте используется синхронизированный кеш с инвалидацией по тегам. Запросы (чтение) и команды (запись) имеют теги. При исполнении команды происходит сброс всех ключей с указанными тегами. Основная сложность этого подхода состоит в правильном выборе тегов, чтобы они были максимально точными и не сбрасывали лишние ключи.

Write-through cache похож на подход CQRS, когда при обновлении нормализованного хранилища обновляется так же хранилище для чтения (проекции). Это весьма сложно в реализации, плюс есть шанс прочитать устарвешие данные, т.к. денормализация занимает какое-то время и происходит в фоне.
На чем реализован такой кэш? Я имею в виду тэги.
В качестве кеш-сервера используется memcached (Couchbase). Сам по себе мемкеш не поддерживает теги ключей и сброс по ним, потому пришлось использовать одно известное решение:

— теги (строки) сами являются ключами, а значениями являются даты их последнего сброса
— вместе с ключами кеша мы храним дату их записи
— при получении ключа мы проверяем, что дата его записи больше, чем дата сброса любого из его тегов
— если какой-то тег был сброшен позже, чем мы записали ключ в кеш, такой ключ считаем протухшим.

Минус данного подхода в том, что при каждом запросе ключа требуется также запросить даты сброса всех его тегов (можно использовать мульти-гет). Плюс — в актуальности кеша и удобстве управления инвалидацией ключей.
А если memcached решить выкинуть слаб с частью тегов?
memcached, когда ему начинает не хватать места, начинает выкидывать данные, причем выкидывает он их целыми кусками, блоками.

То есть любой ключ может пропасть раньше времени наступления expire-а.

Одна из нод кластера может ребутнуться, наконец.
Интересное замечение. В таком случае при отсутствии тега можно считать, что он инвалидирован. Еще можно дублировать теги в памяти самого приложения (они ничего не весят). Но, поскольку теги довольно часто обновляются и занимают очень малый объем памяти, вероятность того, что мемкеш их выкинет, довольно мала.
UFO just landed and posted this here
Чтобы избежать проблем с кэшем, нужно его использовать в первую очередь с данными на чтение…

Идея кэшировать редактируемые данные, компрометирует слой (делает его лишним и это обычно БД), отвечающий за хранение данных. Тогда уж лучше отказаться совсем от БД.
Sign up to leave a comment.

Articles