Инвалидация кеша: Инвалидация кеша — Symfony Framework Documentation documentation

О проблемах инвалидации кэша. Тегирование кэша. — @emacsway’s blog

О моем опыте решения проблем инвалидации кэша, и о принципах работы библиотеки cache-dependencies.

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

Обычно за инвалидацию кэша отвечает паттерн Observer, или его разновидность — паттерн Multicast. Но даже в этом случае механизмы инвалидации получаются слишком сложными, достигаемая точность слишком низкая, a код обрастает лишним сопряжением и зачастую жертвует своей инкапсуляцией.

И тут на выручку приходит тегирование кэша, т.е. прошивание кэша метками. Например, кэш главной страницы может быть прошит тегом product.id:635. А все посты пользователя могут быть прошиты меткой

user.id:10. Коллекции можно кэшировать составным тегом, состоящим из критериев выборки, например type.id:1;category.id:15;region.id:239.

Теперь достаточно инвалидировать метку, чтобы все зависимые кэши автоматически инвалидировались. Эта технология не нова, и активно используется в других языках программирования. Одно время ее даже пытались внедрить в memcached, см. memcached-tag.

Также смотрите:

Возникает вопрос реализации инвалидации зависимых от метки кэшей. Возможны два варианта:

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

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

Я выбрал второй вариант.

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

Поэтому система кэширования должна автоматически отслеживать связи между вложенными кэшами, и передавать метки от дочернего кэша к родительскому.

При инвалидации кэша параллельный поток может успеть воссоздать кэш с устаревшими данными, прочитанными из slave в перид времени после инвалидации кэша, но до момента обновления slave.

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

В своей практике мне приходилось встречать такой подход как регенерация кэша вместо его удаления/инвалидации. Такой подход влечет за собой не совсем эффективное использование памяти кэша (работающего по LRU принципу). К тому же, он не решает проблему сложности инвалидации, и, в данном вопросе, мало чем отличается от обычного удаления кэша по его ключу, возлагая всю сложность на само приложение. Также он таит множество потенциальных баг. Например, он чувствителен к качеству ORM, и если ORM не приводит все атрибуты инстанции модели к нужному типу при сохранении, то в кэш записываются неверные типы данных. Мне приходилось видеть случай, когда атрибут даты записывался к кэш в формате строки, в таком же виде, в каком он пришел от клиента. Хотя он и записывался в БД корректно, но модель не делала приведение типов без дополнительных манипуляций при сохранении (семантическое сопряжение).

Updated on Nov 10, 2016

Добавлено описание реализации блокировки меток.

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

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

Почему не была использована пессимистическая блокировка меток (Pessimistic Offline Lock), или Mutual Exclusion? Вопрос резонный, ведь закэшированная логика может быть достаточно ресурсоемкой. При такой реализации параллельные потоки ожидали бы освобождения заблокированной метки.

Библиотека предназначена, прежде всего, для управления инвалидацией кэша.

Предположим, поток П1 начал транзакцию с уровнем изоляции Repeatable read.

Следом за ним, поток П2 начал транзакцию, изменил данные в БД, и вызвал инвалидацию метки М1, что наложило блокировку на метку М1 до момента фиксации транзакции.

Поток П1 пытается прочитать кэш К1, который прошит меткой М1, и является невалидным. Не сумев прочитать невалидный кэш К1, поток П1 получает данные из БД, которые уже утратили актуальность (напомню, уровень изоляции — Repeatable read). Затем он пытается создать кэш К1, и встает в ожидание, так как на метку К1 наложена пессимистическая блокировка.

Во время фиксации транзакции, поток П2 освобождает метку М1. Затем поток П1 записывает в кэш устаревшие данные. Смысла от такой блокировки нет.

Но что если мы будем проверять статус метки не во время создания кэша, а во время чтения кэша? Изменило бы это хоть что-то?

Изменило бы. Во-первых, добавило бы оверхед на логику чтения. Во-вторых, изменило бы результат, если бы уровень изоляции транзакции не превышал Read committed. Для уровня изоляции Repeatable read (который выбран по умолчанию для ряда БД, и является минимально необходимым для корректной работы паттерна Identity Map) и выше, — ничего не изменило бы. Для этого пришлось бы блокировать поток еще до начала транзакции.

Таким образом, данное решение было бы половинчатым, не универсальным, и содержало бы неконтролируемую зависимость. Для 2-х из 4-х уровней изоляции транзакций работать не будет.

Кроме конструктивного препятствия есть еще и другие.

Библиотека ориентирована главным образом на веб-приложения. Ожидание параллельных потоков до момента окончания транзакции, или до момента обновления slave, который в некоторых случаях может длиться 8 секунд и более, практически не реализуемо в веб-приложениях.

Основных причин здесь три:

  • Для веб-приложения важна быстрота отклика, так как клиент может просто не дождаться ответа.
  • Нет смысла ожидать создание кэша более, чем требуется времени на само создание кэша.
  • Рост количества ожидающих потоков может привести к перерасходу памяти, или доступных воркеров сервера, или исчерпанию максимально допустимого числа коннектов к БД или других ресурсов.

Также возникла бы проблема с реализацией, поскольку корректно заблокировать все метки одним запросом невозможно.

  • Во-первых, для блокировки метки нужно использовать метод cache.add() вместо cache.set_many(), чтобы гарантировать атомарность проверки существования и создания кэша.
  • Во-вторых, каждую метку нужно блокировать отдельным запросом, что увеличило бы накладные расходы.
  • В-третьих, поодиночное блокирование чревато взаимной блокировкой (Deadlock), вероятность которой можно заметно сократить с помощью топологической сортировки.

Отдельно стоит упомянуть возможность блокировки строк в БД при использовании выражения SELECT FOR UPDATE. Но это будет работать только в том случае, если обе транзакции используют выражение SELECT FOR UPDATE, в противном случае:

When a transaction uses this isolation level, a SELECT query (without a FOR UPDATE/SHARE clause) sees only data committed before the query began; it never sees either uncommitted data or changes committed during query execution by concurrent transactions. In effect, a SELECT query sees a snapshot of the database as of the instant the query begins to run.

Однако, выборку для модификации не имеет смысла кэшировать (да и вообще, в веб-приложениях ее мало кто использует, так как этот вопрос перекрывается уже вопросом организации бизнес-транзакций), соответственно, ее блокировка мало чем может быть полезна в этом вопросе. К тому же она не решает проблему репликации.

Но что делать, если закэшированная логика действительно очень ресурсоемка?

Dogpile известен также как Thundering Herd effect или cache stampede.

Ответ прост, — пессимистическая блокировка. Только не меток кэша, а ключа кэша (или группы связанных ключей, см. Coarse-Grained Lock, особенно при использовании агрегирования запросов). Потому что при освобождении блокировки кэш должен быть гарантированно создан (а кэш и метки связаны отношением many to many).

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

Существует ряд решений для реализации такой блокировки, вот только некоторые из них:

Если Ваш проект имеет более-менее нормальную посещаемость, то с момента инвалидации кэша и до момента фиксации транзакции, параллельный поток может успеть воссоздать кэш с устаревшими данными. В отличии от проблемы репликации, здесь проявление проблемы сильно зависит от качества ORM, и вероятность проблемы снижается при использовании паттерна Unit of Work.

Рассмотрим проблему для каждого уровня изоляции транзакции по отдельности.

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

Тут уже проблема может присутствовать, особенно если Вы используете ActiveRecord. Использование паттерна DataMapper в сочетании с Unit of Work заметно снижает интервал времени между сохранением данных и фиксацией транзакции, но вероятность проблемы все равно остается.

В отличии от проблемы репликации, здесь предпочтительней было бы блокирование создания кэша до момента фиксации транзакции, так как текущий поток видит в БД не те данные, которые видят параллельные потоки. А поскольку нельзя гарантированно сказать какой именно поток, текущий или параллельный, создаст новый кэш, то создание кэша до фиксации транзакции было бы желательно избежать.

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

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

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

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

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

Поскольку несуществующие объекты обычно не кэшируются, то здесь достаточно ограничится той же проблематикой, что и для уровня Repeatable read.

Если Вы используете разные БД, и их транзакции синхронны, или просто используется репликация, Вы можете использовать по одному экземляру внешнего кэша (враппера) для каждого экземпляра внутреннего кэша (бэкенда). Транзакции кэша не обязаны строго соответствовать системным транзакциям каждой БД. Достаточно того, чтобы они выполняли свое предназначение, — не допускать подмену данных посредством кэша в параллельных потоках. Поэтому, они могут охватывать несколько системных транзакций БД.

Но если Вы используете несколько соединений к одной и той же БД (что само по себе странно, но теоретически могут быть случаи когда нет возможности расшарить коннект для нескольких ORM в едином проекте), или же просто транзакции различных БД не синхронны, то Вы можете сконфигурировать внешний кэш так, чтобы иметь по одному экземпляру внешнего кэша на каждое соединение с БД для каждого экземпляра внутреннего кэша.

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

Один из вариантов решения этой проблемы — это использование технологии Server Side Includes.

Другой вариант — это использование двухфазной шаблонизации, например, используя библиотеку django-phased. Справедливости ради нужно отметить, что решение имеет немаленькое ресурсопотребление, и в некоторых случаях может нивелировать (если не усугублять) эффект от кэширования. Возможно, именно поэтому, оно не получило широкого распространения.

Популярный шаблонный движок Smarty на PHP имеет функцию {nocache}.

Но более интересной мне показалась возможность использовать в качестве динамического окна обычный Python-код, и абстрагироваться от сторонних технологий.

Updated on Nov 06, 2016

Добавлен абстрактный менеджер зависимостей.

Долгое время мне не нравилось то, что о логике, ответственной за обработку тегов, были осведомлены сразу несколько различных классов с различными обязанностями.

Было желание инкапсулировать эту обязанность в отдельном классе-стратегии, как это сделано, например, в TagDependency of YII framework, но не хотелось ради этого увеличивать накладные расходы в виде дополнительного запроса на каждый ключ кэша для сверки его меток, что означало бы лишение метода cache.get_many() своего смысла — агрегирования запросов. По моему мнению, накладные расходы не должны превышать одного запроса в совокупности на каждое действие, даже если это действие агрегированное, такое как cache.get_many().

Кроме того, у меня там был еще один метод со спутанными обязанностями для обеспечения возможности агрегации запросов в хранилище, что большого восторга не вызывало.

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

Решение появилось с введение класса Deferred. Собственно это не Deferred в чистом виде, в каком его привыкли видеть в асинхронном программировании, иначе я просто использовал бы эту элегантную и легковесную библиотечку, любезно предоставленную ребятами из Canonical.

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

