データベース、アプリケーション、エッジレイヤー全体での最適なデータキャッシュ戦略
Wenhao Wang
Dev Intern · Leapcell

はじめに
高性能でスケーラブルなアプリケーションの追求において、キャッシュは不可欠なテクニックです。特にユーザーのトラフィックが増加し、データ量が増大するにつれて、データ取得はしばしば重大なボトルネックとなります。リクエストごとにプライマリ永続ストレージから直接データを取得するだけでは、応答時間の遅延、データベース負荷の増加、そして最終的にはユーザーエクスペリエンスの低下につながる可能性があります。そこで登場するのがキャッシュです。これは、再計算や永続ストレージへのルックアップの必要性を減らすために、頻繁にアクセスされるデータを、より高速でアクセスしやすい場所に格納することです。しかし、キャッシュは万能な解決策ではありません。キャッシュを適用できるレイヤーは複数あり、それぞれに独自の利点と理想的なユースケースがあります。データベースクエリキャッシュ、アプリケーションレベルキャッシュ(Redisなど)、CDNキャッシュの間のニュアンスを理解することは、パフォーマンスが高く回復力のあるシステムを構築することを目指すすべてのアーキテクトまたは開発者にとって重要です。この記事では、これらの各キャッシュレイヤーを掘り下げ、それらのメカニズム、適切なシナリオ、そしてデータアクセスを最適化するための適切な戦略を効果的に選択する方法を説明します。
コアキャッシュの概念
各キャッシュレイヤーの詳細に入る前に、この議論全体で参照される基本的なキャッシュの概念を確立しましょう。
キャッシュヒット (Cache Hit): 要求されたデータの一部がキャッシュで見つかった場合に発生します。これは、データが遅いストレージにアクセスせずに迅速に提供できることを意味するため、望ましい結果です。 キャッシュミス (Cache Miss): 要求されたデータの一部がキャッシュで見つからなかった場合に発生します。これにより, システムは元のデータソース(例:データベース)からそれを取得し、後で要求のためにキャッシュに格納するオプションが要求されます。 キャッシュ無効化 (Cache Invalidation): 元のデータソースが変更されたときに、キャッシュされたデータを削除または古いものとしてマークするプロセスです。これはキャッシュにおける重要な課題です。古いデータは、アプリケーションの誤った動作につながる可能性があります。 生存時間 (Time-to-Live, TTL): キャッシュされたデータが所定の期間後に自動的に削除される、キャッシュ無効化の一般的な戦略です。 追放ポリシー (Eviction Policy): キャッシュが容量に達した場合、追放ポリシーは新しいアイテムのためのスペースを作るために削除するアイテムを決定します。一般的なポリシーには、最も最近使用されていない (LRU)、最も頻繁に使用されていない (LFU)、先入れ先出し (FIFO) があります。
データベースクエリキャッシュ
データベースクエリキャッシュは、データベースサーバーレベルで動作します。その主な目的は、頻繁に実行されるSELECTステートメントの結果と、それに対応するSQLクエリを格納することです。同じクエリが再度実行されると、データベースシステムはクエリを再実行、解析、最適化、あるいは基盤となるデータファイルにアクセスすることなく、キャッシュから直接結果を提供できます。
メカニズムと実装
MySQL(バージョン8.0で削除される前)やPostgreSQL(外部拡張機能またはより包括的なバッファを介して)などのほとんどのリレーショナルデータベース管理システム(RDBMS)は、歴史的にクエリキャッシュの形態を提供してきたか、現在も提供しています。
単純なユーザーデータをクエリするアプリケーションを考えてみましょう。
SELECT * FROM users WHERE id = 123;
このクエリが最初に実行されると、データベースはそれを処理し、データを取得し、そのクエリキャッシュに{query_string: result_set}を格納します。同じクエリ文字列が再度送信されると、データベースはまずクエリキャッシュをチェックします。一致が見つかり、キャッシュされた結果がまだ有効な場合、キャッシュされた結果をすぐに返します。
長所と短所
長所:
- 自動: 有効化および設定されると、一致するクエリに対して自動的に機能します。
- データベース負荷の軽減: 繰り返し実行される同一クエリに対するデータベースサーバーのCPUおよびI/O負荷を大幅に軽減します。
短所:
- 無効化の課題: これが最大の弱点です。キャッシュされたクエリに関与する* any テーブルの any *データが変更された場合、そのクエリ(および潜在的に多くの他のクエリ)のキャッシュされた結果全体が古くなり、無効化する必要があります。これは、特に書き込み負荷が高いワークロードの場合、重い無効化オーバーヘッドにつながる可能性があります。
- スコープの制限:
SELECTクエリの正確な文字列のみをキャッシュします。わずかなバリエーション(例:異なる空白、WHERE id = 123ではなくWHERE id = 124)はキャッシュミスにつながります。 - スケーラビリティの問題: 一部のデータベースシステム(MySQLのグローバルクエリキャッシュなど)では、書き込み中または頻繁な無効化のためのキャッシュロックの競合が激しいと、パフォーマンスが実際に低下する可能性があり、ボトルネックになります。
- 最新DBでの削除: その複雑さとパフォーマンスの落とし穴のため、多くの最新データベースバージョン(例:MySQL 8.0)では、ページレベルでキャッシュを管理する、より詳細なバッファキャッシュ(InnoDBバッファプールなど)を優先して、汎用クエリキャッシュを削除または非推奨にしました。
いつ使用するか
その制限を考慮すると、専用のデータベースクエリキャッシュは、最新の、高同時実行性、または書き込み負荷の高いアプリケーションでは、一般的に推奨されません。特定のクエリで非常に高いヒット率を持つ、読み取り負荷が高く書き込みが少ないワークロードで役立つ場合がありますが、その場合でも、その有効性はメンテナンスの負担と潜在的なパフォーマンス低下によってしばしば上回られます。ほとんどのユースケースでは、アプリケーションレベルのキャッシュの方がはるかに優れた制御と効率を提供します。
アプリケーションレベルキャッシュ(例:Redis)
アプリケーションレベルのキャッシュは、RedisやMemcachedのような専用のインメモリデータストアに、アプリケーションレイヤーの近くにデータを格納することを含みます。このキャッシュは、アプリケーションとデータベースの間に配置されます。アプリケーションは、どのデータをキャッシュに格納するか、どのくらいの期間保持するか、どのように無効化するかを明示的に管理します。
メカニズムと実装
アプリケーションがデータが必要な場合、まずアプリケーションキャッシュをチェックします。データが見つかった場合(キャッシュヒット)、すぐに返されます。見つからなかった場合(キャッシュミス)、アプリケーションはデータベースからデータを取得し、後で取得するためにアプリケーションキャッシュに格納し、それを返します。
Redisを使用した単純なPythonの例で示します。
import redis import json # Redisがlocalhost:6379で実行されていると仮定 r = redis.Redis(host='localhost', port=6379, db=0) def get_user_data(user_id): cache_key = f"user:{user_id}" # 1. キャッシュからデータを取得しようとする cached_data = r.get(cache_key) if cached_data: print(f"User {user_id} のキャッシュヒット") return json.loads(cached_data) # 2. キャッシュにない場合、データベースから取得(シミュレーション) print(f"User {user_id} のキャッシュミス、DBから取得中...") # 実際のアプリケーションでは、これはDBクエリになる user_data = {"id": user_id, "name": f"User {user_id}", "email": f"user{user_id}@example.com"} # 3. TTL(例:600秒)を設定してキャッシュにデータを格納 r.setex(cache_key, 600, json.dumps(user_data)) return user_data # 最初の呼び出しはキャッシュミスになる print(get_user_data(1)) # TTL内で後続の呼び出しはキャッシュヒットになる print(get_user_data(1)) # 別のユーザーをシミュレート print(get_user_data(2))
長所と短所
長所:
- きめ細かな制御: 開発者は、どのデータがキャッシュされるか、いつ期限切れになるか、どのように無効化されるかについて完全な制御権を持ちます。これにより、よりインテリジェントなキャッシュ戦略(例:安定性の高いデータを長くキャッシュする)が可能になります。
- 高性能: Redisのようなインメモリストアは信じられないほど高速で、マイクロ秒レベルの応答時間を提供します。
- スケーラビリティ: キャッシュサーバーはデータベースとは独立してスケーリングできるため、システムは大量の読み取り負荷を処理できます。
- 柔軟なデータ構造: Redisはさまざまなデータ構造(文字列、ハッシュ、リスト、セット、ソート済みセット)をサポートしており、多様なキャッシングパターンを可能にします。
- データベースオーバーヘッドの削減: クエリを回避するだけでなく、トランザクショナルでない頻繁にアクセスされるアイテムのデータストレージと取得をオフロードすることによっても、データベースの負荷を軽減します。
短所:
- キャッシュ無効化ロジック: 開発者は無効化ロジックを自分で実装する必要があります。アプリケーションがデータベースでデータを更新した場合、キャッシュ内の対応するエントリを無効化または更新する必要もあります。これは、注意深く処理されない場合、複雑さと古いデータが発生する可能性をもたらす可能性があります。
- メモリフットプリント: 大量のデータをキャッシュすると、キャッシュサーバーでかなりのメモリが消費される可能性があります。
- 単一障害点(クラスター化されていない場合): 単一のキャッシュサーバーは、高可用性(Redis SentinelまたはClusterなど)のために適切に構成されていない場合、SPOFになる可能性があります。
- コスト: 専用のキャッシュサーバーの実行と保守には、インフラストラクチャコストがかかります。
いつ使用するか
アプリケーションレベルのキャッシュは、以下に最も一般的で汎用性の高いキャッシュ戦略です。
- 読み取り負荷が高く、頻繁にアクセスされるデータ: ユーザープロファイル、製品カタログ、構成設定、リーダーボード。
- 計算結果: 頻繁に変更されない、コストのかかる計算またはレポート。
- セッション管理: ユーザーセッションデータの格納。
- フルページキャッシュ: レンダリングされたHTMLページ全体をキャッシュすること。
- マイクロサービスアーキテクチャ: サービス間の高速データレイヤーの提供。
- データベースクエリキャッシュの制限が明らかになった場合: ほとんどすべての最新アプリケーションで堅牢なキャッシュが必要な場合、これはデータベースクエリキャッシュよりも優先されるアプローチです。
CDNキャッシュ
コンテンツデリバリーネットワーク(CDN)キャッシュは、ネットワークのエッジで、エンドユーザーに地理的に近接した場所で動作します。主に静的コンテンツに使用されますが、動的な応答をキャッシュすることもできます。CDNは、画像、ビデオ、CSS、JavaScriptファイル、さらにはHTMLページ全体などのアセットをキャッシュします。
メカニズムと実装
ユーザーがリソース(例:画像 logo.png)を要求すると、リクエストは最初に最も近いCDN「エッジロケーション」(CDNのグローバルネットワーク内のサーバー)に送信されます。CDNに logo.png のキャッシュされたコピーがある場合、それを直接ユーザーに提供します。これは「エッジキャッシュヒット」です。
それ以外の場合、CDNはオリジンサーバー(アプリケーションサーバーまたはストレージバケット)にリクエストを送信し、logo.png を取得し、エッジロケーションにキャッシュしてから、ユーザーに配信します。その後、そのエッジロケーションの近くのユーザーからの後続のリクエストは、CDNキャッシュにヒットします。
通常、CDNキャッシングは、オリジンサーバーの応答ヘッダー(例:Cache-Control、Expires)または管理コンソールでの特定のCDN構成設定を介して構成されます。
CDNキャッシングを有効にするためのHTTPヘッダーの例:
Cache-Control: public, max-age=3600, s-maxage=86400
Expires: Tue, 01 Jan 2025 12:00:00 GMT
max-age はブラウザがキャッシュできる期間を規定し、s-maxage (共有最大年齢)はCDNおよびその他の共有キャッシュがキャッシュできる期間を規定します。
長所と短所
長所:
- レイテンシの削減: データはユーザーに地理的に最も近いサーバーから提供されるため、ネットワークレイテンシが大幅に削減され、知覚されるパフォーマンスが向上します。
- オリジン負荷の軽減: アプリケーションのオリジンサーバーからのリクエストをオフロードし、帯域幅とコンピューティングリソースを節約します。
- 信頼性と回復力の向上: CDNは分散システムであり、トラフィックの急増を処理し、オリジンサーバーがダウンした場合でも高可用性を提供するように設計されていることがよくあります。
- DDoS保護: 多くのCDNは、分散型サービス拒否(DDoS)攻撃に対する組み込み保護を提供します。
短所:
- キャッシュ無効化の複雑さ: グローバルCDNの無効化は困難になる可能性があります。動的コンテンツの場合、鮮度を確保するには、複雑なキャッシュ制御ヘッダー、Webフック、またはプログラムによるパージが必要になることがよくあります。
- 古いデータの可能性: 無効化が完璧に処理されない場合、積極的なキャッシングはユーザーに古いコンテンツが表示される原因となる可能性があります。
- コスト: CDNサービスは、高帯域幅の使用量や複雑な構成の場合、高価になることがあります。
- SSL証明書: CDNエッジロケーション全体でのSSL証明書の管理は、複雑さを増す可能性があります。
いつ使用するか
CDNキャッシュは以下に最適です。
- 静的アセット: 画像、ビデオ、CSS、JavaScriptファイル、フォント、PDF。これが主な用途です。
- 世界中に分散したオーディエンス: ユーザーがさまざまな大陸に分散している場合。
- 高トラフィックウェブサイト: オリジンサーバーの負荷を劇的に軽減します。
- まれに変更される動的コンテンツ: 主に静的だが、マイナーな動的コンポーネント(短いTTLまたは高度な無効化を伴うことが多い)を持つページ。
- APIレスポンス: 静的データまたはごくゆっくりとしか変更されないデータを配信する読み取り専用APIエンドポイントの場合。
適切なキャッシュ戦略の選択
最も効果的なキャッシュ戦略は、しばしばこれらのレイヤーの組み合わせであり、マルチティアキャッシュアーキテクチャを形成します。
-
アプリケーションレイヤー(Redis/Memcached)から始める: ほとんどの内部アプリケーションデータと動的コンテンツの場合、アプリケーションレベルのキャッシュは、パフォーマンス、制御、スケーラビリティの最適なバランスを提供します。これは通常、データベース負荷に対する最初の防御線です。
-
静的および公開コンテンツのためにエッジ(CDN)に移動する: 静的アセットおよび公開キャッシュ可能な動的コンテンツの場合、CDNにデータをプッシュすることで、ユーザーに最も近づけ、最も大幅なレイテンシ改善とオリジンオフロードを提供します。適切な
Cache-Controlヘッダーを使用してください。 -
データベースクエリキャッシュ(注意して使用、または全く使用しない): 前述のように、専用のデータベースクエリキャッシュは、最新のワークロードでは無効化オーバーヘッドのために、しばしばそれ以上の手間がかかる以上の価値があります。代わりに、データ整合性とパフォーマンスのために最適化されているデータベースの内部ページ/ブロックバッファ(MySQLのInnoDBバッファプール、PostgreSQLの共有バッファなど)に依存してください。これらは透過的であり、頻繁にアクセスされるデータブロックを自動的に管理します。
例:ニュースサイト
- CDN: 記事の画像、CSS/JSファイル、およびおそらく短いTTLを持つ人気のある最近公開された記事のHTMLをキャッシュする。
- アプリケーションキャッシュ(Redis): 人気のある記事のテキストコンテンツ、ユーザーセッションデータ、記事メタデータ(タグ、著者情報)、および複雑なクエリ(例:「トップ10トレンド記事」)の結果をキャッシュする。これらのアイテムは、記事が編集または公開されたときに無効化される。
- データベース: すべてのコンテンツの信頼できるソースとして使用し、書き込みとアクセス頻度の低い読み取りを処理する。その内部バッファは、最近アクセスされたデータブロックのキャッシュを処理する。
重要なのは、尋ねることです。
- このデータを消費しているのは誰ですか? 世界中のエンドユーザーですか(CDN)?それとも内部アプリケーションコンポーネントですか(アプリケーションキャッシュ)?
- このデータはどのくらいの頻度で変更されますか? 毎日(長いTTL)、時々(中程度のTTL)、それともリクエストごと(キャッシュなしまたは非常に短いTTL)?
- データの鮮度はどのくらい重要ですか? ユーザーはわずかに古いデータを許容できますか(CDN、アプリケーションキャッシュ)?それともリアルタイムである必要がありますか(キャッシュなしまたは積極的な無効化)?
- このデータを生成または取得するにはどのくらいのコストがかかりますか? 計算集約的ですか?データベース集約的ですか?ネットワーク集約的ですか?
結論
効果的なデータキャッシュは、高性能でスケーラブルで回復力のあるアプリケーションを構築するために不可欠です。データベース、アプリケーション、CDNレイヤーに戦略的にキャッシュを配置することにより、開発者はレイテンシを大幅に削減し、データベース負荷を軽減し、全体的なユーザーエクスペリエンスを向上させることができます。データベースクエリキャッシュは、一見便利ですが、最新のワークロードではしばしばそれ以上の問題をもたらします。Redisのようなアプリケーションレベルキャッシュは、動的データに対して比類のない制御とパフォーマンスを提供し、CDNキャッシュは、グローバルの静的および公開コンテンツの配信、近接性とスケーリングの最適化に優れています。最適な戦略はレイヤードアプローチであり、適切な場所で適切なデータに適切なキャッシュを慎重に選択し、データが高速で最新であることを保証します。
