第 3 章 - 詳細なキャッシュに関するトピック

「コンピューターサイエンスで難しいのは 2 つだけ。それは、キャッシュの無効化と名前付けです」

​- フィル カールトン

概要

これは、AEM のキャッシュに関して 3 部にわたるシリーズの第 3 部です。第 1 部と第 2 部では、Dispatcher のプレーン HTTP キャッシュとその制限事項について説明しました。第 3 部では、そうした制限を克服する方法に関するいくつかの案について説明します。

一般的なキャッシュ

このシリーズの第 1 章および第 2 章では、主に Dispatcher に焦点を当てました。その基本や制限事項、トレードオフが必要なところなどを説明しました。

キャッシュの複雑さや入り組んだ仕組みは、Dispatcher に固有の問題ではありません。キャッシュは一般的に困難です。

Dispatcher がお手持ちの唯一のツールであることは、実は、本当の制限になります。

この章では、キャッシュについての視野をさらに広げ、Dispatcher の欠点を克服する方法をいくつか考えてみましょう。確実な方法はありません。プロジェクトでトレードオフが必要です。キャッシュと無効化の精度は常に複雑になり、複雑さはエラーの可能性を生み出すことに注意します。

次の領域でトレードオフを行う必要があります。

  • パフォーマンスと待ち時間
  • リソース消費量/CPU負荷/ディスク使用量
  • 正確性/通貨/古さ/セキュリティ
  • シンプルさ/複雑さ/コスト/保守性/エラーが発生しやすい

これらのディメンションは、とても複雑なシステムで相互にリンクしています。単純な条件分岐で処理できるわけではありません。システムをよりシンプルにすることで、速くなったり遅くなったりする場合があります。開発コストは削減できるでしょうが、例えば、顧客が古いコンテンツを見たり、遅い web サイトに苦情を言ったりすれば、ヘルプデスクのコストが増加します。これらの要因をすべて考慮して、互いにバランスを取る必要があります。しかし、今ではもう確実な方法や、単一の「ベストプラクティス」は存在せず、多くの悪い方法といくつかの良い方法があるだけです。

チェーンキャッシュ

概要

データフロー

サーバーからクライアントのブラウザーにページを配信する際に、多数のシステムとサブシステムを横断します。注意深く見ると、出発してから到着するまでの間に、データはいくつもの場所を通る必要があり、それぞれがキャッシュの候補となり得ます。

一般的な CMS アプリケーションのデータフロー

一般的な CMS アプリケーションのデータフロー

ハードディスク上に配置され、ブラウザーに表示する必要のあるデータを使用して、ジャーニーを開始しましょう。

ハードウェアおよびオペレーティングシステム

まず、ハードディスクドライブ(HDD)自体には、ハードウェアに組み込まれたキャッシュがあります。次に、ハードディスクをマウントするオペレーティングシステムは、頻繁にアクセスするブロックをキャッシュしてアクセスを高速化するために空きメモリを使用します。

コンテンツリポジトリ

次のレベルは CRX または Oak です。これは、AEM が使用するドキュメントデータベースです。CRX と Oak は、HDD へのアクセスの遅延を避けるために、データをメモリにキャッシュできるセグメントに分割します。

サードパーティのデータ

ほとんどの大規模な web インストールでは、サードパーティのデータも含まれます。つまり、製品情報システム、顧客関係管理システム、従来のデータベース、またはその他の任意の web サービスからのデータなどです。このデータは、必要なときでもソースから取り出す必要はありません。特に、頻繁に変更されないことがわかっている場合は、取り出す必要はありません。そのため、CRX データベースで同期されていない場合はキャッシュされます。

ビジネスレイヤー - アプリ/モデル

通常、テンプレートスクリプトは、JCR API を介して CRX からの Raw コンテンツをレンダリングしません。多くの場合、ビジネスドメインオブジェクト内のデータを結合、計算、変換するビジネスレイヤーが中間にあります。これらの操作が高価な場合は、キャッシュを検討する必要があります。

マークアップフラグメント

モデルは、コンポーネントでのマークアップのレンダリングの基盤になります。レンダリングされたモデルもキャッシュしてはいかがでしょう。

Dispatcher、CDN およびその他のプロキシ

レンダリングされた HTML ページから Dispatcher に移動しましょう。既に説明しましたが、Dispatcher の主な目的は(その名前に反して)HTML ページとその他の web リソースをキャッシュすることです。リソースがブラウザーに到達する前に、キャッシュ可能なリバースプロキシと、キャッシュにも使用される CDN を渡す可能性があります。クライアントはオフィスにあり、プロキシ経由でのみ web アクセスを許可している場合があります。また、そのプロキシがトラフィックを減らすためにキャッシュしている場合もあります。

ブラウザーキャッシュ

ブラウザーもキャッシュすることを忘れないでください。これは見過ごされやすい利点です。ただし、これはチェーンキャッシュの中でも最も近く、最も高速なキャッシュです。残念ながら、ユーザー間で共有されることはありませんが、1 人のユーザーの様々なリクエスト間で共有されます。

キャッシュするべき場所とその理由