Возможно, название Queue или Aggregator здесь подошло бы лучше, но так как с точки зрения интерфейса мы всего лишь откладываем выполнение задачи, не вникая в детали ее реализации, то я предпочел оставить название Deferred.

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

Это открывает перспективы создания других вариантов реализаций управления зависимостями, например, на основе наблюдения за изменением какого-либо файла, или SQL-запроса, или какого-то системного события.

Трудности и стратегии кэширования

Взлеты и падения технологии кеширования

За годы разработки продуктов в Amazon мы снова и снова разыгрывали разные варианты одного и того же сценария: команда создает новый сервис, и он должен совершать сетевые вызовы для удовлетворения своих запросов. Эти вызовы могут предназначаться реляционной базе данных или сервису AWS, например Amazon DynamoDB, либо другому внутреннему сервису. Во время простых тестов или при низком количестве запросов сервис работает превосходно. Проблемы начинаются позже. Причины могут быть разными: скорость вызова к другому сервису очень низкая или масштабировать базу данных под возрастающие запросы слишком дорого. Мы также заметили, что многие вызовы используют одинаковые подчиненные ресурсы или результаты выполнения. Поэтому нам и пришла в голову идея кэшировать такие данные, чтобы избежать проблем в будущем. После того как мы добавили кэш в свои сервисы, их продуктивность значительно возросла. В частности, задержка вызовов уменьшилась, расходы снизились, а доступность малых подчиненных ресурсов улучшилась. Теперь уже сложно и вспомнить, как мы вообще справлялись без кэша. Зависимые структуры уменьшили размеры своих групп кэширования, а базы данных – свой масштаб. Казалось, что технология сервисов налажена и работает хорошо. Но раз за разом мы сталкивались с новыми проблемами. Схемы трафика могли измениться внезапно, группы кэша переставали работать, возникали многие другие непредвиденные трудности, в следствие которых кэш становился «холодным», попросту говоря, плохо заполненным данными, или недоступным. Такие проблемы влекли за собой резкие всплески трафика в подчиненных сервисах, что приводило к перебоям в работе как зависимых структур, так и наших сервисов.

Мы только что привели пример типичного сервера, который на 100 % зависит от своего кэширования. Кэш случайно превратился из просто полезной функции сервиса в критически необходимую его часть, от которой зависит вся работоспособность. Вся загвоздка заключается в модальном поведении кэша, которое разнится в зависимости от того, кэшируется ли данный объект. Непредвиденные скачки в таком поведении могут потенциально привести к большим проблемам.

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

Когда нужно использовать кэширование

Когда вы принимаете решение о добавлении кэша в свою систему, нужно учитывать несколько факторов. Чаще всего сначала мы анализируем задержки зависимых структур или эффективность работы при текущей частоте запросов. Например, мы определяем, что зависимая структура начинает уходить в троттлинг (ограничивать нагрузку) или больше не способна поддерживать ожидаемый уровень нагрузки в следствие других факторов. Наша команда считает, что кэширование полезно, когда система сталкивается с неравномерными схемами запросов, которые приводят к ограничению нагрузки в «горячих» точках или секциях. Данные из этой зависимости действительно следует кэшировать, если после этого улучшится коэффициент попадания в кэш для всех запросов. Так результаты вызовов к зависимой структуре могут использоваться для множества запросов или операций. Если для каждого запроса обычно требуется уникальный вызов к зависимому сервису с уникальными результатами на один запрос, то кэш будет иметь низкий коэффициент попадания и станет бесполезным. Второй важный фактор – насколько хорошо команда сервиса и его клиенты могут адаптироваться к потенциальной непротиворечивости. Кэшируемые данные обязательно со временем будут противоречить источнику, поэтому кэширование может быть успешным только в том случае, если сервис и его клиенты будут компенсировать эти несоответствия. Скорость изменения исходных данных, а также правила кэширования для их обновления будут определять, насколько противоречивыми окажутся данные. Эти два пункта взаимосвязаны. Например, относительно статичные или медленно меняющиеся данные могут кэшироваться в течение более длительных периодов времени.

Локальные кэши

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

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

Другим недостатком является то, что нисходящая нагрузка теперь пропорциональна размеру парка сервиса, поэтому по мере увеличения количества серверов зависимые сервисы все еще могут быть перегружены. Мы обнаружили, что для эффективного мониторинга нужно генерировать метрики для попаданий/промахов кэша и количества запросов, направленных к нисходящим сервисам.

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

Внешний кэш

С помощью внешнего кэша можно решить многие из перечисленных выше проблем. Внешний кэш хранит кэшированные данные в отдельном парке с использованием, например, Memcached или Redis. Согласованность кэшированных данных улучшается, поскольку внешний кэш содержит значение, используемое всеми серверами в группах. (Учтите, что проблема не исчезает полностью, поскольку могут возникнуть ошибки при обновлении кэша.) Общая нагрузка на подчиненные сервисы сокращается в сравнении с кэшем в памяти, а также становится непропорциональной размерам групп. Проблемы с «холодным» запуском во время таких событий, как развертывания, отсутствуют, поскольку внешний кэш остается заполненным на протяжении всего процесса. Наконец, внешний кэш предоставляет больше места для хранения, чем кэш в памяти, уменьшая количество случаев вытеснения из-за ограничений пространства.

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

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

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

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

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

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

Сравнение линейного кэширования с кэшированием на стороне

Во время анализа различных подходов также необходимо рассмотреть линейное кэширование и кэширование на стороне. Линейное кэширование, т. е. кэширование с возможностью чтения и записи, позволяет встраивать функции управления кэшем в главный API доступа к данным. В качестве примера можно указать реализацию для определенных приложений Amazon DynamoDB Accelerator (DAX) или реализацию с использованием стандартов, такую как кэширование HTTP (с локальным клиентом кэширования или внешним сервером кэширования, таким как Nginx или Varnish). Кэширование на стороне – это хранилища обобщенных объектов, таких как в Amazon ElastiCache (Memcached и Redis), или библиотеки, как Ehcache и Google Guava для внутренней кэш-памяти. С помощью кэширования на стороне код приложения напрямую управляет кэшем после вызовов, сделанных к источнику данных, и перед ними. Он проверяет объекты кэширования перед тем, как выполнить выходящий вызов, и помещает объекты в кэш после того, как вызов завершится.

Основное преимущество линейного кэширования – единая модель API для клиентов. Кэширование можно добавить, удалить или настроить согласно собственным требованиям, не меняя логику клиента. При линейном кэшировании используется логика управления, присутствующая в коде приложения, что позволяет избежать многих потенциальных проблем. Преимущество кэширования HTTP заключается в наличии большого выбора стандартных опций, готовых к использованию. Например, библиотеки в памяти, отдельные прокси-серверы HTTP, описанные выше, и такие управляемые сервисы, как сети доставки контента (CDN).

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

Окончание срока действия кэша

Одной из самых трудных задач во время настройки кэширования является выбор размера кэша, определение политики окончания срока действия и вытеснения данных. Политика окончания срока действия определяет продолжительность размещения объекта в кэше. Наиболее распространенное правило использует абсолютное значение времени для определения этого периода, т. е. оно устанавливает время жизни (TTL) объекта при его загрузке. Значение TTL устанавливается согласно запросу клиента. Например, можно регулировать правила для устаревших и статических данных, поскольку кэширование медленно меняющихся данных требует больших ресурсов. Чтобы выбрать идеальный размер кэша, необходимо знать ожидаемое количество запросов и понимать механизм распределения сохраненных в кэше объектов с помощью этих запросов. На основе этих параметров мы рассчитываем размер кэша, чтобы обеспечить высокую частоту попаданий в кэш с учетом этой схемы передачи данных. Политика вытеснения данных контролирует процесс удаления объектов из кэша, когда он переполняется. Чаще всего к данным применяется правило «дольше всего не используется» (LRU).

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

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

Чтобы улучшить отказоустойчивость, когда целевые сервисы недоступны, мы применяем два значения TTL: «мягкое» и «жесткое». «Мягкое» значение применяется, если клиент пытается обновить объекты, сохраненные в кэше, а целевой сервис недоступен или не отвечает на запросы. В этом случае данные, которые уже находятся в кэше, будут использоваться, пока не будет достигнуто «жесткое» значение TTL. Данный механизм применяется в работе клиента AWS Identity and Access Management (IAM).

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

Другие факторы

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

Важный аспект кэширования – безопасность. Когда мы настраиваем кэширование для сервиса, мы учитываем все возможные риски и предпринимаем меры по их предотвращению. Например, внешние серверы кэширования не используют кодирование для сериализованных данных и средства защиты для передачи данных. Это важно учитывать, если в кэше хранится конфиденциальная информация пользователей. В этом случае можно использовать Amazon ElastiCache for Redis с поддержкой шифрования данных при передачи и хранении. Серверы кэширования часто подвергаются атакам, созданным путем подделки записей кэша. Уязвимость протокола передачи данных позволяет злоумышленникам наполнить кэш необходимыми им значениями. Это усиливает воздействие атаки: при наличии в кэше данного значения для всех запросов будет отображаться значение злоумышленника. Кроме того, кэширование также подвержено атакам по времени по сторонним каналам. Кэшированные данные возвращаются быстрее, чем некэшированные, поэтому злоумышленник может использовать время ответа, чтобы получить информацию о запросах, которые делают другие клиенты или участники.

В заключение о ситуации, возникающей, когда несколько клиентов делают запросы, для которых требуется один и тот же ресурс для обработки некэшеруемых данных почти в одно и то же время. Это также может произойти, если сервер подключается к группе с незаполненным локальным кэшем. В результате возникает подчиненная зависимость обработки большого количества запросов для различных серверов, что может привести к ограничению производительности или отключению. Чтобы устранить эту проблему, мы используем объединение запросов: серверы или внешний кэш позволяют сделать только один запрос на рассмотрение для некэшируемых ресурсов. Некоторые кэш-библиотеки и серверы кэширования (например, Nginx или Varnish) поддерживают объединение запросов. Объединение запросов также может быть реализовано вместе с существующими кэшами. 

Рекомендации и ограничения Amazon

В этой статье приведены рекомендации Amazon по использованию кэширования, описываются его преимущества и недостатки, а также связанные с ним риски. Ниже приводятся рекомендации Amazon, которыми пользуются наши команды при внедрении кэширования.

