Unieważnianie pamięci podręcznej Rails na poziomie pola: rozwiązanie DSL

Opublikowany: 2022-03-11

W nowoczesnym tworzeniu stron internetowych buforowanie jest szybkim i potężnym sposobem na przyspieszenie działania. Prawidłowo wykonane buforowanie może znacznie poprawić ogólną wydajność aplikacji. Jeśli zrobisz to źle, z całą pewnością skończy się to katastrofą.

Unieważnienie pamięci podręcznej, jak być może wiesz, jest jednym z trzech najtrudniejszych problemów w informatyce — pozostałe dwa to nazywanie rzeczy i błędy pojedyncze. Jednym łatwym wyjściem jest unieważnienie wszystkiego, z lewej i prawej strony, gdy coś się zmieni. Ale to niweczy cel buforowania. Chcesz unieważnić pamięć podręczną tylko wtedy, gdy jest to absolutnie konieczne.

Jeśli chcesz jak najlepiej wykorzystać buforowanie, musisz być bardzo ostrożny w odniesieniu do tego, co unieważniasz i uchronić aplikację przed marnowaniem cennych zasobów na powtarzającą się pracę.

Unieważnianie pamięci podręcznej Railsów na poziomie pola

W tym wpisie na blogu poznasz technikę lepszej kontroli nad zachowaniem pamięci podręcznej Railsów: w szczególności implementację unieważniania pamięci podręcznej na poziomie pola. Ta technika opiera się na Rails ActiveRecord i ActiveSupport::Concern , a także na manipulowaniu zachowaniem metody touch .

Ten wpis na blogu jest oparty na moich ostatnich doświadczeniach w projekcie, w którym zauważyliśmy znaczną poprawę wydajności po wdrożeniu unieważniania pamięci podręcznej na poziomie pola. Pomogło to zredukować niepotrzebne unieważnienia pamięci podręcznej i wielokrotne renderowanie szablonów.

Szyny, Ruby i Wydajność

Ruby nie jest najszybszym językiem, ale ogólnie jest odpowiednią opcją, jeśli chodzi o szybkość rozwoju. Co więcej, jego metaprogramowanie i wbudowane możliwości języka specyficznego dla domeny (DSL) zapewniają programiście ogromną elastyczność.

Istnieją badania, takie jak badanie Jakoba Nielsena, które pokazują nam, że jeśli zadanie zajmie więcej niż 10 sekund, stracimy koncentrację. A odzyskanie koncentracji wymaga czasu. Więc może to być nieoczekiwanie kosztowne.

Niestety, w Ruby on Rails bardzo łatwo jest przekroczyć ten 10-sekundowy próg przy generowaniu szablonu. Nie zobaczysz tego w żadnej aplikacji „hello world” lub małym projekcie dla zwierząt, ale w rzeczywistych projektach, w których wiele rzeczy jest ładowanych na jedną stronę, uwierz mi, generowanie szablonów może bardzo łatwo zacząć się przeciągać.

I to jest dokładnie to, co musiałem rozwiązać w swoim projekcie.

Proste optymalizacje

Ale jak dokładnie to przyspieszyć?

Odpowiedź: Benchmark i optymalizacja.

W moim projekcie dwa bardzo skuteczne kroki optymalizacji to:

  • Eliminacja zapytań N+1
  • Przedstawiamy dobrą technikę buforowania szablonów

Zapytania N+1

Naprawianie zapytań N+1 jest łatwe. To, co możesz zrobić, to sprawdzić pliki dziennika — za każdym razem, gdy zobaczysz w swoich dziennikach wiele zapytań SQL, takich jak te poniżej, wyeliminuj je, zastępując je szybkim ładowaniem:

 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' = ?

Jest w tym klejnot, który nazywa się pociskiem, aby pomóc wykryć tę nieefektywność. Możesz także przejść przez każdy przypadek użycia, a w międzyczasie sprawdzić logi, porównując je z powyższym wzorcem. Eliminując wszystkie nieefektywności N+1, możesz mieć pewność, że nie przeciążysz swojej bazy danych, a czas spędzony na ActiveRecord znacznie się zmniejszy.

