フィールドレベルのRailsキャッシュの無効化:DSLソリューション

公開: 2022-03-11

最新のWeb開発では、キャッシングは物事をスピードアップするための迅速で強力な方法です。 キャッシングを正しく実行すると、アプリケーションの全体的なパフォーマンスが大幅に向上します。 間違って行われると、間違いなく災害に終わります。

ご存知かもしれませんが、キャッシュの無効化は、コンピュータサイエンスで最も難しい3つの問題の1つです。他の2つは、名前の付け方と1つずつのエラーです。 簡単な方法の1つは、何かが変更されたときに、左右のすべてを無効にすることです。 しかし、それはキャッシングの目的を無効にします。 どうしても必要な場合にのみキャッシュを無効にします。

キャッシュを最大限に活用したい場合は、無効にする内容に細心の注意を払い、繰り返しの作業で貴重なリソースを浪費しないようにアプリケーションを保存する必要があります。

フィールドレベルのRailsキャッシュの無効化

このブログ投稿では、Railsキャッシュの動作をより適切に制御するための手法、具体的には、フィールドレベルのキャッシュ無効化の実装について学習します。 この手法は、RailsActiveRecordとActiveSupport::Concern 、およびtouchメソッドの動作の操作に依存しています。

このブログ投稿は、フィールドレベルのキャッシュ無効化を実装した後、パフォーマンスが大幅に向上したプロジェクトでの最近の経験に基づいています。 これは、不要なキャッシュの無効化とテンプレートの繰り返しのレンダリングを減らすのに役立ちました。

Rails、Ruby、およびパフォーマンス

Rubyは最速の言語ではありませんが、全体として、開発速度が懸念される場合に適したオプションです。 さらに、そのメタプログラミングと組み込みのドメイン固有言語(DSL)機能により、開発者は非常に柔軟に対応できます。

ヤコブ・ニールセンの研究のように、タスクに10秒以上かかると、焦点が失われることを示す研究があります。 そして、焦点を取り戻すには時間がかかります。 したがって、これは予想外にコストがかかる可能性があります。

残念ながら、Ruby on Railsでは、テンプレート生成でその10秒のしきい値を超えるのは非常に簡単です。 「HelloWorld」アプリや小規模なペットプロジェクトではこれが発生することはありませんが、多くのものが1つのページに読み込まれる実際のプロジェクトでは、テンプレートの生成が非常に簡単にドラッグを開始する可能性があります。

そして、それはまさに私が私のプロジェクトで解決しなければならなかったことです。

単純な最適化

しかし、どのように正確に物事をスピードアップしますか?

答え:ベンチマークと最適化。

私のプロジェクトでは、最適化における2つの非常に効果的なステップは次のとおりです。

  • 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の作者は、フラグメントキャッシングがどのように機能するかについてSignalv。Noiseに非常に優れた記事を書いています。

エンティティのいくつかのフィールドを表示するユーザーインターフェイスが少しあるとしましょう。

  • ページの読み込み時に、Railsはエンティティのクラスとupdated_atフィールドに基づいてcache_keyを計算します。
  • その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

したがって、上記のユーザーインターフェイスの場合、ユーザーの場所が変更されたときに参加者またはイベントに触れるのは非論理的です。 しかし、ユーザーの名前が変わったら、イベントと参加者の両方に触れる必要がありますね。

したがって、Signal v。Noiseの記事の手法は、上記のように、特定のUI/UXインスタンスでは非効率的です。

Railsは単純なことには非常に効果的ですが、実際のプロジェクトには独自の複雑さがあります。

フィールドレベルのRailsキャッシュの無効化

私のプロジェクトでは、上記のような状況を処理するために小さなRubyDSLを使用しています。 これにより、関連付けを通じてキャッシュの無効化をトリガーするフィールドを宣言的に指定できます。

それが本当に役立つ場所のいくつかの例を見てみましょう:

例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

このスニペットは、Rubyのメタプログラミング機能と内部DSL機能を活用しています。

具体的には、イベントの名前を変更するだけで、関連するタスクのフラグメントキャッシュが無効になります。 目的や場所など、イベントの他のフィールドを変更しても、タスクのフラグメントキャッシュは無効になりません。 これをフィールドレベルのきめ細かいキャッシュ無効化制御と呼びます。

名前フィールドのみを持つイベントエンティティのフラグメント

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

そして、それだけです! これで、単純なDSLを使用してRailsでフィールドレベルのキャッシュ無効化を実行できます。

結論

Railsキャッシングは、アプリケーションのパフォーマンスを比較的簡単に向上させることを約束します。 ただし、実際のアプリケーションは複雑になる可能性があり、多くの場合、固有の課題が発生します。 デフォルトのRailsキャッシュの動作はほとんどのシナリオでうまく機能しますが、キャッシュの無効化をもう少し最適化することが大いに役立つ特定のシナリオがあります。

Railsでフィールドレベルのキャッシュ無効化を実装する方法がわかったので、アプリケーションでのキャッシュの不要な無効化を防ぐことができます。