• Убедитесь в обоснованности использования кэширования. Как оно согласуется с затратами, задержками и/или доступностью. Проверьте возможность кэширования: убедитесь, что данные пригодны для использования при обработке нескольких клиентских запросов. Скрупулезно оцените преимущества и возможные риски использования кэша.
• Тщательно спланируйте использование кэширования, соблюдая такие же жесткие требования и придерживаясь тех же процессов, что и для остального комплекта услуг и инфраструктуры. Не стоит недооценивать эти меры. Сопоставьте данные использования кэша и частоты попаданий, чтобы проверить правильность настроек кэширования. Следите за ключевыми параметрами (например, ЦПУ и памятью), чтобы обеспечить надлежащую работу и масштабирование внешний платформы для кэширования. Настройте предупреждения для этих параметров. Убедитесь, что группа кэширования может быть увеличена без простоя или массовой инвалидации кэша (проверьте правильность работы функции согласованного хэширования).
• Взвешенно и последовательно выберите размер кэша и определите политики окончания срока действия и вытеснения данных. Для их проверки и настройки проведите тестирование и используйте показатели, указанные в предыдущем пункте.
• Проверьте отказоустойчивость сервиса в случае некорректной работы кэша, возможной при возникновении условий, которые приводят к проблемам обработки запросов с использованием кэшированных данных. Это могут быть «холодные» запуски, перебои в работе групп кэширования, изменения в схемах трафика или продолжительные отключения подчиненных сервисов. Во многих случаях это приводит к частичной потере доступности для обеспечения непрерывной работы ваших и подчиненных сервисов (например, за счет сброса нагрузки, ограничения запросов к зависимым сервисам или обслуживания устаревших данных). Для проверки запустите тесты на нагрузку с отключенными кэшами.
• Оцените аспекты безопасности обслуживания кэшированных данных, включая шифрование, безопасность пересылки при обмене данными с внешними группами кэширования, а также воздействие атак с отравлением кэша и атак по сторонним каналам.
• Разработайте формат хранения кэшируемых объектов с изменениями с течением времени (например, используйте номер версии) и напишите код преобразования в последовательную форму, способный читать более старые версии. Остерегайтесь попадания подделок записей кэша в механизм сериализации.
• Оцените обработку кэшем ошибок подчиненных сервисов, а также рассмотрите вопрос хранения негативного кэша с определенным временем жизни. Не вызывайте и не усугубляйте сбои в работе, неоднократно запрашивая один и тот же подчиненный ресурс и сбрасывая ответы об ошибках.

Многие сервисные службы в Amazon используют методы кэширования. Несмотря на все положительные стороны использования этих методов, принятие решения о внедрении кэширования не дается легко: имеющиеся недостатки порою перевешивают преимущества. Надеемся, что эта статья поможет вам при оценке преимуществ использования кэширования в ваших сервисах.


Об авторах

Мэтт – главный инженер отдела новых устройств Amazon. Он занимается разработкой ПО и сервисов для потребительских устройств, которые еще не вышли на рынок. Ранее он работал в AWS Elemental. Возглавляемая Мэттом команда запустила MediaTailor, сервис на стороне сервера для вставки индивидуально настроенных объявлений во время показа прямых трансляций и видео по требованию. Кроме того, он участвовал в запуске первого сезона трансляций NFL Thursday Night Footbal на сервисе Prime Video. До Amazon Мэтт 15 лет проработал в индустрии безопасности (в том числе в таких компаниях, как McAfee и Intel, а также в нескольких стартапах), занимаясь разработками в области управления корпоративной безопасностью и технологий для защиты от вредоносных программ, устранением уязвимостей ПО, аппаратной поддержкой безопасности и DRM.

Джас Чхабра – главный инженер в AWS. Он стал членом команды AWS в 2016 году. До этого Джас несколько лет работал над AWS IAM, а потом сменил специализацию, и теперь его деятельность связанна с AWS Machine Learning. До AWS он работал на различных технических должностях в Intel, связанных с системами контроля, идентификацией и безопасностью. Текущая сфера деятельности: машинное обучение, безопасность и крупномасштабные распределенные системы. Бывшая сфера деятельности: системы контроля, биткойны, идентификация и криптография. Имеет ученую степень в области информатики.

Похожие материалы
Работа распределенных систем без необходимости откатов Сброс нагрузки во избежание перегрузок Обеспечение безопасности отката во время развертывания

Каскадная инвалидация кэша. Часть 1

Вот уже несколько лет, как почти каждая статья о передовых подходах к кэшированию рекомендует пользоваться в продакшне следующими методиками:
  • Добавление в имена файлов информации о версии содержащихся в них данных (обычно — в виде хэша данных, находящихся в файлах).
  • Установка HTTP-заголовков Cache-Control: max-age и Expires, управляющих временем кэширования материалов (что позволяет исключить повторную валидацию соответствующих материалов для посетителей, возвращающихся на ресурс).

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

filename: '[name]-[contenthash].js'

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

Эксперты в сфере производительности веб-проектов, кроме того, рекомендуют пользоваться методиками разделения кода. Эти методики позволяют разбивать JavaScript-код на отдельные бандлы. Такие бандлы могут быть загружены браузером параллельно, или даже лишь тогда, когда в них возникнет необходимость, по запросу браузера.

Одним из многих преимуществ разделения кода, в частности, имеющих отношение к передовым методикам кэширования, называют то, что изменения, внесённые в отдельный файл с исходным кодом, не приводят к инвалидации кэша всего бандла. Другими словами, если для npm-пакета, созданного разработчиком «X», вышло обновление безопасности, и при этом содержимое node_modules разбито на фрагменты по разработчикам, то изменить придётся только фрагмент, содержащий пакеты, созданные «X».

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

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

Проблема, касающаяся версионирования имён файлов


Представьте, что вы создали и развернули веб-сайт. Вы воспользовались разделением кода, в результате большая часть JavaScript-кода вашего сайта загружается по запросу.

На следующей диаграмме зависимостей можно видеть точку входа кодовой базы — корневой фрагмент main, а также — три фрагмента-зависимости, загружаемых асинхронно — dep1, dep2 и dep3. Есть здесь и фрагмент vendor, содержащий все зависимости сайта из node_modules. Все имена файлов, в соответствии с рекомендациями по кэшированию, включают в себя хэши содержимого этих файлов.


Типичное дерево зависимостей JavaScript-модуля

Так как фрагменты dep2 и dep3 импортируют модули из фрагмента vendor, то в верхней части их кода, сгенерированного сборщиком проекта, мы, скорее всего, обнаружим команды импорта, выглядящие примерно так:

import {...} from '/vendor-5e6f.mjs';

Теперь подумаем о том, что произойдёт, если изменится содержимое фрагмента vendor.

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

-import {...} from '/vendor-5e6f.mjs';
+import {...} from '/vendor-d4a1.mjs';

Однако так как эти команды импорта являются частью содержимого фрагментов dep2 и dep3, то их изменение означает, что изменится и хэш содержимого файлов dep2 и dep3. А значит — и имена этих файлов тоже изменятся.

Но на этом всё не заканчивается. Так как фрагмент main импортирует фрагменты dep2 и dep3, а имена их файлов изменились, команды импорта в main тоже поменяются:

-import {...} from '/dep2-3c4d.mjs';
+import {...} from '/dep2-2be5.mjs';
-import {...} from '/dep3-d4e5.mjs';
+import {...} from '/dep3-3c6f.mjs';

И наконец, так как содержимое файла main изменилось, имя этого файла тоже должно будет измениться.

Вот как теперь будет выглядеть диаграмма зависимостей.


Модули в дереве зависимостей, на которые повлияло единственное изменение в коде одного из листовых узлов дерева

Из этого примера видно, как небольшое изменение кода, сделанное всего лишь в одном файле, привело к инвалидации кэша 80% фрагментов бандла.

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

Это приводит нас к следующему вопросу: «Можно ли получить преимущества, даваемые иммутабельными ресурсами и долговременным кэшированием, и при этом не страдать от каскадных инвалидаций кэша?».

Подходы к решению проблемы


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

Решение этой проблемы заключается в том, чтобы, говоря языком вышеприведённого примера, сделать возможным импорт фрагмента vendor фрагментами dep2 и dep3 без указания информации о версии файла фрагмента vendor. При этом нужно гарантировать, чтобы загруженная версия vendor была бы правильной, принимая во внимание текущие версии dep2 и dep3.

Как оказалось, существует несколько способов достижения этой цели:

  • Карты импорта.
  • Сервис-воркеры.
  • Собственные скрипты для загрузки ресурсов.

Рассмотрим эти механизмы.

Подход №1: карты импорта


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

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

Использование карт импорта для предотвращения каскадной инвалидации кэша состоит из трёх шагов.

▍Шаг 1


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

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

dep1.mjs
dep2.mjs
dep3.mjs
main.mjs
vendor.mjs

Команды импорта в соответствующих модулях тоже не будут включать в себя хэши:
import {...} from '/vendor.mjs';

▍Шаг 2


Нужно воспользоваться инструментом, наподобие rev-hash, и сгенерировать с его помощью копию каждого файла, к имени которого добавлен хэш, указывающий на версию его содержимого.
После того, как эта часть работы выполнена, содержимое выходной директории должно будет выглядеть примерно так, как показано ниже (обратите внимание на то, что там теперь присутствует по два варианта каждого файла):
dep1-b2c3.mjs",
dep1.mjs
dep2-3c4d.mjs",
dep2.mjs
dep3-d4e5.mjs",
dep3.mjs
main-1a2b.mjs",
main.mjs
vendor-5e6f.mjs",
vendor.mjs

▍Шаг 3


Нужно создать JSON-объект, хранящий сведения о соответствии каждого файла, в имени которого нет хэша, каждому файлу, в имени которого хэш есть. Этот объект нужно добавить в HTML-шаблоны.

Этот JSON-объект и является картой импорта. Вот как он может выглядеть:

<script type="importmap">
{
  "imports": {
    "/main.mjs": "/main-1a2b.mjs",
    "/dep1.mjs": "/dep1-b2c3.mjs",
    "/dep2.mjs": "/dep2-3c4d.mjs",
    "/dep3.mjs": "/dep3-d4e5.mjs",
    "/vendor.mjs": "/vendor-5e6f.mjs",
  }
}
</script>

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

Если воспользоваться этой картой импорта как примером, то можно выяснить, что команда импорта, ссылающаяся на файл /vendor.mjs, на самом деле выполнит запрос и загрузку файла /vendor-5e6f.mjs:

// Команда ссылается на `/vendor.mjs`, но загружает`/vendor-5e6f.mjs`.
import {...} from '/vendor.mjs';

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