Po wprowadzeniu tych zmian mój projekt działał już szybciej. Ale postanowiłem przenieść to na wyższy poziom i zobaczyć, czy uda mi się jeszcze bardziej skrócić czas ładowania. W szablonach wciąż było sporo niepotrzebnego renderowania i ostatecznie właśnie tam pomogło buforowanie fragmentów.

Buforowanie fragmentów

Buforowanie fragmentaryczne ogólnie pomaga znacznie skrócić czas generowania szablonu. Ale domyślne zachowanie pamięci podręcznej Railsów nie ograniczało jej dla mojego projektu.

Idea buforowania fragmentów Railsów jest genialna. Zapewnia super prosty i skuteczny mechanizm buforowania.

Autorzy Ruby On Rails napisali bardzo dobry artykuł w Signal v. Noise o tym, jak działa buforowanie fragmentów.

Załóżmy, że masz trochę interfejsu użytkownika, który pokazuje niektóre pola encji.

  • Podczas ładowania strony Railsy obliczają cache_key na podstawie klasy jednostki i pola updated_at .
  • Używając tego cache_key , sprawdza, czy jest coś w pamięci podręcznej powiązanej z tym kluczem.
  • Jeśli w pamięci podręcznej nic nie ma, kod HTML dla tego fragmentu jest renderowany dla widoku (a nowo wyrenderowana zawartość jest przechowywana w pamięci podręcznej).
  • Jeśli w pamięci podręcznej istnieje jakakolwiek zawartość z tym kluczem, widok jest renderowany z zawartością pamięci podręcznej.

Oznacza to, że pamięć podręczna nigdy nie musi być jawnie unieważniana. Za każdym razem, gdy zmieniamy encję i ponownie ładujemy stronę, nowa zawartość pamięci podręcznej jest renderowana dla encji.

Railsy domyślnie oferują również możliwość unieważnienia pamięci podręcznej jednostek nadrzędnych w przypadku zmiany dziecka:

 belongs_to :parent_entity, touch: true

To, gdy jest włączone do modelu, automatycznie dotknie rodzica, gdy dziecko zostanie dotknięte . Więcej o touch dowiesz się tutaj. Dzięki temu Railsy zapewniają nam prosty i skuteczny sposób unieważniania pamięci podręcznej dla naszych encji nadrzędnych jednocześnie z pamięcią podręczną dla encji potomnych.

Buforowanie w Rails

Jednak buforowanie w Railsach jest tworzone w celu obsługi interfejsów użytkownika, w których fragment HTML reprezentujący encję rodzica zawiera fragmenty HTML reprezentujące wyłącznie encje podrzędne rodzica. Innymi słowy, fragment HTML reprezentujący encje podrzędne w tym paradygmacie nie może zawierać pól z encji nadrzędnej.

Ale to nie dzieje się w prawdziwym świecie. Bardzo możliwe, że będziesz musiał robić w swojej aplikacji Railsowe rzeczy, które naruszają ten warunek.

Jak poradziłbyś sobie w sytuacji, gdy interfejs użytkownika pokazuje pola encji nadrzędnej wewnątrz fragmentu HTML reprezentującego encję podrzędną?

Fragmenty dla encji podrzędnych odnoszące się do pól encji nadrzędnych

Jeśli dziecko zawiera pola z encji rodzicielskiej, masz problem z domyślnym zachowaniem Railsów do unieważniania pamięci podręcznej.

Za każdym razem, gdy te pola prezentowane z encji nadrzędnej są modyfikowane, będziesz musiał dotknąć wszystkich encji podrzędnych należących do tego rodzica. Na przykład, jeśli Parent1 zostanie zmodyfikowany, będziesz musiał upewnić się, że pamięć podręczna dla widoków Child1 i Child2 jest unieważniona.

Oczywiście może to spowodować ogromne wąskie gardło wydajności. Dotykanie każdej jednostki podrzędnej za każdym razem, gdy zmieni się rodzic, spowodowałoby wiele zapytań do bazy danych bez ważnego powodu.

Innym podobnym scenariuszem jest sytuacja, w której jednostki powiązane z powiązaniem has_and_belongs_to zostały przedstawione na liście, a modyfikacja tych jednostek rozpoczęła kaskadę unieważniania pamięci podręcznej poprzez łańcuch powiązań.

Stowarzyszenie „Ma i Należy”

 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