これは、潜在的なキャッシュの長いチェーンです。そして、誰もが古いコンテンツを目にするという問題に直面したことがあります。しかし、いくつの段階があることを考慮すると、ほとんどの場合これが機能しているのは奇跡と言えます。

しかし、このチェーンのどこにキャッシュすると意味があるのでしょうか。先頭でしょうか?末尾でしょうか?任意の場所でしょうか?それは時と場合によるのですが、それも膨大な数の要因に依存します。同じ web サイト内の 2 つのリソースであっても、その質問に対する別の回答が必要になる場合があります。

考慮に入れるべき要因について大まかに説明します。

残存期間 - オブジェクト固有の残存時間が短い場合(交通データは天候データよりも残存時間が短い可能性があります)、キャッシュする価値がない可能性があります。

生産コスト - オブジェクトの再生産と配信にかかるコスト(CPU サイクルと I/O の点で)。安価な場合、キャッシュを使用する必要はありません。

サイズ - 大きなオブジェクトをキャッシュするには、より多くのリソースが必要です。これは制限要因になる可能性があり、利益に対してバランスを取る必要があります。

アクセス頻度 - オブジェクトにほとんどアクセスしない場合は、キャッシュが有効でない可能性があります。キャッシュから 2 回目にアクセスする前に、古くなるか無効になるだけです。このようなアイテムは、メモリリソースをブロックするだけです。

共有アクセス - 複数のエンティティで使用されるデータは、チェーンの上流にキャッシュする必要があります。実際には、キャッシュチェーンは鎖ではなく、ツリーです。リポジトリ内の 1 つのデータが複数のモデルで使用される場合があります。これらのモデルは、複数のレンダリングスクリプトで使用して HTML フラグメントを生成できます。複数のページにこれらのフラグメントがインクルードされ、ブラウザーのプライベートキャッシュを使用して複数のユーザーに配布されます。したがって、「共有」とは人々の間だけでなく、むしろソフトウェアの間で共有することを意味します。潜在的な「共有」キャッシュを見つけたい場合は、ツリーをルートまでさかのぼって、共通の祖先を見つけます。そこにキャッシュする必要があります。

地理空間分散 - ユーザーが世界中に分散している場合、キャッシュの分散ネットワークを使用すると、待ち時間を削減できる場合があります。

ネットワーク帯域幅と待ち時間 - 待ち時間と言えば、顧客はどのような人たちで、どのような種類のネットワークを使用していますか?顧客は、旧世代のスマートフォンの 3G 接続を使用している、発展途上国のモバイル顧客でしょうか?より小さいオブジェクトを作成し、ブラウザーのキャッシュにキャッシュすることを検討してください。

ここまで、このリストは決して包括的なものではありませんが、アイデアは理解できたと思います。

チェーンキャッシュの基本ルール

繰り返しますが、キャッシュは難しいものです。プロジェクトの問題を回避するのに役立つ、以前のプロジェクトから抽出した一部の基本ルールを共有します。

重複キャッシュの回避

前の章で紹介した各レイヤーは、キャッシュチェーンにある程度の価値を提供します。計算サイクルを節約するか、データを消費者に近づけることで可能になります。キャッシュチェーンの複数のステージでデータの一部をキャッシュすることは間違いではありませんが、次のステージのメリットとコストを常に考慮する必要があります。パブリッシュシステムでページ全体をキャッシュしても、通常は何のメリットもありません。これは既に Dispatcher で行われているからです。

無効化戦略の混合

基本的な無効化戦略には、以下の 3 つがあります。

  • TTL、残存期間: ​オブジェクトは、一定の時間(「今から 2 時間後」など)が経過すると有効期限が切れます
  • 有効期限: ​オブジェクトは、将来の定義された時刻(「2019年6月10日の午後 5 時」など)に有効期限が切れます
  • イベントベース: ​オブジェクトは、プラットフォームで発生したイベント(ページが変更されてアクティブ化された場合など)によって明示的に無効化されます

様々なキャッシュレイヤーで異なる戦略を使用できるようになりましたが、「有害な」戦略がいくつかあります。

イベントベースの無効化

純粋なイベントベースの無効化

純粋なイベントベースの無効化:内部キャッシュから外部レイヤーへの無効化

純粋なイベントベースの無効化は、理解するのが最も簡単で、理論的に正しいものを得るのが最も簡単で、最も正確です。

簡単に言えば、オブジェクトが変更された後、キャッシュは 1 つずつ無効化されます。

覚えておくべきルールは 1 つだけです。

内部から外部へのキャッシュは常に無効化します。最初に外部キャッシュを無効にすると、古いコンテンツが内部キャッシュから再キャッシュされる可能性があります。キャッシュが何時にまた更新されるかを推測するのではなく、確実にしておくことです。最良の方法は、内部キャッシュを無効化した​ ​に外部キャッシュの無効化をトリガーすることです。

これが理論です。ただし、実際には多くの問題があります。イベントは、場合によってはネットワーク経由で配信する必要があります。実際には、これは実装が最も難しい無効化スキームになります。

自動 - 修復

イベントベースの無効化では、不測の事態に備えた計画を立てておく必要があります。無効化イベントを見逃すとどうなるでしょう。簡単な戦略は、一定時間後に無効化またはパージすることです。そのため、そのイベントを見逃して、古いコンテンツを提供している可能性があります。ただし、オブジェクトには、数時間(日)のみの暗黙の TTL もあります。したがって、最終的にシステムは自動的に修復されます。

純粋な TTL ベースの無効化

非同期 TTL ベースの無効化

非同期 TTL ベースの無効化

これもかなり一般的なスキームです。キャッシュの複数のレイヤーをスタックし、それぞれが特定の時間、オブジェクトを提供する資格があります。

実装は簡単です。残念ながら、データの有効な寿命を予測することは困難です。

内部オブジェクトの寿命を延ばす外部キャッシュ

内部オブジェクトの寿命を延ばす外部キャッシュ

上の図を検討してください。各キャッシュレイヤーの TTL は 2 分になります。では、全体の TTL も 2 分にする必要があるんですね?実は、そうではありません。期限切れになる直前のオブジェクトを外側のレイヤーが取得すると、外側のレイヤーは実際にはそのオブジェクトの実質的な有効期限を延長します。 その場合、実質的な有効時間は 2~4 分にすることができます。 許容時間は 1 日ということでビジネス部門と同意し、今 4 層のキャッシュがあるとします。各レイヤーの実際の TTL は 6 時間以下にする必要があります。結果として、キャッシュミス率が増えることになります。

これが悪い計画だとは言っているわけではありません 。ただ、計画の限界をわかっている必要があります。そうすれば、これは出発点としては容易で悪くない戦略です。サイトのトラフィックが増加した場合にのみ、より精密な戦略を検討すればよいでしょう。

具体的な日付の設定による無効化時間の同期

有効期限に基づく無効化

内部のオブジェクトに具体的な日付を設定し、それを外部のキャッシュに伝播すると、より予測可能な実質的有効期間が得られます。

有効期限の同期

有効期限の同期

ただし、すべてのキャッシュが日付を伝播できるわけではありません。 有効期限が異なる 2 つの内部オブジェクトを外部キャッシュが集約すると、好ましくない状況になる可能性があります。

イベントベースと TTL ベースの無効化の混在

イベントベースと TTL ベースの戦略の混在

イベントベースと TTL ベースの戦略の混在

また、AEM で一般的なスキームは、内部キャッシュ(例えば、イベントをほぼリアルタイムで処理できるメモリ内キャッシュ)でイベントベースの無効化を使用し、外部(明示的な無効化にアクセスできない可能性がある)で TTL ベースのキャッシュを使用することです。

AEM では、パブリッシュシステムにビジネスオブジェクトと HTML フラグメントのメモリ内キャッシュがあります。このキャッシュは、基になるリソースが変更され、その変更イベントが同じくイベントベースで機能する Dispatcher に反映されると、無効化されます。 そのキャッシュの前に、例えば TTL ベースの CDN があります。

Dispatcher の前に(短い)TTL ベースのキャッシュのレイヤーがあると、通常は自動無効化の後で発生するスパイクを効果的に緩和できます。

TTL ベースとイベントベースの無効化の混在

TTL ベースとイベントベースの無効化の混在

有害なケース:TTL ベースとイベントベースの無効化の混在

このような組み合わせは有害です。TTL つまり有効期限ベースのキャッシュの後に、イベントベースのキャッシュを配置しないでください。 「純粋な TTL」戦略でのスピルオーバー効果を思い出してください。ここでも同じ効果が見られます。ただし、外部キャッシュの無効化イベントが既に発生した場合は再び発生する可能性はなさそうです。これで、キャッシュされたオブジェクトの寿命を無限に伸ばすことが可能です。

TTL ベースとイベントベースの組み合わせ:無限へのスピルオーバー

TTL ベースとイベントベースの組み合わせ:無限へのスピルオーバー

部分キャッシュとメモリ内キャッシュ

レンダリングプロセスのステージに関連付けて、キャッシュレイヤーを追加することができます。リモートデータ転送オブジェクトの取得やローカルビジネスオブジェクトの作成から、単一コンポーネントのレンダリング済みマークアップのキャッシュに至るまでです。具体的な実装は、後のチュートリアルに委ねます。 ただし、これらのキャッシュレイヤーのいくつかを先に自分で実装しておく計画があるかもしれません。 したがって、ここで最低限できるのは、基本的な原則と潜在的な問題を紹介することです。

警告

アクセス制御の尊重

ここで説明するテクニックは非常に強力で、AEM 開発者のツールボックスに​ なくてはならないもの ​です。しかし、だからといって乱用せず、賢く使用してください。 オブジェクトをキャッシュに格納し、フォローアップリクエストで他のユーザーと共有することは、実はアクセス制御を回避することを意味します。これは、通常、公開 web サイトでは問題になりませんが、アクセス権を取得する前にユーザーがログインする必要がある場合には問題になる可能性があります。