Возможно, вы сейчас задаётесь вопросом о том, почему я создал копию каждого файла вместо того, чтобы просто файлы переименовать. Это нужно для поддержки браузеров, которые не могут работать с картами импорта. В предыдущем примере подобные браузеры увидят лишь файл /vendor.mjs и просто загрузят этот файл, поступив так, как обычно поступают, встречая подобные конструкции. В результате и оказывается, что на сервере должны существовать оба файла.

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

Продолжение следует…

Уважаемые читатели! Знакома ли вам проблема каскадной инвалидации кэша?


AEM Dispatcher. Часть 4: Инвалидация кэша — Сертифицированная Команда Разработчиков Adobe Experience Manager

Предыдущие части:


Многие из нас скорее всего сталкивались с ситуацией, когда диспетчер использует старую версию кода (CSS, HTML, JS). В этой статье я расскажу о правильной конфигурации инвалидации страниц на диспетчере чтобы таких проблем не возникало.

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

Начальные настройки секции инвалидации

Внутри секции /cache находится блок /invalidate, который устанавливает кэшированные файлы, доступные для автоматической инвалидации при обновлении контента. Например, следующие настройки устанавливают инвалидацию всех HTML страниц:

/cache
{
    /invalidate
    {
        /0000  { /glob "*" /type "deny" }
        /0001  { /glob "*.html" /type "allow" }
    }
}

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

/cache
{
    /invalidate
    {
        /0000  { /glob "*" /type "allow" }
    }
}

Перезапустите httpd-сервер после обновления секции /invalidate для применения новых изменений.

Инвалидация в подробностях

На низком уровне диспетчер использует специальные пустые стат-файлы с именем по умолчанию «.stat». По умолчанию установлен параметр /statfileslevel «0», что подразумевает использование только одного стат-файла, располагающегося в корне папки htdocs. Если время изменения стат-файла более позднее, чем время изменения ресурса, тогда диспетчер расценивает такой ресурс устаревшим или невалидным.

Например, пусть у нас есть следующие кэшированные ресурсы после запроса страницы http://localhost/content/geometrixx/en/products.html :

Давайте инвалидируем их, применяя низкоуровневый механизм стат-файлов. Создайте пустой файл с именем «.stat» в корне вашей папки htdocs:

Обратите внимание, что время изменения стат-файла более позднее, чем кэшированных ресурсов. Для диспетчера это означает, что все ресурсы устаревшие. Это и есть низкоуровневый механизм инвалидации. Если мы после создания такого стат-файла снова посетим страницу http://localhost/content/geometrixx/en/products.html, то запрашиваемые кэшированные ресурсы будут обновлены:

Этот пример иллюстрирует схему инвалидации с параметром по умолчанию /statfileslevel «0». Давайте разберёмся, как мы можем настроить инвалидацию более тонко и детально при помощи параметра /statfileslevel.

Настройка параметра /statfileslevel

Вы можете пользоваться свойством /statfileslevel в конфигурационном файле диспетчера для выборочной инвалидации кэшированных файлов соответственно их путям. У механизма работы свойства /statfileslevel есть несколько правил:

  • Диспетчер создаёт .stat файлы в каждой папке начиная от docroot и до уровня, который вы указываете. Уровень папки docroot равен 0.
  • При обновлении файла диспетчер по пути файла находит папку, лежащую на уровне statfileslevel и инвалидирует все файлы этой папки и все файлы, лежащие ниже внутри этой папки.
  • Если уровень обновлённого файла меньше уровня statfileslevel, тогда инвалидируются только файлы папки, содержащей обновлённый файл, но файлы, лежащие ниже внутри этой папки остаются валидными.
  • При обновления файла все файлы соответствующей папки и выше вплоть до корневого уровня включительно станут невалидными.

Для лучшего понимания правил свойства /statfileslevel давайте рассмотрим парочку примеров. Наш демонстрационный случай по умолчанию, в котором /statfileslevel «0», выглядит следующим образом:

Есть только один стат-файл в корневой папке docroot. А область ответственности или зона охвата этого стат-файла будет целиком всё файловое дерево нашего docroot. Если у какого-то файла дерева дата изменения старше даты изменения стат-файла, тогда диспетчер расценивает такой файл невалидным.

Если мы установим /statfileslevel «4», тогда инвалидация работает уже вот так:

Стат-файлы есть на всех уровнях начиная от 0 (корень) до 4 включительно.

Стат-файл на уровне меньше 4 обладает областью ответственности или зоной охвата только содержащей его папки. Это означает, что если стат-файл внутри папки content/geometrixx/en новее, чем какой-то файл из этой папки, то такой файл является невалидным, но валидация всех файлов из остальных папок определяется другими стат-файлами.

Только стат-файлы, лежащие на уровне со значением statfileslevel (в нашем случае уровень равен 4), обладают областью ответственности или зоной охвата всего нижележащего дерева, начинающего от папки, содержащей такой стат-файл и простирающегося вниз до более низких уровней файлового дерева. Это означает, что если у стат-файла внутри папки content/geometrixx/en/products время изменения новее, чем у какого-то файла из нижележащего файлового дерева, включающего папку products, тогда диспетчер считает такой файл невалидным. Валидация всех файлов, которые не находятся в этом дереве, определяется другими стат-файлами.

Автоматическая инвалидация и flush-агенты

В целях автоматизации инвалидации вы можете включить авторские или публичные flush-агенты. Рекомендуется пользоваться публичным flush-агентом для более надёжной автоматической инвалидации, потому что использование авторского flush-агента может вызывать следующие проблемы:

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

Публичный flush-агент находится по адресу http://localhost:4503/etc/replication/agents.publish/flush.html

Для включения публичного flush-агента нажмите кнопку «Edit» и установите галочку в чекбоксе «Enabled»:

Также обновите URI порт на вкладке Transport и установите его значение равным 80:

Сохраните ваши изменения, и вы увидите, что публичный flush-агент активировался:

Запросы для инвалидации вручную

Вы можете отправлять следующие запросы для того, чтобы вручную инвалидировать ваши кэшированные ресурсы:

  • для удаления кэшированных файлов
    POST /dispatcher/invalidate.cache HTTP/1.1
    CQ-Action: Activate
    CQ-Handle: path-pattern
    Content-Length: 0
    
  • для удаления и перекэширования файлов
    POST /dispatcher/invalidate.cache HTTP/1.1
    CQ-Action: Activate 
    Content-Type: text/plain
    CQ-Handle: path-pattern
    Content-Length: numchars in bodypage_path0
    Page_path2
    …
    Page_pathn
    

Резюме

В итоге мы узнали, как работают механизм инвалидации на низком уровне, flush-агенты для автоматической инвалидации после публикации страниц и запросы для инвалидации вручную.

Подробная и полезная документация доступна на следующих страницах:

Автор: Виталий Киселев, AEM Разработчик

Четыре вопроса, которые вы должны постоянно задавать себе во время программирования

Большинство современных языков программирования общего назначения предоставляют огромное количество возможностей и способов его использовать. Часто их можно применять на целом наборе уровней абстракции, выбирая из десятков вариантов реализации определённой фичи. Некоторые возможности предназначены для разработчиков библиотек, другие для быстрой реализации приложений для конечных пользователей. И программисты не всегда пользуются оптимальными инструментами для своих задач.

Например, новички довольно часто применяют в коде низкоуровневые потоки, тогда как обычно намного выгоднее использовать более высокоуровневые вещи вроде workers, tasks или actors. С ними не только проще обращаться, зачастую они ещё и более точно подходят для решения исходной задачи.

Как понять, что именно оптимально подходит для решения задачи?

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

А затем задайте себе несколько простых вопросов:

  1. Наверняка кто-то уже решал аналогичную задачу до меня. Поискал ли я подсказки или готовые решения других разработчиков?
  2. Существует ли (лучшая) библиотека, структура данных или абстракция для моей задачи?
  3. Мне действительно нужно решать эту задачу именно так? Наверняка должен быть лучший или более простой способ.
  4. Какие плюсы я получу от использования этой фичи, библиотеки или инструмента и сколько усилий это будет мне стоить? Есть ли альтернативы?

В качестве заключения

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

Хинт для программистов: если зарегистрируетесь на соревнования Huawei Cup, то бесплатно получите доступ к онлайн-школе для участников. Можно прокачаться по разным навыкам и выиграть призы в самом соревновании.

Перейти к регистрации

По материалам записи в блоге Mihael Vrbanec

Оценить

Оптимизация производительности Django проектов (часть 3)

Остальные статьи цикла:

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

Кэш фреймворк Django

Django предоставляет ряд средств для кэширования из коробки. Хранилище кэша настраивается при помощи словаря CACHES в settings.py:

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.db.DatabaseCache",
        "LOCATION": "my_cache_table",
    }
}

Django предоставляет несколько встроенных бекендов для кэша, рассмотрим некоторые из них:

  • DummyCache — ничего не кэширует, используется при разработке/тестировании, если нужно временно отключить кэширование,
  • DatabaseCache — хранит кэш в БД, не самый быстрый вариант, но может быть полезен для хранения результатов долгих вычислений или сложных SQL запросов,
  • MemcachedCache — использует Memcached в качестве хранилища, для использования этого бекенда вам понадобится поднять сервер(ы) Memcached.

Для использования в продакшене лучше всего подходит MemcachedCache и в некоторых случаях может быть полезен DatabaseCache. Также Django позволяет использовать сторонние бекенды, например, удачным вариантом может быть использование Redis в качестве хранилища для кэша. Redis предоставляет больше возможностей чем Memcached и вы скорее всего и так уже используете его в вашем проекте. Вы можете установить пакет django-redis и настроить его как бекенд для вашего кэша.

Кэширование всего сайта

Если на вашем сайте нет динамического контента, который часто меняется, то вы можете решить проблему кэширования просто — включив кэширование всего сайта. Для этого нужно добавить несколько настроек в settings.py:

MIDDLEWARE = [
    'django.middleware.cache.UpdateCacheMiddleware',

    # place all other middlewares here

    'django.middleware.cache.FetchFromCacheMiddleware',
]

# Key in `CACHES` dict
CACHE_MIDDLEWARE_ALIAS = 'default'

# Additional prefix for cache keys
CACHE_MIDDLEWARE_KEY_PREFIX = ''

# Cache key TTL in seconds
CACHE_MIDDLEWARE_SECONDS = 600

После добавления показанных выше middleware первым и последним в списке, все GET и HEAD запросы будут кэшироваться на указанное в параметре CACHE_MIDDLEWARE_SECONDS время.