Tak więc w przypadku powyższego interfejsu użytkownika nielogiczne byłoby dotykanie uczestnika lub zdarzenia, gdy zmienia się lokalizacja użytkownika. Ale powinniśmy dotknąć zarówno wydarzenia, jak i uczestnika, gdy zmienia się nazwa użytkownika, czyż nie?

Tak więc techniki opisane w artykule Signal v. Noise są nieefektywne w przypadku niektórych instancji UI/UX, jak opisano powyżej.

Chociaż Railsy są bardzo skuteczne w prostych rzeczach, prawdziwe projekty mają swoje własne komplikacje.

Unieważnienie pamięci podręcznej Railsów na poziomie pola

W moich projektach używałem małego Ruby DSL do obsługi sytuacji takich jak powyżej. Umożliwia deklaratywne określenie pól, które będą powodować unieważnianie pamięci podręcznej poprzez asocjacje.

Rzućmy okiem na kilka przykładów, w których to naprawdę pomaga:

Przykład 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

Ten fragment kodu wykorzystuje możliwości metaprogramowania i wewnętrzne możliwości DSL Rubiego.

Mówiąc dokładniej, tylko zmiana nazwy w zdarzeniu spowoduje unieważnienie pamięci podręcznej fragmentów powiązanych z nią zadań. Zmiana innych pól zdarzenia — takich jak cel lub lokalizacja — nie spowoduje unieważnienia pamięci podręcznej fragmentów zadania. Nazwałbym to drobnoziarnistą kontrolą unieważniania pamięci podręcznej na poziomie pola .

Fragment dla encji zdarzenia z tylko polem nazwy

Przykład 2:

Rzućmy okiem na przykład, który pokazuje unieważnianie pamięci podręcznej poprzez łańcuch asocjacji has_many .

Poniższy fragment interfejsu użytkownika pokazuje zadanie i jego właściciela:

Fragment encji zdarzenia z nazwą właściciela zdarzenia

W przypadku tego interfejsu użytkownika fragment HTML reprezentujący zadanie powinien zostać unieważniony tylko w przypadku zmiany zadania lub zmiany nazwy właściciela. Jeśli wszystkie inne pola właściciela (takie jak strefa czasowa lub preferencje) ulegną zmianie, pamięć podręczna zadań powinna pozostać nienaruszona.

Osiąga się to za pomocą pokazanego tutaj 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

Wdrożenie DSL

Główną istotą DSL jest metoda touch . Jego pierwszym argumentem jest asocjacja, a kolejnym argumentem jest lista pól, które wyzwalają touch tej asocjacji:

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

Tę metodę zapewnia moduł 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

W tym kodzie głównym punktem jest to, że przechowujemy argumenty wywołania touch . Następnie przed zapisaniem encji zaznaczamy powiązanie jako brudne, jeśli dane pole zostało zmodyfikowane. Dotykamy bytów w tym stowarzyszeniu po zapisaniu, jeśli stowarzyszenie było brudne.

Następnie prywatną częścią koncernu jest:

 ... 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 …

W metodzie check_touchable_entities sprawdzamy , czy zadeklarowane pole uległo zmianie . Jeśli tak, oznaczamy powiązanie jako brudne, ustawiając meta_info[association] na true .

Następnie po zapisaniu bytu sprawdzamy nasze brudne skojarzenia i w razie potrzeby dotykamy znajdujących się w nim bytów:

 … 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 …

I to jest to! Teraz możesz wykonać unieważnianie pamięci podręcznej na poziomie pola w Railsach za pomocą prostego DSL.

Wniosek

Buforowanie w Railsach obiecuje poprawę wydajności aplikacji ze względną łatwością. Jednak rzeczywiste aplikacje mogą być skomplikowane i często stwarzają wyjątkowe wyzwania. Domyślne zachowanie pamięci podręcznej Railsów działa dobrze w większości scenariuszy, ale są pewne scenariusze, w których trochę większa optymalizacja unieważniania pamięci podręcznej może zajść daleko.

Teraz, gdy wiesz, jak zaimplementować unieważnianie pamięci podręcznej na poziomie pola w Rails, możesz zapobiec niepotrzebnemu unieważnianiu pamięci podręcznej w swojej aplikacji.