サイトのメインメニューの HTML マークアップをメモリ内キャッシュに格納して、様々なページ間で共有することを検討してください。実際にはこれは部分的にレンダリングされた HTML を保存するのに最適な例です。ナビゲーションの作成に多くのページをトラバースする必要があるので、通常はコストがかかるからです。

すべてのページで同じメニュー構造を共有するのではなく、すべてのユーザーで共有することで、より効率的になります。しかし、待ってください。メニューには、特定のユーザーのグループに対してのみ確保されている項目がいくつかあるかもしれません。その場合、キャッシュはもう少し複雑になります。

カスタムビジネスオブジェクトのみをキャッシュ

これが、私たちが提供できる最も重要なアドバイスです。

WARNING
自分のオブジェクト、不変のオブジェクト、自分で作成したオブジェクト、浅く、外部参照を持たないオブジェクトのみをキャッシュします。

これはどういうことでしょうか。

  1. 他のユーザーのオブジェクトの意図されたライフサイクルについてはわかりません。リクエストオブジェクトへの参照を取得し、それをキャッシュすることにしたとします。リクエストが終了すると、サーブレットコンテナは、次の受信リクエストでそのオブジェクトをリサイクルします。その場合、自分が排他的に制御できたと思っていたコンテンツを他のユーザーが変更しています。否定しないでください。プロジェクトでそのようなことが起きたのを見たことがあります。顧客に、自分のデータではなく他の顧客データが表示されたのです。

  2. オブジェクトがその他の参照チェーンによって参照されている限り、ヒープから削除できません。一般に小さいと考えられるオブジェクトをキャッシュに保持し、そのオブジェクトが、例えば、画像の 4MB 表現を参照する場合、メモリの漏洩に問題が発生する可能性が高くなります。キャッシュは、弱い参照に基づくことになっています。しかし、弱い参照は期待どおりに機能しません。これは、まさに最もよくメモリリークを生成し、メモリ不足エラーで終了する方法です。それに、外部オブジェクトで保持されるメモリのサイズもわからないと思います。

  3. 特に Sling では、各オブジェクトを(ほぼ)互いに適応させることができます。リソースをキャッシュに配置するとします。次のリクエスト(異なるアクセス権を持つ)は、そのリソースを取得し、それを resourceResolver またはセッションに適応させて、アクセス権を持たない他のリソースにアクセスします。

  4. AEM からのリソースに薄い「ラッパー」を作成する場合でも、それが自分の不変リソースであっても、それをキャッシュしてはなりません。ラップされたオブジェクトは(以前は禁止されていましたが)参照であり、詳しく見れば、基本的に最後の項目で説明したのと同じ問題を引き起こします。

  5. キャッシュする場合は、プリミティブデータを独自のシャローオブジェクトにコピーして、独自のオブジェクトを作成します。オブジェクトのツリーをキャッシュする場合など、参照を使用して独自のオブジェクト間をリンクすることができます。これは問題ありません。ただし、同じリクエストで作成したオブジェクトのみをキャッシュし、他の場所(「自分の」オブジェクトの名前空間でも)からリクエストされたオブジェクトは一切キャッシュしません。オブジェクトをコピーする ​ことが重要です。また、リンクされたオブジェクトの構造全体を一度にパージし、構造への参照と参照の出入りを避けるようにしてください。

  6. もちろん、オブジェクトを不変に保ちます。プライベートプロパティのみで、セッターはなしです。

多くのルールがありますが、それに従う価値があります。経験があり、超スマートで、すべてをコントロールしているユーザーだとしてもです。あなたのプロジェクトの若い同僚は新卒です。その人はこうした落とし穴についてすべてを知りません。落とし穴がなければ、回避するものは何もありません。簡潔で分かりやすく保ちましょう。

ツールとライブラリ

このシリーズは、概念を理解し、ユースケースに最適なアーキテクチャを構築できるようにすることを目的としています。

個別のツールを推進しているわけではありません。ただし、それらを評価する方法を示すヒントをお伝えします。例えば、AEM には、バージョン 6.0 以降の固定 TTL を持つ単純な組み込みキャッシュがありますが、使用するべきでしょうか。イベントベースのキャッシュがチェーン内でフォローするパブリッシュでは、おそらく使うべきではありません(ヒント:Dispatcher)。しかし、オーサーにとっては適した選択かもしれません。また、Adobe ACS Commons による HTTP キャッシュもあり、検討する価値があるかもしれません。

または、Ehcache のような成熟したキャッシングフレームワークに基づいて独自に作成します。これを使用して、Java オブジェクトとレンダリングされたマークアップ(String オブジェクト)をキャッシュできます。

単純なケースでは、同時ハッシュマップを使用してもうまくいくかもしれません。ツールまたはスキルのいずれかで、すぐに限界を見ることになるでしょう。同時実行性は、名前とキャッシュと同じくらいマスターするのが困難です。

参照

基本用語

ここでは、あまり深いキャッシュ理論は説明しませんが、すぐに始められるように、バズワードを少し説明する必要があります。

キャッシュのエビクション

無効化とパージについてはかなりお話ししました。キャッシュのエビクション は、それらの用語に関連しています。エントリに対してエビクションが行われると、そのエントリは使用できなくなります。 ただし、エビクションは、エントリが古くなったときではなく、キャッシュがいっぱいになったときに発生します。 より新しい(「より重要な」)アイテムは、古い(またはそれほど重要でない)アイテムをキャッシュから追い出します。 どのエントリを犠牲にする必要があるかは、時と場合によって決まります。 最も古いものや、ごくまれに使用されたもの、長い間アクセスされたものに対してエビクションを行いたい場合もあります。

プリエンプティブキャッシュ

プリエンプティブキャッシュとは、無効化または古いと見なされた瞬間に、新しいコンテンツでエントリを再作成することを意味します。 もちろん、頻繁に、即座にアクセスされることが確実であるような、少数のリソースでのみ実行します。そうしないと、リクエストされない可能性のあるキャッシュエントリを作成してリソースを無駄にすることになります。 キャッシュエントリを事前に作成することで、キャッシュの無効化後に、リソースに対する最初の要求の待ち時間を短縮できます。

キャッシュウォーミング

キャッシュウォーミングは、プリエンプティブキャッシュと密接に関係しています。 ただし、実際のシステムではこの用語を使用しません。 時間の制約は前者に比べて少ないです 。無効化の直後にキャッシュを再作成する必要はありませんが、時間が許す限り、キャッシュを徐々にいっぱいにします。

例えば、ロードバランサーからパブリッシュ/Dispatcher レッグを取り出して、更新します。 再統合する前に、最も頻繁にアクセスするページを自動的にクロールして、再びキャッシュに取り込みます。 キャッシュが「ウォーム」の場合は、ロードバランサーにレッグを再組み込みするのに十分な量が入ります。

または、レッグを一度に再統合するかもしれませんが、通常の使用でキャッシュを「ウォーム」する機会を得るために、レッグへのトラフィックをスロットルします。

また、実際のリクエストで実際にアクセスされる際に、システムがアイドル状態になって待ち時間を短縮するために、アクセス頻度の低いページをキャッシュする場合もあります。

キャッシュオブジェクト ID、ペイロード、無効化依存関係および TTL

一般に、キャッシュされたオブジェクトつまり「エントリ」には、5 つの主要なプロパティがあります。

キー

ID は、オブジェクトを認識するためのプロパティです。 ペイロードを取得するか、キャッシュからパージします。 例えば、Dispatcher はページの URL をキーとして使用します。 Dispatcher はページパスを使用しません。 これは、別々に異なるレンダリングを識別するには不十分です。 他のキャッシュは異なるキーを使用する場合があります。 後でいくつか例を見てみましょう。

値/ペイロード

これは、取得するデータのオブジェクトの宝箱です。 Dispatcher の場合は、ファイルコンテンツです。 ただし、Java オブジェクトツリーにすることもできます。

TTL

TTL については既に説明しました。 それ以降はエントリが古いと見なされ、配信されない期間のことです。

依存関係

これは、イベントベースの無効化に関連します。 そのオブジェクトは何の元のデータに基づいていますか? 第 1 部で既に、真の正確な依存関係のトラッキングは複雑すぎると述べました。しかし、システムに関する知識があれば、依存関係をより単純なモデルで近似することができます。古いコンテンツをパージするのに十分なオブジェクトを無効にします。そして、不注意にも、必要以上に多くのコンテンツをパージするかもしれません。しかし、それでも「すべてをパージ」しないようにしています。

どのオブジェクトが他のオブジェクトに依存しているかは、個々のアプリケーションで純正のものです。後で、依存関係戦略を実装する方法の例をいくつか示します。

HTML フラグメントのキャッシュ

別のページでのレンダリングされたフラグメントの再利用

別のページでのレンダリングされたフラグメントの再利用

HTML フラグメントのキャッシュは強力なツールです。アイデアは、コンポーネントによって生成された HTML マークアップをインメモリキャッシュにキャッシュすることです。なぜそうすべきかと疑問に思うかもしれません。とにかく、そのコンポーネントのマークアップを含め、ページ全体のマークアップを Dispatcher にキャッシュしています。よろしいですか。しかし、1 ページにつき 1 回だけです。そのマークアップはページ間で共有されていません。

例えば、各ページの上にナビゲーションをレンダリングするとします。マークアップは各ページで同じように表示されます。ただし、Dispatcher 内にない各ページに対して、繰り返しレンダリングしているのです。そこで、自動無効化の後、すべてのページを再レンダリングする必要があることに注意してください。基本的に、同じコードを何百回も実行して同じ結果を得ているのです。

経験上、ネストされたトップナビゲーションのレンダリングは非常に高価なタスクです。通常は、ドキュメントツリーの適切な部分を移動してナビゲーション項目を生成します。ナビゲーションタイトルと URL のみが必要な場合でも、ページはメモリに読み込む必要があります。貴重な資源を詰まらせているのです。何度も何度も。