При необходимости вы даже можете програмно сбрасывать кэш:

from django.core.cache import caches
cache = caches['default']  # `default` is a key from CACHES dict in settings.py
ache.clear()

Или можно сбросить кэш непосредственно в используемом хранилище. Например, для Redis:

$ redis-cli -n 1 FLUSHDB # 1 is a DB number specified in settings.py

Кэширование view

Если в вашем случае не целесообразно кэшировать весь сайт, то вы можете включить кэширование только определенных view, которые создают наибольшую нагрузку. Для этого Django предоставляет декоратор cache_page:

from django.views.decorators.cache import cache_page


@cache_page(600, cache='default', key_prefix='')
def author_page_view(request, username):
    author = get_object_or_404(Author, username=username)
    show_articles_link = author.articles.exists()
    return render(
        request, 'blog/author.html',
        context=dict(author=author, show_articles_link=show_articles_link))

cache_page принимает следующие параметры:

  • первый обязательный аргумент задает TTL кэша в секундах,
  • cache — ключ в словаре CACHES,
  • key_prefix — префикс для ключей кэша.$’, cache_page(600)(ArticlesListView.as_view()), name=’articles_list’), … ]

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

    Кэширование части шаблона

    В предыдущей части этой серии статей было описано, что QuerySet объекты ленивые и SQL запросы не выполняются без крайней необходимости. Мы можем воспользоваться этим и закэшировать фрагменты шаблона, что позволит избежать SQL запросов на время жизни кэша. Для этого нужно воспользоваться тегом шаблона cache:

    {% load cache %}
    
    <h2>Articles list</h2>
    
    <p>Authors count: {{ authors_count }}</p>
    
    <h3>Top authors</h3>
    
    {% cache 500 top_author %}
    <ul>
        {% for author in top_authors %}
        <li>{{ author.username }} ({{ author.articles_count }})</li>
        {% endfor %}
    </ul>
    {% endcache %}
    
    {% cache 500 articles_list %}
    {% for article in articles %}
    <article>
        <h3>{{ article.title }}</h3>
        <time>{{ article.created_at }}</time>
        <p>Author: <a href="{% url 'author_page' username=article.author.username %}">{{ article.author.username }}</a></p>
        <p>Tags:
        {% for tag in article.tags.all %}
            {{ tag }}{% if not forloop.last %}, {% endif %}
        {% endfor %}
    </article>
    {% endfor %}
    {% endcache %}
    

    Результат добавления тегов cache в шаблон (до и после соответственно):

    cache принимает следующие аргументы:

    • первый обязательный аргумент означает TTL кэша в секундах,
    • обязательное название фрагмента,
    • не обязательные дополнительные переменные, которые идентифицируют фрагмент по динамическим данным,
    • ключевой параметр using='default', должен соответствовать ключу словаря CACHES в settings.py.

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

    {% cache 500 personal_articles_list request.user.username %}
        <!-- ... -->
    {% %}
    

    При необходимости можно передавать несколько таких переменных для создания ключей на основе комбинации их значений.

    Низкоуровневое кэширование

    Django предоставляет доступ к низкоуровневому API кэш фреймворка. Вы можете использовать его для сохранения/извлечения/удаления данных по определенному ключу в кэше. Рассмотрим небольшой пример:

    from django.core.cache import cache
    
    class ArticlesListView(ListView):
    
        ...
    
        def get_context_data(self, **kwargs):
            context = super().get_context_data(**kwargs)
            authors_count = cache.get('authors_count')
            if authors_count is None:
                authors_count = Author.objects.count()
                cache.set('authors_count', authors_count)
            context['authors_count'] = authors_count
            ...
            return context
    

    В этом фрагменте кода мы проверяем, есть ли в кэше количество авторов, которое должно быть по ключу authors_count. Если есть (cache.get вернул не None), то используем значение из кэша. Иначе запрашиваем значение из БД и сохраняем в кэш. Таким образом в течении времени жизни ключа в кэше мы больше не будем обращаться к БД.

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

    • настроить адекватное TTL для кэша, которое бы соответствовало частоте изменения кэшируемых данных,
    • реализовать инвалидацию кэша.

    Инвалидация кеша должна происходить по событию изменения данных. Рассмотрим, как можно реализовать инвалидацию для примера с количеством авторов:

    from django.db.models.signals import post_delete, post_save
    from django.dispatch import receiver
    from django.core.cache import cache
    
    
    def clear_authors_count_cache():
        cache.delete('authors_count')
    
    
    @receiver(post_delete, sender=Author)
    def author_post_delete_handler(sender, **kwargs):
        clear_authors_count_cache()
    
    
    @receiver(post_save, sender=Author)
    def author_post_save_handler(sender, **kwargs):
        if kwargs['created']:
            clear_authors_count_cache()
    

    Были добавлены 2 обработчика сигналов: создание и удаление автора. Теперь при изменении количества авторов значение в кэше по ключу authors_count будет сбрасываться и в view будет запрашиваться новое количество авторов из БД.

    cached_property

    Кроме кэш фреймворка Django также предоставляет возможность кэшировать обращение к функции прямо в памяти процесса. Такой вид кэша возможен только для методов не принимающих никаких параметров кроме self. Такой кэш будет жить до тех пор пока существует соответствующий объект.

    cached_property это декоратор входящий в Django. Результат применения его к методу, кроме кэширования, метод становится свойством и вызывается неявно без необходимости указания круглых скобок. Рассмотрим пример:

    class Author(models.Model):
    
        username = models.CharField(max_length=64, db_index=True)
        email = models.EmailField()
        bio = models.TextField()
    
        @cached_property
        def articles_count(self):
            return self.articles.count()
    

    Проверим как работает свойство article_count с включенным логированием SQL:

    >>> from blog.models import Author
    >>> author = Author.objects.first()
    (0.002) SELECT "blog_author"."id", "blog_author"."username", "blog_author"."email", "blog_author"."bio" FROM "blog_author" ORDER BY "blog_author"."id" ASC LIMIT 1; args=()
    >>> author.articles_count
    (0.001) SELECT COUNT(*) AS "__count" FROM "blog_article" WHERE "blog_article"."author_id" = 142601; args=(142601,)
    28
    >>> author.articles_count
    28
    

    Как вы видите, повторное обращение к свойству article_count не вызывает SQL запрос. Но если мы создадим еще один экземпляр автора, то в нем это свойство не будет закэшированно, до того как мы впервые к нему обратимся, т.к. кэш в данном случае привязан к экземпляру класса Author.

    Cacheops

    django-cacheops это сторонний пакет, который позволяет очень быстро внедрить кэширование запросов к БД практически не меняя код проекта. Большую часть случаев можно решить просто задав ряд настроек этого пакета в settings.py.

    Рассмотрим на примере простой вариант использования этого пакета. В качестве тестового проекта будем использовать пример из прошлой части серии.

    Cacheops использует Redis в качестве хранилища кэша, в settings.py нужно указать параметры подключения к серверу Redis.

    CACHEOPS_REDIS = "redis://localhost:6379/1"
    
    INSTALLED_APPS = [
        ...
        'cacheops',
    ]
    
    CACHEOPS = {
        'blog.*': {'ops': 'all', 'timeout': 60*15},
        '*.*': {'timeout': 60*60},
    }
    

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

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

    CACHEOPS = {
        'blog.author': {'ops': 'all', 'timeout': 60 * 60},  # cache all queries to `Author` model for an hour
    
        'blog.article': {'ops': 'fetch', 'timeout': 60 * 10},  # cache `Article` fetch queries for 10 minutes
        # Or
        'blog.article': {'ops': 'get', 'timeout': 60 * 15},  # cache `Article` get queries for 15 minutes
        # Or
        'blog.article': {'ops': 'count', 'timeout': 60 * 60 * 3},  # cache `Article` fetch queries for 3 hours
    
        '*.*': {'timeout': 60 * 60},
    }
    

    Кроме этого cacheops имеет ряд других функций, некоторые из них:

    • ручное кэширование Article.objects.filter(tag=2).cache(),
    • кэширование результатов выполнения функций с привязкой к модели и автоматической инвалидацией,
    • кэширование view с привязкой к модели и автоматической инвалидацией,
    • кэширование фрагментов шаблона и многое другое.

    Рекомендую ознакомится с README cacheops чтобы узнать подробности.

    HTTP кэширование

    Если ваш проект использует HTTP, то кроме серверного кэширования вы также можете использовать встроенные в HTTP протокол механизмы кэширования. Они позволяют настроить кэширование результатов безопасных запросов (GET и HEAD) на клиенте (например, браузере) и на промежуточных прокси-серверах.

    Управление кэшированием осуществляется при помощи HTTP заголовков. Установку этих заголовков можно настроить в приложении или, например, на web-сервере (Nginx, Apache, etc).

    Django предоставляет middleware и несколько удобных декораторов для управления HTTP кэшем.

    Vary

    Заголовок Vary позволяет задать список названий заголовков, значения в которых будут учитываться при создании ключа кэша. Django предоставляет view декоратор vary_on_headers для управления этим заголовком.

    from django.views.decorators.vary import vary_on_headers
    
    
    @vary_on_headers('User-Agent')
    def author_page_view(request, username):
        ...
    

    В данном случае, для разных значений заголовка User-Agent будут разные ключи кэша.

    Cache-Control

    Заголовок Cache-Control позволяет задавать различные параметры управляющие механизмом кэширования. Для задания этого заголовка можно использовать встроенный в Django view декортатор cache_control.

    from django.views.decorators.cache import cache_control
    
    
    @cache_control(private=True, max_age=3600)
    def author_page_view(request, username):
        ...
    

    Рассмотрим некоторые директивы заголовка Cache-Control:

    • public, private — разрешает или запрещает кэширование в публичном кэше (прокси серверах и тд). Это важные директивы, которые позволяют обезопасить приватный контент, который должен быть доступен только определенным пользователям.
    • no-cache — отключает кэширование, что заставляет клиент делать запрос к серверу.
    • max-age — время в секундах, после которого считается, что контент устарел и его нужно запросить заново.

    Last-Modified & Etag

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

    • Last-Modified — дата и время последнего изменения ресурса.
    • Etag — идентификатор версии ресурса (уникальный хэш или номер версии).

    После этого при повторном обращении к ресурсу клиент должен использовать заголовки If-Modified-Since и If-None-Match соответственно. В таком случае, если ресурс не изменился (исходя из значений Etag и/или Last-Modified), то сервер вернет статус 304 без тела ответа. Это позволяет выполнять повторную загрузку ресурса только в том случае, если он изменился и тем самым съекономить время и ресурсы сервера.

    Кроме кэширования, описанные выше заголовки применяются для проверки предусловий в запросах изменяющих ресурс (POST, PUT и тд). Но обсуждение этого вопроса выходит за рамки данной статьи.

    Django предоставляет несколько способов задания заголовков Etag и Last-Modified. Самый простой способ — использование ConditionalGetMiddleware. Этот middleware добавляет заголовок Etag, на основе ответа view, ко всем GET запросам приложения. Также он проверяет заголовки запроса и возвращает 304, если ресурс не изменился.

    Этот подход имеет ряд недостатков:

    • middleware применяется сразу ко всем view проекта, что не всегда нужно,
    • для проверки актуальности ресурса необходимо сгенерировать полный ответ view,
    • работает только для GET запросов.

    Для тонкой настройки нужно применять декоратор condition, который позволяет задавать кастомные функции для генерации заголовков Etag и/или Last-Modified. В этих функциях можно реализовать более экономный способ определения версии ресурса, например, на основе поля в БД, без необходимости генерации полного ответа view.

    # models.py
    
    class Author(models.Model):
        ...
        updated_at = models.DateTimeField(auto_now=True)
    
    
    # views.py
    from django.views.decorators.http import condition
    
    
    def author_updated_at(request, username):
        updated_at = Author.objects.filter(username=username).values_list('updated_at', flat=True)
        if updated_at:
            return updated_at[0]
        return None
    
    
    @condition(last_modified_func=author_updated_at)
    def author_page_view(request, username):
        ...
    

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

    Кэширование статических файлов

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

    В продакшн окружении вы скорее всего не будете отдавать статические файлы через Django, т.к. это медленно и не безопасно. Для этой задачи обычно используется Nginx или другой web-сервер. Рассмотрим как настроить кэширование статики на примере Nginx:

    server {
        # ...
    
        location /static/ {
            expires 360d;
            alias /home/www/proj/static/;
        }
    
        location /media/ {
            expires 360d;
            alias /home/www/proj/media/;
        }
    }
    

    Где,

    • /static/ это базовый путь к статическим файлам, куда, в том числе, копируются файлы при выполнении collectstatic,
    • /media/ базовый путь к файлам загружаемым пользователями.

    В данном примере мы кэшируем всю статику на 360 дней. Важно, чтобы при изменении какого-либо статического файла, его URL также изменялся, что приведет к загрузке новой версии файла. Для этого можно добавлять GET параметры к файлам с номером версии: script.js?version=123. Но мне больше нравится использовать Django Compressor, который кроме всего прочего, генерирует уникальное имя для скриптов и стилей при их изменении.

    Почему сложно сделать правильное кэширование

    Вы, наверняка, знаете, как это бывает. Ой, у нас тут такие тяжелые вычисления / так долго тянутся данные из базы. А давайте просто прикрутим кэшик. Что может пойти не так? Так вот, опыт показывает, что пойти не так может очень и очень многое.

    Постановка задачи

    Рассмотрим типичный сценарий. Есть больше одного бэкенда. Есть база данных. На практике баз часто несколько и у них еще бывают реплики. В рамках этой задачи представим, что база одна, так как тут многое зависит от специфики БД. Есть несколько серверов Memcached или Redis. Для определенности далее будем говорить про Memcached. Некоторые запросы выполняются слишком долго, чтобы постоянно слать их в базу, поэтому их нужно кэшировать. На каком кэш-сервере какой ключ искать определяется, например, алгоритмом Ketama. Следует отметить, что определять сервер просто по хэшу от ключа очень плохо, потому что такое решение приводит к практически полной инвалидации кэша при решардинге.

    Распределенные кэши в Akka Cluster, которые мы недавно рассматривали, как оказывается, в ряде граничных случаев сводятся к описанной выше проблеме. Во-первых, после перераскладки серверов возникает проблема холодных кэшей. В связи с этим возникает желание дублировать содержимое in-memory кэшей во внешнем Memcached и считывать это содержимое при старте. Во-вторых, при изменении состава кластера какое-то время разные узлы ищут один и тот же кэш в разных местах. В итоге получаем те же несколько бэкендов, которые читают и пишут в Memcached по одному ключу.

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

    Почему не получается все сделать правильно

    Ниже представлен не претендующий на полноту список возникающих проблем:

    1. Параллельная запись. Несколько бэкендов одновременно пишут значение по одному ключу. Получаем состояние гонки и испорченные данные. К счастью, эта проблема легко решается, если кэш-сервер поддерживает CAS. Кроме того, у вас нет такой проблемы, если данные в кэше всегда неизменяемы.
    2. Нетсплиты и падения серверов. Падение одного или нескольких серверов можно с большой точностью считать частным случаем нетсплита. Вы можете сделать возникновение нетсплита очень маловероятными, например, если будете физически дублировать все сетевые соединения. Но это сложно, дорого, и требует своего ДЦ. PACELC, более известный, как «CAP-теорема», говорит нам, что в случае нетсплита мы должны выбирать между консистентностью и доступностью. То есть, если вы не можете достучаться до кэш-сервера, то либо отдаете пользователю ерунду (скажем, последнее значение из памяти или какой-то дэфолт), либо говорите «простите, сервис недоступен». В случае, например, с котировками в системе торговли ценными бумагами, строго говоря, ничто из этого не является допустимым. Конкретно в случае с кэшами можно еще и сходить в базу напрямую, но это медленно и может положить базу под увеличенной нагрузкой.
    3. Поднятие и опускание кэш-серверов. В этом случае какое-то время разные бэкенды ходят по одному ключу на разные кэш-сервера. Вы можете получать список кэш-серверов перед каждой операцией из ZooKeeper, Consul или etcd, но это не устранит гонку полностью. Можно дождаться минимальной нагрузки, зафаерволить все кэш-сервера, сведя проблему к предыдущей, обновить список, затем сделать кэш-сервера доступными. Но не в каждом приложении такое возможно. Плюс при частом изменении состава кэш-кластера есть шанс считать по ключу очень-очень старое значение, записанное еще до изменения состава. На данный момент мне неизвестно на 100% рабочее решение этой проблемы.
    4. Разъезжание базы и кэшей. Допустим, вы реализовали следующий алгоритм, который предусматривает многое. При чтении (1) проверяем кэш, если что-то есть — возвращаем, иначе запоминаем токен для CAS, (2) читаем из базы (3) пишем в кэш, и если CAS не проходит, goto 1. При записи (1) инвалидируем кэш и запоминаем токен, иначе если приложение упадет после записи в БД, все разъедется (2) пишем в БД (3) обновляем кэш, если конфликт, goto 4, иначе ОК (4) считать данные из БД, goto 3. Но даже тут есть гонка. Допустим, мы делаем запись и падаем после шага 2. Параллельно с записью в базу данных происходило чтение. В результате в кэше оказались старые данные, которые будут там до следующей записи или очистки кэша по TTL. Для полноценного решения проблемы, насколько я понимаю, нужна некая разновидность распределенных транзакций.
    5. Самая главная проблема. Все это должны поддерживать люди, понимая, как все работает, вдумчиво тестируя и не плодя багов при внесении в код изменений. Разумеется, все это в условиях авралов и дэдлайнов. К тому же, вдумчивых и понимающих людей нужно еще где-то найти и убедить работать именно у вас.

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

    Варианты тупых, но рабочих решений

    На ум приходят следующие возможные варианты решений:

    1. Никогда не хранить данные в кэше дольше 1-2 минут. Тогда по любому ключу можно записать любую ерунду, через какое-то время все само себя восстановит. Плюс таймауты на чтение из кэша. Если не удалось считать, идем в базу или возвращаем ошибку, по вкусу. Такая схема должна отлично работать в вебе и подобных системах.
    2. Не использовать кэши. Использовать БД, которая умеет кэшировать горячие данные в памяти и выделить этой самой памяти побольше. Те же PostgreSQL, MySQL и MongoDB вполне это умеют. В теории должно быть хорошо. На практике строить системы совсем без кэшей я лично не пробовал. Если у кого-нибудь есть опыт, поделитесь!
    3. Какая-нибудь Lambda или Kappa архитектура, в которых все события пишутся только в лог, из которого потом фактически строиться materialized view. Увы, этот подход довльно трудно внедрить, когда проект разрабатывается не с нуля.

    Если есть варианты получше, предлагайте.

    Вердикт

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

    Важный нюанс. При записи в базу никогда не используйте данные, прочитанные из кэша. Иначе получим мусор и все разъедется. База — это истина в последней инстанции. Кэш представляет собой просто «слепок», возможно, потерявший актуальность. Использовать его нужно с осторожностью и исключительно там, где действительно нужна скорость, а не где попало.

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

    Дополнение: Как я поднимал Couchbase-кластер в Амазоне

    Метки: Разработка, Философия.

    Введение в недействительность кэша — документация FOSHttpCache

    Проблема

    HTTP-кеширование

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

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

    Альтернативы

    Есть три альтернативы аннулированию кеша.

    1. Во-первых, истекает кэшируемый контент быстро, сокращая его время до жить (TTL). Однако короткие TTL вызывают более высокую нагрузку на приложение. потому что контент нужно извлекать из него чаще.Более того, уменьшен TTL не гарантирует, что у клиентов будет свежий контент, особенно если содержание меняется очень быстро в результате взаимодействия клиента с заявление.
    2. Вторая альтернатива — проверить актуальность кэшированного содержимого на каждый запрос. Опять же, это означает большую нагрузку на ваше приложение, даже если вы возвратиться раньше (например, с помощью запросов HEAD).
    3. В крайнем случае, если не кэширует изменчивое содержимое вообще. Пока это гарантирует, что пользователь всегда видит изменения без промедления, очевидно еще больше увеличивает нагрузку на ваше приложение.

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

    кэширование — кеш — очистить и аннулировать операцию

    Пожалуйста, подтвердите правильность моего понимания выше?

    В целом вы совершенно правы, но есть камни, которые могут споткнуться.Вы не указали HW-платформу. Если мы не говорим о небольших встроенных контроллерах, упакованных с SRAM, учтите следующее. Процессоры, упакованные с MMU, поддерживают различные атрибуты памяти для обычной памяти DDR и памяти драйверов (связанной с аппаратным обеспечением). Последний не кэшируется, поэтому не нужно беспокоиться о сбросе / аннулировании.


    Когда вы хотите объединить и очистку, и аннулирование? я слышал, что во время игры с дескрипторами управления устройством нам нужно синхронизировать комбинируя промывку и недействительность.Почему так?

    Поскольку DMA упоминается в тегах, существует несколько сценариев (при условии, что буфер HW не является кэшируемой памятью устройства):

    1. DMA передает данные из памяти DDR в аппаратный буфер.
    2. DMA передает данные из HW-буфера в DDR (HW получил данные и хочет сделать их доступными для CPU)
    3. Перенос DMA из DDR в другую область DDR.

    1. Буфер DDR должен быть сброшен перед DMA. Буфер драйвера не кэшируется, поэтому нет необходимости делать его недействительным.
    2. Буфер DDR должен быть признан недействительным до или после (подробности см. ПРИМЕЧАНИЕ ниже) передачи DMA, чтобы ЦП не использовал «старые» данные из кеша. Очистка HW-буфера избыточна.
    3. «Исходный» буфер должен быть очищен, «Целевой» буфер должен быть признан недействительным. Таким образом, действительные данные находятся в памяти для DMA перед передачей, а ЦП не забирает «грязь» из кеша после того, как DMA выполнил свою работу.


    ПРИМЕЧАНИЕ : Очевидно, что «источник» должен быть сброшен перед DMAing.Тем не менее, остается вопрос, когда аннулирует . Технически это до того, как ЦП попытается получить доступ к данным «Назначения», и это может быть до или после DMA (мы должны убедиться, что DMA завершил работу). IRL, аннулирующий после DMAing, может привести к проблеме. Ссылаться на Очистить / аннулировать диапазон по виртуальному адресу; ARMv8; Кэш;

    Как вы могли заметить, аннулирование для этой конкретной платформы должно быть выполнено перед DMAing. Также путаница кода BSP для устройства ARMv7 я нашел рекомендацию аннулировать целевой буфер перед передачей DMA.
    Но это еще не все. Возможно, вы захотите сделать недействительным буфер назначения после передачи DMA снова (правильно, во второй раз). Если рассматриваемый чип имеет предварительную выборку, он может загружать данные обратно в кеш во время работы DMA. Таким образом, последовательность может быть следующей: после первого аннулирования предварительной выборки снова поместить данные в кеш -> данные переопределения DMA в памяти -> кеш содержит данные, отличные от памяти, и кеш помечается как имеющий действительные данные. Второй аннулирует гарантирует, что данные будут снова извлечены в кеш, поэтому кеш и память будут синхронизированы 🙂


    Нужно ли нам следовать такой последовательности, как очистка с последующим аннулированием?

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


    Есть ли сценарий, в котором будет полезно выполнить аннулирование с последующим сбросом?

    Не думаю.

    Кэширование

    — Стратегия аннулирования кеша

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

    У вас есть два варианта, когда происходит ОБНОВЛЕНИЕ:

    1. Вы можете попробовать установить новое значение во время операции обновления или
    2. Просто удалите старую и обновите во время операции чтения .

    Если вам нужен кеш LRU , то UPDATE может просто удалить старое значение, и при первом извлечении объекта вы создадите его снова после чтения из реальной базы данных. Однако, если вы знаете, что ваш кеш очень мал, и вы используете другую основную базу данных для проблем, отличных от размера данных, вы можете обновить его непосредственно во время ОБНОВЛЕНИЯ.

    Однако всего этого недостаточно, чтобы быть полностью последовательным.
    Когда вы пишете в свою БД, кеш Redis может быть недоступен, например, в течение нескольких секунд, поэтому данные между ними не синхронизируются.
    Что делать в таком случае?
    Есть несколько опций, которые вы можете использовать одновременно.

    1. В любом случае установите TTL, чтобы в конечном итоге обновлять поврежденные данные.
    2. Использовать восстановление с отложенным чтением. Когда вы читаете из БД, время от времени проверяйте с первичным, совпадает ли значение. Если нет, обновите кешированный элемент (или удалите его).
    3. Используйте эпохи или аналогичные способы доступа к своим данным. Не всегда возможно, однако иногда вы получаете доступ к кэшированным данным о данном объекте.По возможности вы можете изменять идентификатор / дескриптор объекта каждый раз, когда вы его изменяете, так что вы не сможете получить доступ к устаревшим данным в кэше: каждое имя ключа относится к определенной версии вашего объекта .

    Таким образом, del-cache-on-update и write-cache-on-read являются базовой стратегией, но вы можете использовать другие дополнительные системы, чтобы в конечном итоге исправить несоответствия.

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

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

    Аннулирование кеша — Настройка и обслуживание Sitefinity CMS

    Обзор

    Когда данные хранятся в кэше Sitefinity CMS с использованием встроенного CacheManager, существует механизм, делающий кеш недействительным, когда элементы были обновлены или прошло заданное время.Для этого вам нужно передать объекты ICacheItemExpiration при добавлении элементов в кеш.

    ПРИМЕР :

    Вы можете использовать один из встроенных типов ICacheItemExpiration — AbsoluteTime, SlidingTime или DataItemCacheDependency, или вы можете реализовать собственный тип ICacheItemExpiration.

    AbsoluteTime и SlidingTime

    Эти два типа ICacheItemExpiration реализуют истечение срока действия на основе времени.

    • Срок действия AbsoluteTime устанавливает, как долго данные будут оставаться в кеше после добавления.

      ПРИМЕР :


    • SlidingTime аналогичен AbsoluteTime, но всякий раз, когда осуществляется доступ к кэшированным элементам, время истечения срока действия сбрасывается до заданного значения. Это используется для хранения элементов, которые часто используются в кэше, и отбрасывания элементов, которые не используются.

      ПРИМЕР :

    ПРИМЕЧАНИЕ : Если к кэшируемым элементам обращаются часто и для истечения срока действия кэша используется только скользящее время, может пройти много времени, прежде чем кеш станет недействительным.Рекомендуется использовать скользящее истечение времени в сочетании с DataItemCacheDependency.

    DataItemCacheDependency

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

    Рекомендуется использовать DataItemCacheDependency в сочетании с объектами истечения срока действия SlidingTime или AbsoluteTime.

    DataItemCacheDependency может быть создан с использованием одной из доступных перегрузок конструктора:

    • DataItemCacheDependency (IDataItem trackedItem)
      Кэш становится недействительным при обновлении или удалении определенного элемента.
    • DataItemCacheDependency (Type trackedItemType, Guid trackedItemId)
      Кэш становится недействительным, когда создается, обновляется или удаляется элемент определенного типа и с определенным идентификатором.
    • DataItemCacheDependency (Тип trackedItemType, строковый ключ)
      Кэш становится недействительным, когда создается, обновляется или удаляется элемент определенного типа с предоставленным настраиваемым ключом.

    Под капотом DataItemCacheDependency работает с использованием шаблона подписка — уведомление. Когда некоторые объекты добавляются в кеш с использованием истечения срока действия DataItemCacheDependency, кеш подписывается на уведомления для данного типа и данного ключа. Затем, в какой-то момент, когда элементы обновляются, запускается уведомление с использованием CacheDependency.Notify (), который делает кеш недействительным. Для встроенных типов контента и динамических типов контента, созданных с помощью построителя модулей, при создании, обновлении или удалении элемента Sitefinity CMS отправляет уведомления со следующими ключами:

    • ключ = ноль
    • ключ = itemId
    • ключ = item.Provider.ApplicationName
    • ключ = itemStatus + разделитель + itemId
    • ключ = itemStatus + разделитель + item.Provider.ApplicationName

    ПРИМЕЧАНИЕ : В настройке NLB уведомления распространяются на все узлы.Таким образом, когда элемент создается, обновляется или удаляется на данном узле, кеш становится недействительным на всех узлах.

    Сделать кеш недействительным при обновлении определенного элемента

    Когда кэшируется один элемент, вы можете отслеживать изменения только для этого элемента. Для этого вы создаете экземпляр DataItemCacheDependency с типом элемента и идентификатором элемента. Идентификатор элемента может быть передан как в виде GUID, так и в виде строки.

    ПРИМЕР :

    Сделать кеш недействительным при обновлении любого элемента данного типа

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

    ПРИМЕР :

    Сделать кеш недействительным при обновлении опубликованных элементов

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

    ПРИМЕР :

    Сделать недействительным кеш для настраиваемого события

    Ключ в DataItemCacheDependency может быть любой строкой. Это означает, что можно создать специальный ключ и использовать его для аннулирования кеша при возникновении определенного события.Чтобы сделать кеш недействительным по заданному ключу, вам необходимо вызвать CacheDependency.Notify (). Обычно вы подписываетесь на событие IDataItem и оттуда уведомляете настраиваемый ключ. Для получения дополнительной информации см. IDataEvent.

    ПРИМЕР :

    Аннулирование кеш-памяти в OutSystems 11

    1. Последнее обновление
    2. Сохранить как PDF
    1. Как это работает?
    2. См. Также

    Шаблон: OutSystems / Documentation_KB / ContentCollaboration

  • Редактировать
  • Кэширование позволяет приложениям временно хранить подмножество данных, делая запросы на одни и те же данные быстрее, потому что чтение данных из кеша происходит быстрее, чем повторное чтение тех же данных из базы данных.Аннулирование кэша — это процесс, при котором записи в кэше заменяются или удаляются.

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

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

    Как это работает?

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

    Эта служба использует шаблон публикации / подписки: приложения подписываются на очереди, которые представляют модули и клиентов, из которых могут поступать данные.Базовая технология, используемая службой аннулирования кэша, называется RabbitMQ , службой распределенной очереди сообщений, которая реализует шаблон публикации / подписки.

    Необходимо настроить платформу, чтобы приложения могли обращаться к этой службе. Вы можете установить и настроить экземпляр RabbitMQ с помощью OutSystems Configuration Tool. Выполнение этой операции устанавливает RabbitMQ на том же компьютере, на котором запущен Инструмент настройки. Проверьте контрольный список установки Platform Server 11 для получения дополнительной информации о том, как установить и настроить службу недействительности кэша.

    Каждый раз, когда происходит аннулирование кеша для модуля или клиента, все приложения, отслеживающие изменения этих элементов, получают уведомление и помечают свои локальные копии как грязные. Когда служба недействительности кэша не работает или недоступна (например, во время изменения конфигурации этой службы), приложения не уведомляются о каких-либо случаях недействительности кеша, которые могли произойти. Это может вызвать некоторую несогласованность действий до тех пор, пока эти приложения снова не смогут связаться со службой или пока не будут применены новые конфигурации службы.Чтобы повысить доступность службы аннулирования кэша, вы можете настроить кластер RabbitMQ и использовать балансировщик нагрузки TCP.

    Недействительность кеша (Symfony Docs)

    «В информатике есть только две сложные вещи: инвалидация кеша. и называть вещи «. — Фил Карлтон

    После того, как URL-адрес кэшируется кешем шлюза, кеш не будет запрашивать приложение для этого контента больше. Это позволяет кешу быстро предоставлять ответов и снижает нагрузку на ваше приложение.Однако вы рискуете доставка устаревшего контента. Выход из этой дилеммы — использовать длинные время жизни кеша, но чтобы активно уведомлять кеш шлюза, когда содержимое изменения. Обратные прокси-серверы обычно предоставляют канал для получения таких уведомления, обычно через специальные HTTP-запросы.

    Осторожность

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

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

    Однако иногда требуется дополнительная производительность, которую можно получить, когда явно аннулирует. Для признания недействительным ваше приложение должно обнаруживать при изменении содержимого и сообщить кешу, чтобы он удалил URL-адреса, содержащие эти данные из своего кеша.

    Кончик

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

    Если один контент соответствует одному URL, модель PURGE работает хорошо. Вы отправляете запрос к прокси-серверу с помощью HTTP-метода PURGE (используя слово «PURGE» является условным, технически это может быть любая строка) вместо из ПОЛУЧИТЕ и заставьте прокси-сервер кеша обнаружить это и удалить данные из кеш вместо обращения к приложению для получения ответа.

    Вот как вы можете настроить обратный прокси Symfony (см. HTTP Cache) для поддержки метода HTTP PURGE :

    Осторожность

    Вы должны каким-то образом защитить HTTP-метод PURGE , чтобы избежать случайных людей очистка ваших кэшированных данных.

    Purge предписывает кешу удалить ресурс в во всех его вариантах (в соответствии с заголовком Vary , см. Изменение ответа для HTTP-кеша).Альтернативой продувке является обновление содержимого. Обновление означает, что прокси-сервер кеширования проинструктирован удалить его локальный кеш и снова получить содержимое. Сюда, новый контент уже доступен в кеше. Недостаток освежения в том, что варианты не считаются недействительными.

    Во многих приложениях один и тот же бит содержимого используется на разных страницах с разные URL. Для таких случаев существуют более гибкие концепции:

    • Запрет делает недействительными ответы, соответствующие регулярным выражениям на URL или другие критерии;
    • Cache tagging позволяет добавлять теги для каждого содержимого, используемого в ответе. чтобы вы могли аннулировать все URL-адреса, содержащие определенный контент.

    Больше никогда не аннулируйте кэш Amazon CloudFront

    Последнее обновление 25 февраля 2020 г.

    Один из наиболее частых вопросов, которые мы получаем в поддержку WP Offload Media, — это что-то вроде…

    Почему в моих URL-адресах много случайных чисел и как их удалить?

    Короткий ответ заключается в том, что настройка управления версиями объектов в WP Offload Media включена по умолчанию.

    Однако это только вызывает вопросы; «Почему?», «Что делать, если мне не нужны эти лишние числа в своих URL?» И «Безопасно ли отключать управление версиями объектов?»

    Давайте ответим на эти вопросы.

    Что такое управление версиями объектов?

    Когда WP Offload Media выгружает элемент библиотеки мультимедиа в корзину поставщика хранилища, он создает «ключ» (также известный как путь) для файлов с использованием различных настроек.

    Очевидные настройки — «Путь» и «Год / Месяц».

    Они могут дать нам что-то вроде «wp-content / uploads /» и «2020/01 /» соответственно, что означает файл под названием «puppies».jpg », загруженный в медиатеку 29 января 2020 года, может оказаться в корзине с таким ключом:

      wp-content / uploads / 2020/01 / puppies.jpg
      

    Однако, если включено управление версиями объектов, этот ключ может иметь следующий вид:

      wp-content / uploads / 2020/01/29173059 / puppies.jpg
      

    Этот дополнительный сегмент «29173059» в ключе файла исходит из настройки управления версиями объектов WP Offload Media. Это просто отметка времени, в данном случае мы можем сказать, что это щенков.jpg был выгружен из в корзину 29-го дня (января) в 17:30 и 59 секунд.

    Если этот файл затем был обслужен через CDN, такой как Amazon CloudFront, с использованием личного домена (CNAME) «cdn.example.com», конечный URL-адрес, отображаемый на странице для этого изображения, может быть:

      https://cdn.example.com/wp-content/uploads/2020/01/29173059/puppies.jpg
      

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

      wp-content / uploads / 2020/01/04132435 / puppies.jpg
      

    Теперь версия объекта — «04132435», она изменилась, потому что WP Offload Media генерирует новую версию при выгрузке файла, чтобы гарантировать уникальность ключа объекта для повторной выгрузки этого файла.

    Теперь URL изображения:

    .
      https://cdn.example.com/wp-content/uploads/2020/01/04132435/puppies.jpg
      

    Поскольку WP Offload Media заботится о перезаписи локальных URL-адресов для выгруженных элементов библиотеки мультимедиа, когда посетитель сайта просматривает страницу, ничего не ломается, посетитель видит обновленных щенков .jpg изображение, не подозревая, что оно было перемещено на другой путь в корзине.

    Зачем использовать управление версиями объектов?

    Почему мы делаем это по умолчанию? Зачем возиться с URL-адресом выгруженного элемента медиатеки? Почему наш обновленный файл puppies.jpg нельзя выгружать каждый раз по одному и тому же пути?

    Сети доставки контента (CDN) , вот почему!

    Когда вы используете CDN, например Amazon CloudFront, Cloudflare или KeyCDN, они кэшируют файлы, которые они обслуживают, в течение определенного периода времени, обычно от одного дня до года, и не обновляют кеш, пока это время не истечет.

    Допустим, вы загружаете фотографию симпатичного щенка с ключом wp-content / uploads / 2020/01 / puppies.jpg , публикуете, и люди посещают вашу запись. Но вскоре после этого вы найдете еще более симпатичное фото щенка. Вы загружаете новый с помощью плагина Enable Media Replace, чтобы изображение сохранило тот же ключ wp-content / uploads / 2020/01 / puppies.jpg . Поскольку CDN уже кэшировал первого менее симпатичного щенка, он продолжит обслуживать этого щенка. Посетители вашего сайта могут не видеть симпатичного щенка целый год!

    Если у вас включено управление версиями объектов, замените эти щенков.jpg photo, WP Offload Media генерирует новый ключ для файла, который нужно выгрузить, начинает использовать этот новый путь во всем содержимом и удаляет старую, менее симпатичную фотографию щенков из корзины.

    Поскольку управление версиями объектов WP Offload Media генерирует новый ключ для выгруженного файла при его повторной выгрузке, сеть CDN видит его как новый файл и помещает в свои кэши пограничного сервера.

    Amazon CloudFront рекомендует метод предотвращения недействительности кеша:

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

    Зачем нам все равно нужно обновлять выгруженный файл?

    Существует множество причин, по которым вам может потребоваться обновить выгруженный элемент библиотеки мультимедиа и, следовательно, эти пограничные серверы CDN должны заметить новый файл и начать его обслуживание.

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

    Вы заметили, что была загружена целая куча изображений, которые не были оптимизированы для показа в Интернете.

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

    Этот встроенный отчет в формате PDF необходимо заменить, чтобы устранить проблему с его форматированием.

    Всегда есть еще милые щенки!

    В любом случае, после того, как вы использовали что-то вроде плагинов Regenerate Thumbnails, EWWW Image Optimizer или Enable Media Replace для исправления ваших медиа, вам понадобится CDN для повторного кеширования и обслуживания недавно выгруженных файлов.

    Безопасно ли отключать управление версиями объектов?

    Что ж, если вы не используете CDN, думаю, с вами все в порядке. Но, честно говоря, вам действительно стоит использовать CDN.

    А что с кешем браузера? Если у вас есть постоянные посетители и вы обновляете важное изображение, которое вы хотите, чтобы они увидели изменения при возвращении, управление версиями объектов поможет в этом. WordPress имеет встроенный параметр? Ver = x.y.z, который он добавляет к конечным продуктам, таким как файлы CSS и JS, но он не распространяется на медиа в темах и плагинах, не говоря уже о контенте.

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

    Есть ли альтернатива?

    Серьезно? Альтернативы нет?

    Не совсем.

    Вы можете использовать такую ​​схему, как предлагает CloudFront, для обновления существующего контента с использованием тех же имен файлов, и просто установить довольно короткое максимальное время жизни в распределении CDN.Однако проблема в том, что объектам присваивается заголовок Cache-Control , равный одному году, и CloudFront будет уважать этот заголовок, полагая, что объект на этом пути годен в течение года, прежде чем его следует повторно получить.

    Вы можете настроить заголовок Cache-Control на что-то гораздо более короткое с помощью фильтра «as3cf_object_meta» в WP Offload Media, переопределив значение $ args ['CacheControl'] . Однако, используя S3 / CloudFront в качестве нашего примера, поскольку каждый пограничный сервер CloudFront, расположенный в разных регионах, теперь должен повторно получать любой запрошенный объект чаще, вы понесете гораздо более высокие затраты на запросы S3, даже если нет затрат на передачу данных на CloudFront.

    У вас также больше нет контроля над повторной выборкой объекта, это зависит исключительно от интервала, в течение которого CloudFront выполняет свои проверки, тогда как с версией объектов новый объект должен быть повторно кэширован и обслуживаться по первому запросу каждым краем CDN. сервер.

    Я мог бы предположить, что можно создать какой-то запутанный механизм, который использует фильтр as3cf_post_upload_attachment , чтобы вызвать недействительность пути к файлам в распределении CDN. Но это относительно медленно и быстро может стать дорогостоящим, поскольку CloudFront дает вам всего 1000 бесплатных аннулирований в месяц, а затем взимает 0 долларов США.005 на запрошенный путь аннулирования.

    Конечно, вы можете вручную аннулировать любой путь в распределении CDN через консоль провайдера. В нашем документе о том, как исправить проблемы CORS с веб-шрифтами, мы показываем, как сделать недействительным весь дистрибутив Amazon CloudFront с путем «/ », но вы можете сделать недействительным только «/ wp-content / uploads / 2018/», если вы я только что перезагрузил некоторые элементы медиатеки с 2018 года.

    Кроме того, совершенно очевидно, что CloudFront рекомендует управление версиями объектов, а не недействительность…

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

    Сводка

    Как видите, самый простой и дешевый способ убедиться, что ваши самые последние и самые лучшие файлы как можно быстрее обслуживаются через CDN, — это включить параметр управления версиями объектов WP Offload Media.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *