Инвалидация кэша Rails на уровне поля: решение DSL

Опубликовано: 2022-03-11

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

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

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

Инвалидация кеша Rails на уровне поля

В этом сообщении блога вы узнаете, как лучше контролировать поведение кешей Rails: в частности, реализовать инвалидацию кеша на уровне поля. Этот метод основан на Rails ActiveRecord и ActiveSupport::Concern , а также на манипулировании поведением touch метода.

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

Rails, Ruby и производительность

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

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

К сожалению, в Ruby on Rails очень легко превысить этот 10-секундный порог при создании шаблона. Вы не увидите, чтобы это произошло ни в одном приложении «hello world» или мелкомасштабном любимом проекте, но в реальных проектах, где много вещей загружается на одну страницу, поверьте мне, генерация шаблонов может очень легко начать тормозить.

И это именно то, что я должен был решить в своем проекте.

Простые оптимизации

Но как именно вы ускоряете процесс?

Ответ: Сравните и оптимизируйте.

В моем проекте было два очень эффективных шага в оптимизации:

  • Устранение запросов N+1
  • Представляем хорошую технику кэширования шаблонов

N+1 запросов

Исправить запросы N+1 легко. Что вы можете сделать, так это проверить свои файлы журналов — всякий раз, когда вы видите в своих журналах несколько запросов SQL, подобных приведенным ниже, устраните их, заменив их нетерпеливой загрузкой:

 Learning Load (0.4ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ? Learning Load (0.3ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ? Learning Load (0.4ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ?

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

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

Кэширование фрагментов

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

Идея кэширования фрагментов в Rails блестящая. Он обеспечивает супер простой и эффективный механизм кэширования.

Авторы Ruby On Rails написали очень хорошую статью в Signal v. Noise о том, как работает кэширование фрагментов.

Допустим, у вас есть небольшой пользовательский интерфейс, который показывает некоторые поля объекта.

  • При загрузке страницы Rails вычисляет cache_key на основе класса объекта и поля updated_at .
  • Используя этот cache_key , он проверяет, есть ли в кеше что-либо, связанное с этим ключом.
  • Если в кеше ничего нет, то HTML-код для этого фрагмента отображается для представления (и вновь отображаемый контент сохраняется в кеше).
  • Если в кеше есть какой-либо существующий контент с этим ключом, то представление отображается с содержимым кеша.

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

Rails по умолчанию также предлагает возможность аннулировать кеш родительских сущностей в случае изменения дочерних:

 belongs_to :parent_entity, touch: true

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

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

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

Но это не то, что происходит в реальном мире. Вам вполне может понадобиться сделать что-то в своем приложении Rails, нарушающее это условие.

Как бы вы справились с ситуацией, когда пользовательский интерфейс показывает поля родительского объекта внутри фрагмента HTML, представляющего дочерний объект?

Фрагменты дочерних сущностей, ссылающихся на поля родительских сущностей

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

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

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

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

Ассоциация «Имеет и принадлежит»

 class Event < ActiveRecord::Base has_many :participants has_many :users, through: :participants end class Participant < ActiveRecord::Base belongs_to :event belongs_to :user end class User < ActiveRecord::Base has_many :participants has_many :events, through :participants end

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

Таким образом, методы, описанные в статье «Сигнал против шума», неэффективны для определенных случаев UI/UX, как описано выше.

Хотя Rails очень эффективен для простых вещей, в реальных проектах есть свои сложности.

Инвалидация кэша Rails на уровне поля

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

Давайте посмотрим на несколько примеров, где это действительно помогает:

Пример 1:

 class Event < ActiveRecord::Base include Touchable ... has_many :tasks ... touch :tasks, in_case_of_modified_fields: [:name] ... end class Task < ActiveRecord::Base belongs_to :event end

Этот фрагмент использует возможности метапрограммирования и внутренние возможности DSL Ruby.

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

Фрагмент объекта события только с полем имени

Пример 2:

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

Фрагмент пользовательского интерфейса, показанный ниже, показывает задачу и ее владельца:

Фрагмент сущности события с именем владельца события

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

Это достигается с помощью DSL, показанного здесь:

 class User < ActiveRecord::Base include Touchable touch :tasks, in_case_of_modified_fields: [:first_name, :last_name] ... end class Task < ActiveRecord::Base has_one owner, class_name: :User end

Реализация DSL

Основная суть DSL заключается в touch методе. Его первый аргумент — ассоциация, а следующий аргумент — список полей, которые инициируют touch этой ассоциации:

 touch :tasks, in_case_of_modified_fields: [:first_name, :last_name]

Этот метод обеспечивается модулем Touchable :

 module Touchable extend ActiveSupport::Concern included do before_save :check_touchable_entities after_save :touch_marked_entities end module ClassMethods def touch association, options @touchable_associations ||= {} @touchable_associations[association] = options end end end

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

Тогда частная часть концерна:

 ... private def klass_level_meta_info self.class.instance_variable_get('@touchable_associations') end def meta_info @meta_info ||= {} end def check_touchable_entities return unless klass_level_meta_info.present? klass_level_meta_info.each_pair do |association, change_triggering_fields| if any_of_the_declared_field_changed?(change_triggering_fields) meta_info[association] = true end end end def any_of_the_declared_field_changed?(options) (options[:in_case_of_modified_fields] & changes.keys.map{|x|x.to_sym}).present? end …

В методе check_touchable_entities мы проверяем , изменилось ли объявленное поле . Если это так, мы помечаем ассоциацию как грязную, устанавливая для параметра meta_info[association] значение true .

Затем, после сохранения сущности, проверяем наши грязные ассоциации и при необходимости трогаем сущности в ней:

 … def touch_marked_entities return unless klass_level_meta_info.present? klass_level_meta_info.each_key do |association_key| if meta_info[association_key] association = send(association_key) association.update_all(updated_at: Time.zone.now) meta_info[association_key] = false end end end …

Вот и все! Теперь вы можете выполнить инвалидацию кеша на уровне поля в Rails с помощью простого DSL.

Заключение

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

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