しかし、コンポーネントは多くのページで共有されます。何かを共有することは、キャッシュを使用することを示します。したがって、ナビゲーションコンポーネントが既にレンダリングされ、キャッシュされているかどうかを確認し、再レンダリングする代わりに、単にキャッシュ値を生成します。

その計画では 2 つの素晴らしい点が簡単に見逃されています。

  1. Java 文字列をキャッシュしています。String は外部参照を持たず、不変です。上記の警告を考慮すると、これは超安全です。

  2. 無効化も非常に簡単です。Web サイトに何か変更が生じた場合は、このキャッシュエントリを無効にします。再構築は 1 回だけ実行する必要があり、数百ページすべてで再利用されるので、比較的安価です。

これは、パブリッシュサーバーにとって大きな安心材料です。

フラグメントキャッシュの実装

カスタムタグ

以前は、JSP をテンプレートエンジンとして使用し、コンポーネントのレンダリングコードの周りにカスタム JSP タグをラッピングするのが一般的でした。

<!-- Pseudo Code -->

<myapp:cache
  key=' ${info.homePagePath} + ${component.path}'
  cache='main-navigation'
  dependency='${info.homePagePath}'>

… original components code ..

</myapp:cache>

カスタムタグの本文がキャプチャされてキャッシュに書き込まれるか、本文が実行されないようにして、代わりにキャッシュエントリのペイロードを出力します。

「キー」は、ホームページ上に表示されるコンポーネントのパスです。現在のページではコンポーネントのパスを使用しません。これにより、ページごとに 1 つのキャッシュエントリが作成されるので、そのコンポーネントを共有する意図と矛盾します。また、コンポーネントの相対パス(jcr:conten/mainnavigation)を使用しません。他のサイトで異なるナビゲーションコンポーネントを使用できなくなるからです。

「キャッシュ」は、エントリを保存する場所を示すインジケーターです。通常、項目を保存するキャッシュは複数あります。それぞれが少し異なる行動を取るかもしれません。そのため、格納されているものを区別すると便利です。結局のところ、文字列に過ぎません。

「依存関係」は、キャッシュエントリが依存するものです。「メインナビゲーション」キャッシュには、ノード「依存関係」の下に変更がある場合、それに応じたエントリをパージする必要があるというルールが含まれている場合があります。したがって、キャッシュ実装では、変更を認識するために自身をリポジトリ内のイベントリスナーとして登録し、キャッシュ固有のルールを適用して、無効化する必要があるものを見つけ出す必要があります。

上記は一例に過ぎません。キャッシュのツリーを持つように選択することもできます。第 1 レベルを使用してサイト(またはテナント)を分割し、第 2 レベルをコンテンツのタイプ(例:「main-navigation」)に分岐します。これにより、上の例のように、ホームページのパスを追加する必要がなくなります。

ちなみに、この方法は、より最新の HTL ベースのコンポーネントでも使用できます。その場合、HTL スクリプトの周りに JSP ラッパーがあります。

コンポーネントフィルター

しかし、純粋な HTL アプローチでは、Sling コンポーネントフィルターを使用してフラグメントキャッシュを構築します。まだ実際には見たことがありませんが、その問題に取り組むべきアプローチはこれです。

Sling Dynamic Include

変更する環境(異なるページ)のコンテキストで一定のもの(ナビゲーション)が存在する場合、フラグメントキャッシュが使用されます。

逆の場合もあります。比較的一定のコンテキスト(ほとんど変更されないページ)と、そのページ上で絶え間なく変化するフラグメント(ライブティッカーなど)があるかもしれません。

この場合、Sling Dynamic Include の出番かもしれません。基本的に、これは、動的コンポーネントをラップし、ページにコンポーネントをレンダリングする代わりに参照を作成するコンポーネントフィルターです。この参照は Ajax 呼び出しにすることができます。そのため、コンポーネントはブラウザーによってインクルードされ、周囲のページを静的にキャッシュできます。または、その代わりに、Sling Dynamic Include は SSI ディレクティブ(サーバーサイドインクルード)を生成できます。このディレクティブは Apache サーバーで実行されます。Vanish や、ESI スクリプトをサポートする CDN を利用する場合は、ESI(エッジサイドインクルード)ディレクティブも使用できます。

Sling Dynamic Include を使用したリクエストのシーケンス図

Sling Dynamic Include を使用したリクエストのシーケンス図

SDI ドキュメントによると、「*.nocache.html」で終わる URL のキャッシュを無効にする必要があります。動的コンポーネントを扱っているので、これは理にかなっています。

SDI の使用方法に関する別のオプションが表示される場合があります。インクルードの Dispatcher キャッシュを無効に​ しない ​場合、Dispatcher は、最後の章で説明したようなフラグメントキャッシュのように動作します。ページとコンポーネントフラグメントは、均等に、または独立して Dispatcher にキャッシュされ、ページがリクエストされると、Apache サーバーの SSI スクリプトによって結合されます。そうすることで、(常に同じコンポーネント URL を使用する場合は)メインナビゲーションなどの共有コンポーネントを実装できます。

理論上は、機能するはずです。ただし…

それは実行しないことをお勧めします。実際の動的コンポーネントのキャッシュをバイパスする機能が失われるからです。SDI はグローバルに設定され、「poor-mans-fragment-cache」に対して行う変更も動的コンポーネントに適用されます。

SDI のドキュメントを慎重に検討することをお勧めします。他にもいくつかの制限がありますが、SDI は、場合によっては役立つツールです。

参照

モデルのキャッシュ

モデルベースのキャッシュ:2 種類のレンダリングを持つ 1 つのビジネスオブジェクト

モデルベースのキャッシュ:2 種類のレンダリングを持つ 1 つのビジネスオブジェクト

再びナビゲーションの事例を見てみましょう。各ページには同じマークアップのナビゲーションが必要になるという想定でした。

でも、そうではないかもしれません。現在のページ ​を表すナビゲーション内の項目に対して、別のマークアップをレンダリングしたい場合があります。

Travel Destinations

<ul class="maninnav">
  <li class="currentPage">Travel Destinations
    <ul>
      <li>Finland
      <li>Canada
      <li>Norway
    </ul>
  <li>News
  <li>About us
<ul>
News

<ul class="maninnav">
  <li>Travel Destinations
  <li class="currentPage">News
    <ul>
      <li>Winter is coming>
      <li>Calm down in the wild
    </ul>
  <li>About us
<is

これら 2 つは完全に異なるレンダリングです。しかしそれでも、ビジネスオブジェクト(ナビゲーションツリー全体)は同じです。この​ ビジネスオブジェクト ​は、ツリー内のノードを表すオブジェクトグラフを示します。このグラフは、メモリ内キャッシュに簡単に格納できます。ただし、このグラフには、自分で作成しなかったオブジェクト(特に現在は JCR ノード)を含めたり、そのオブジェクトを参照したりしないでください。

ブラウザーでのキャッシュ

ブラウザーでのキャッシュの重要性については既に言及しましたし、優れたチュートリアルも多く提供されています。結局、ブラウザーにとって、Dispatcher は HTTP プロトコルに従う web サーバーにすぎません。

しかし、理論であるにもかかわらず、他のどこにも見つからない共有したいと思ういくつかの知識を集めました。

基本的に、ブラウザーのキャッシュは、次の 2 とおりの方法で利用できます。

  1. ブラウザーにリソースがキャッシュされており、そのリソースの正確な有効期限がわかっています。その場合、ブラウザーはリソースを再度リクエストしません。

  2. ブラウザーにリソースがありますが、それがまだ有効かどうかわかりません。その場合、ブラウザーは web サーバー(この例では Dispatcher)にリクエストします。 最後の配信以降にリソースが変更された場合は最新のリソースを配信してください、とサーバーにリクエストするわけです 。 リソースが変更されていない場合、サーバーは「304 - not changed」と応答し、メタデータのみが送信されます。

デバッグ

ブラウザーのキャッシュ用に Dispatcher 設定を最適化する場合は、ブラウザーと web サーバーの間でデスクトッププロキシサーバーを使用すると非常に役に立ちます。 アドビが好んで使用しているのは、Karl von Randow による web デバッグプロキシ「Charles」です。

Charles を使用すると、サーバーとの間でやり取りされるリクエストと応答を読み取ることができます。 そして、HTTP プロトコルについて多くのことを把握できます。 最新のブラウザーもいくつかのデバッグ機能を提供していますが、デスクトッププロキシの機能はこれまでにないものです。 転送されたデータの操作、送信のスロットル、単一リクエストの再現などを行えます。 しかも、ユーザーインターフェイスは明確に配置され、非常に包括的です。

最も基本的なテストは、web サイトを通常のユーザーとして使用し(プロキシが介在)、(/etc/… への)静的リクエストの数が時間の経過と共に少なくなっているかどうかをプロキシで調べることです。これらはキャッシュに存在し、再度リクエストされないはずだからです。

キャッシュされたリクエストはログに表示されないのに対し、一部のブラウザー組み込みデバッガーは引き続き「0 ms」または「from disk」という表記でこれらのリクエストを表示するので、プロキシが示す概要の方が明確な場合があります。 その情報は問題もなく正確ではありますが、少しわかりにくくなるかもしれません。

その後、詳細に立ち入り、転送されたファイルのヘッダーを確認して、例えば http ヘッダー「Expires」が正しいかどうかなどを調べることができます。 if-modified-since ヘッダーが設定されたリクエストを再現して、サーバーが 304 または 200 の応答コードで正しく応答するかどうかを調べることができます。 非同期呼び出しのタイミングを観察し、セキュリティ上の前提条件をある程度テストすることもできます。 以前説明したように、明示的に予期されないセレクターを必ずしもすべて受け入れるわけではありません。ここでは、URL とパラメーターをいろいろと操作して、アプリケーションが適切に動作するかどうかを確認できます。

キャッシュをデバッグする際にやってはいけないことが 1 つだけあります。

ブラウザーでページをリロードしないでください。

「ブラウザーのリロード」つまり​ 単純リロード ​と​ 強制リロードシフトリロード)は、通常のページリクエストとは異なります。単純リロードリクエストでは、ヘッダーが設定されます

Cache-Control: max-age=0

シフトリロード(Shift キーを押しながらリロードボタンをクリック)すると、通常はリクエストヘッダーが設定されます。

Cache-Control: no-cache

どちらのヘッダーも、効果はわずかに異なるものの、似ています。ただし、最も重要なのは、URL スロットから URL を開く場合やサイト上のリンクを使用する場合、通常のリクエストとは完全に異なる点です。 通常のブラウジングで設定されるのは、キャッシュ制御ヘッダーではなくおそらく if-modified-since ヘッダーです。

したがって、通常のブラウジング動作をデバッグする場合は、そのとおりの操作を行う必要があります。つまり​ 通常の閲覧 ​をしてください。ブラウザーのリロードボタンを使用するのは、設定におけるキャッシュ設定エラーを表示しないようにする場合に最適です。

Charles プロキシを使用して、ここまで説明してきた事柄を確認しましょう。 そして、ブラウザーを開いたままで、リクエストを再現できます。 ブラウザーからリロードする必要はありません。

パフォーマンステスト

プロキシを使用すると、ページのタイミング動作を把握できます。 もちろん、それはパフォーマンステストではありません。 パフォーマンステストであれば、多数のクライアントが同時にページをリクエストする必要があります。

よくある間違いは、パフォーマンステストに非常に少数のページしか含まれず、これらのページが Dispatcher キャッシュからのみ配信されるというものです。

アプリケーションを実稼働システムに昇格させる場合、負荷は、テストしたものとはまったく異なります。

実稼働システムのアクセスパターンでは、テスト(ホームページと少数のコンテンツページ)の場合のように、少数のページが均等に分散しているわけではありません。 ページ数がはるかに多く、リクエストが非常に不均等に分散しています。 しかも、もちろん、ライブページはキャッシュから 100%提供できるわけではありません。パブリッシュシステムからの無効化リクエストにより、貴重なリソースの大きな部分が自動的に無効化されます。

そして、Dispatcher キャッシュを再構築する場合は、少数のページのみをリクエストするか、多数のページをリクエストするかによって、パブリッシュシステムの動作もまたかなり異なることがわかります。 すべてのページが似たような複雑さであったとしても、その数が物を言います。 チェーンキャッシュについての話を思い出してください。常に同じ少数のページをリクエストする場合は、生データに一致するブロックがハードディスクのキャッシュに格納されているか、オペレーティングシステムによってキャッシュされている可能性が高くなります。 また、対応するセグメントをリポジトリがメインメモリにキャッシュした可能性も高くなります。 したがって、ときどき他のページが様々なキャッシュから交互に立ち上がるような場合よりも、再レンダリングの方がかなり高速です。

キャッシュは難しく、キャッシュに基づくシステムのテストもまた困難です。 では、より正確で現実的なシナリオを得るには、何ができるでしょうか。

複数のテストを実施する必要があると考えられ、ソリューションの品質の尺度として複数のパフォーマンスインデックスを指定する必要があります。

既存の web サイトがある場合は、リクエストの数と、それらの配布方法を測定します。 同様のリクエスト分布を使用するテストのモデル化を試みます。 ランダム性を追加しても問題はありません。 JS や CSS などの静的リソースを読み込むブラウザーをシミュレートする必要はありません。こうしたリソースは重要ではありません。ブラウザーまたは最終的には Dispatcher にキャッシュされるので、負荷は大して増えません。 しかし、参照画像は重要です 。古いログファイルでもその配布を見つけ、類似したリクエストパターンをモデル化します。

それでは、Dispatcher がキャッシュをまったく行わない状態でテストを実施します。 それが最悪のシナリオになります。 この最悪の状況でシステムが不安定になるピーク負荷を調べます。 また、必要に応じていくつかの Dispatcher/Publish 部分を取り除くことで、状況がさらに悪化する可能性もあります。

次に、必要なすべてのキャッシュ設定を「オン」にして同じテストを実施します。 並列リクエストを徐々に増やしてキャッシュをウォームアップし、これらの最適な状況下でシステムがどの程度対応できるかを確認します。

平均的なシナリオとしては、Dispatcher を有効にしたうえで何らかの無効化が発生する状態でテストを実行することも考えられます。 cron ジョブで .stat ファイルにアクセスしたり、無効化リクエストを不規則な間隔で Dispatcher に送信したりして、これをシミュレートできます。 自動的に無効化されないリソースの一部をときどきパージするのも忘れないでください。

最後のシナリオは、無効化リクエストを増やしたり負荷を増やしたりして、変更できます。

これは、単なる線形負荷テストよりも少し複雑ですが、ソリューションに対する信頼を大いに高めることになります。

手間を惜しむこともあるかもしれません。しかし、少なくとも、(均等に分散した)多数のページを使用してパブリッシュシステムで最悪ケースのテストを実施し、システムの制限を確認します。 必ず、最良のシナリオの数を正しく解釈し、十分なヘッドルームをシステムにプロビジョニングしてください。

recommendation-more-help
aeb7eb84-65b7-4bed-b296-3028319d2331