キャッシュは性能改善の定石手段ですが、「とりあえず挟む」という判断が整合性事故・デバッグ困難・運用崩壊を引き起こします。設計判断の核心は「どの層に・何を・どのライフサイクルで置くか」を事前に確定させることです。データの変更頻度、許容できる陳腐化の時間幅、無効化の手段がセットで揃って初めて、キャッシュ導入は安全な選択肢になります。この記事では、層ごとの選定基準・線引き・無効化設計・境界の壊れやすさを順番に整理します。
キャッシュを安易に挟むと不整合で事故る構造
「高速化できる」という認識だけで挟むと起きること
キャッシュは本質的に「古いデータを使い続ける仕組み」です。この事実を軽視したまま導入すると、DBには更新済みのデータが存在するにもかかわらず、ユーザーには古い状態が返り続けるというデータ不整合が発生します。ECサイトで在庫が0になっても購入画面に「残り3点」と表示されるケースは典型的な事例です。こうした事故は、TTLと更新頻度のミスマッチが原因で起きます。速くなるという効果だけに目を向けて、「古くなる」という副作用の設計を後回しにすると、本番環境で大きな代償を払うことになります。
ある社内システムのリプレースプロジェクトでは、アプリケーション層にキャッシュを追加した直後から、同じ社員番号に複数の部署情報が紐付いて表示されるという報告が上がりました。原因は人事異動時の更新処理がキャッシュの無効化を考慮していなかったことです。不整合の発見から修正・影響確認まで3営業日を要し、担当者がシステム全体を1件ずつ手動確認する作業が発生しました。導入前に「更新イベントと無効化の対応関係」を設計していれば防げた事例です。
キャッシュが絡む障害は原因特定が遅れる
通常のバグは「コードを変えたら壊れた」という因果が追いやすいのに対し、キャッシュ起因の不整合は「昨日まで動いていた処理が今日おかしい」という形で現れます。再現環境を作ろうとしても、キャッシュの状態が本番と一致しないため、開発環境では再現しないことが多いです。この特性がデバッグを長期化させます。
複数層にキャッシュを重ねている構成では、さらに困難になります。CDNが30秒、リバースプロキシが60秒、アプリが5分のTTLを持っていると、どの層の古いデータが返ってきているかを特定するだけで時間を取られます。3層以上のキャッシュを持つシステムでは、障害時のトレースコストが単層の構成と比べて2〜3倍に膨らむという経験則が現場では語られています。複雑さに比例して障害対応のコストが上がることを、導入前に織り込んでおく必要があります。
「追加で解決」という発想が構造を複雑にする
パフォーマンス問題が発生したとき、原因を特定する前にキャッシュを追加してしのぐ判断をすると、根本の問題が温存されたまま複雑性だけが増します。N+1クエリが原因のレスポンス低下をキャッシュで隠すと、後からDBスキーマを変えようとしたときに無効化の設計が絡み合って変更できなくなります。
設計判断として正しいのは、「なぜ遅いのか」を計測ツールで特定してからキャッシュを選択肢として検討することです。DBのI/Oが問題なのか、外部API呼び出しの待機が原因なのか、CPUバウンドの処理なのかによって、キャッシュが有効な場面は異なります。キャッシュは「遅い処理を隠す道具」ではなく「高頻度で繰り返す処理の重複を省く道具」として位置付けることが、設計を破綻させない前提条件です。
どの層(CDN/リバプロ/アプリ/DB)に挟むかの判断
各層の役割と「何をオフロードするか」
CDN層のキャッシュは、静的コンテンツや変更頻度の低いHTMLをエッジで返すことで、オリジンサーバーへのリクエスト数そのものを減らします。ピーク時トラフィックの70〜80%をCDNで吸収できる構成は、スケールアウト判断を後ろ倒しにする効果もあります。ただし、ユーザー固有のコンテンツやセッション依存のレスポンスとは相性が悪く、誤ってキャッシュするとユーザーAのデータがユーザーBに返るという深刻なインシデントになります。
リバースプロキシ層(NginxやVarnishなど)はオリジンの手前でHTTPレスポンスをキャッシュします。CDNよりもキャッシュキーの細かい制御が可能で、ヘッダーやクエリパラメータによる分岐ルールを柔軟に設定できます。アプリケーション層は、外部APIのレスポンス、重い計算結果、ユーザー単位のデータをメモリやKVSに保持する形が典型です。DB層に近いキャッシュは、同一クエリの繰り返し実行を減らす目的で使います。
挟む層を誤ると効果が出ない
DBが原因のスローレスポンスに対してCDNを追加しても、オリジンへのリクエスト数は変わらないため、体感レスポンスは改善しません。逆に、静的コンテンツのキャッシュをアプリケーション内のメモリキャッシュで実装すると、サーバー台数が増えるたびにキャッシュが分散して整合性の管理コストが上がります。
どの層に挟むべきかは「ボトルネックがどの境界で発生しているか」によって決まります。リクエスト数を減らしたいならCDN、アプリとDBの間のI/Oを減らしたいならアプリ層またはDB層のキャッシュ、バックエンドの多段呼び出しを整理したいならサービス間キャッシュ、という順番で考えます。ひとつの構成変更で解決できる範囲に絞り込むことが、後の運用負荷を下げる判断軸になります。
複数層を組み合わせるときの設計原則
複数層のキャッシュを組み合わせる場合、それぞれのTTLと無効化の方法を設計図に明示しておく必要があります。CDNが5分、アプリが2分のTTLを持つ場合、アプリ側のデータを更新してもCDNのキャッシュが残り続けます。CDN側のpurge APIを呼び出すトリガーをどこに置くかが、この構成での設計上の問いになります。
現場でよく見られる失敗は、各層を独立して設定した結果、TTLの長さがそれぞれ異なり、「どの層がどのタイミングで更新されるか」を把握しているエンジニアがいなくなることです。ドキュメントだけでなく、キャッシュのライフサイクルを可視化する仕組み(ログ出力やモニタリングアラート)をセットで作ることが、長期運用の前提になります。
比較表:各キャッシュ層の効果・整合性リスク・運用負荷
| キャッシュ層 | 主な効果 | 整合性リスク | 運用負荷 | 適した用途 |
|---|---|---|---|---|
| CDN | オリジンへのリクエスト数を大幅削減(ピーク時70〜80%吸収も可能) | 高(ユーザー固有コンテンツの誤キャッシュで情報漏洩リスク) | 低〜中(purge APIの呼び出し設計が必要) | 静的コンテンツ・低更新頻度のHTML |
| リバースプロキシ | オリジン手前での重複レスポンス削減、キャッシュキーの細かい制御が可能 | 中(ルール設計次第で整合性を保ちやすい) | 中(設定ファイルの管理・ルール複雑化に注意) | APIレスポンスの共通部分・セミ静的コンテンツ |
| アプリケーション層 | 外部API呼び出し・重い計算の重複排除、ユーザー単位の細かい制御 | 中〜高(無効化ロジックをコードで管理する必要あり) | 高(無効化設計・コード変更時の整合性管理が複雑) | 外部APIキャッシュ・計算結果・ユーザー固有データ |
| DB層(クエリキャッシュ等) | 同一クエリの繰り返し実行を削減、DB負荷の平準化 | 低〜中(更新時の自動無効化が機能する範囲は安全) | 低(DBエンジン側で管理される部分が多い) | 集計クエリ・マスタデータの参照 |
何をキャッシュし、何をしないかの線引き
キャッシュに適したデータの特性
キャッシュに向いているのは「更新頻度が低く、複数リクエストで同じ結果を返す処理」です。商品カタログ情報、マスタデータ(都道府県一覧・カテゴリ情報など)、静的コンテンツ(画像・CSS・JS)は典型的な候補です。外部APIへの呼び出しも、レスポンスの内容が一定期間変わらないものはキャッシュの恩恵を受けやすいです。天気情報や為替レートのように、秒単位では変わらないが取得コストが高いデータがこれに当たります。
判断の目安として「読み取りと書き込みの比率」があります。読み取りが書き込みの10倍以上あるデータは、キャッシュで得られる効果が明確です。逆に書き込みと読み取りがほぼ同頻度のデータをキャッシュしても、無効化のコストが得られる性能向上を上回る可能性があります。この比率を計測する前に「キャッシュを使う」と決めてしまうと、後から費用対効果の根拠が残らなくなります。
キャッシュしてはいけないデータ
金融取引の結果、在庫数、認証情報、権限情報は、古い状態が返ることが直接的なビジネスリスクや認証バイパスにつながるため、キャッシュ対象にしてはいけません。特にセキュリティに関わるデータ(セッション、権限チェックの結果)を誤ってキャッシュすると、あるユーザーの権限情報が別ユーザーのリクエストに使われる事態が起きます。
ユーザー固有のコンテンツ(マイページ、パーソナライズされたレコメンド)もキャッシュキーの設計を誤ると情報漏洩の原因になります。CDNでこれらをキャッシュする際は、キャッシュキーにユーザーIDやセッションIDを含める設計が必要ですが、そうするとキャッシュの効率が大幅に落ちます。「このデータが古くなったとき、最悪のケースは何か」を先に問うことが、線引きの実践的な方法です。
グレーゾーンのデータの扱い
「数秒程度の陳腐化は許容できる」データは、TTLを短くする形でキャッシュ対象にできます。ユーザーのポイント残高をリアルタイム表示している画面で、1〜2秒の遅延を許容するなら、アプリケーション内に数秒のTTLで保持することはあり得ます。ただし、これはシステム全体の要件定義とステークホルダーの合意が前提であり、エンジニアが独断で「これくらいいいだろう」と判断して設定するものではありません。
陳腐化の許容範囲はSLAや業務要件から逆算して決めます。「5分以内の更新が画面に反映されなくても業務は成立するか」という問いをサービスオーナーに確認し、その答えをTTLの上限値に落とし込む流れが設計上の正しい順番です。この確認を省くと、後から「なぜこのTTLにしたのか」の根拠が残らず、変更の判断が困難になります。
無効化(expire/purge)をどう設計するか
expireとpurgeの使い分け
TTLによるexpireは、設定した時間が経てばキャッシュが自動的に無効化される仕組みです。実装が単純で運用コストが低い反面、データが更新されても次のexpireまでは古い状態が返り続けます。一方、purge(明示的な削除)は更新イベントに連動してキャッシュを即時削除する方法で、整合性を保ちやすいですが、「何が更新されたときにどのキャッシュを消すか」の対応表を管理する必要があります。
どちらを選ぶかは、データの更新頻度と整合性の要件によって決まります。マスタデータのように更新が日次バッチ処理でまとめて行われるものはTTLで十分です。ユーザーアクションを起点にリアルタイムで変わるデータは、イベント駆動のpurgeを使わないと整合性を維持できません。両者を組み合わせて「purgeを実行しつつTTLでフォールバック」という設計が、実際には安全性の高い構成です。
キャッシュキーの設計がpurgeの難易度を決める
キャッシュキーの粒度が粗すぎると、関係のないデータまで一緒に削除する「芋づる式のpurge」が発生します。商品カテゴリ全体を1つのキーでキャッシュしていると、1商品の価格変更でカテゴリ全体のキャッシュを再生成しなければなりません。更新頻度が上がるほど、この設計はキャッシュの効率を急激に下げます。
逆にキーが細かすぎると、purgeする対象の特定と列挙がパターンマッチに依存するようになり、漏れが生じやすくなります。キャッシュキーの設計は「どの単位でデータが更新されるか」を起点に、更新イベントとキャッシュキーが1対多で対応できる粒度に設定するのが原則です。この設計を怠ると、purgeの実装が場当たり的になり、不整合の温床になります。
無効化処理が失敗したときの設計
purgeの実行が失敗した場合、古いキャッシュが残り続けます。これを前提にした設計として、purgeと並行してTTLを短く設定しておくフォールバック戦略が有効です。「purgeが成功すれば即時反映、失敗しても最大○分でexpireする」という二重の保険を設計に組み込むことで、purge処理の失敗を即座の障害にせず済みます。
purge処理のログを残しておくことも設計上の要件です。「どのキャッシュが、いつ、どのイベントを起点に削除されたか」が追跡できる状態にしておくと、不整合が発生したときの原因調査が大幅に短くなります。システム規模が大きくなるほど、この追跡コストが後の運用で効いてきます。
キャッシュ依存で壊れやすくなる境界の見極め
マイクロサービス間のキャッシュが境界をあいまいにする
複数のサービスが同一データを参照するアーキテクチャでは、それぞれのサービスが独自にキャッシュを持つと、同じデータに対して複数の「真実」が存在する状態になります。サービスAはDBから直接読んでいて、サービスBは5分前のキャッシュを使っているという状況が起きると、同じリクエストでも呼び出し先によって結果が違うという問題が発生します。
この問題を避けるには、データのオーナーシップを明確にし、キャッシュもオーナーサービス内に閉じる設計が有効です。他のサービスはキャッシュではなくオーナーサービスのAPIを通じてデータを取得し、キャッシュの更新責任をオーナーに集約します。20〜30サービスが並存するマイクロサービス構成では、この原則の欠如が後になって整合性問題の温床になるという事例が多く報告されています。
デプロイ時のキャッシュ状態が問題を引き起こす
コードのデプロイとキャッシュの内容は非同期に動きます。新しいコードが想定するデータ形式と、古いキャッシュに保存されているデータ形式が合わない場合、デプロイ直後にデシリアライズエラーや予期しない動作が発生します。ローリングアップデートで台数を順番に入れ替えている最中も同じ問題が起きます。
対策として、デプロイの前後でキャッシュをflushするか、キャッシュのデータ形式にバージョン情報を含めてコードが対応バージョンのキャッシュだけを使うようにする設計があります。デプロイが週1回程度ならflushでも許容できますが、1日に複数回リリースが走る継続的デプロイメント環境ではバージョニングの設計が安全です。どちらを選ぶかはキャッシュの再生成コストとデプロイ頻度から判断します。
キャッシュが「外れると壊れる」設計になっていないか
キャッシュが正常に機能していることを前提にアプリケーションのロジックを書くと、キャッシュが外れたときに本来の処理が耐えられない状態になります。KVSが落ちたらサービス全体が停止するという構成は、キャッシュが可用性の単一障害点になっています。これはキャッシュの役割を超えた設計です。
キャッシュはあくまで「あれば速い」パスであり、「なくても動く」がデザインの原則です。キャッシュミスまたはキャッシュサーバーの障害時にDBへのフォールバックが走るように設計し、そのフォールバックが本番負荷に耐えられるかをあらかじめ負荷テストで確認しておく必要があります。この設計を怠ると、KVSの障害が即座にサービス停止につながる構成が出来上がります。
キャッシュ導入判断チェックフレーム
以下のチェックリストを導入前の設計レビューで使うことで、キャッシュ起因の事故と設計の後退を防ぐことができます。すべての項目に答えが出ていない段階でキャッシュを本番に入れることは、リスクを積み上げる判断です。手を動かす前の確認として活用してください。
- ボトルネックの特定:計測ツールでレスポンス低下の原因(DB I/O・外部API待機・計算コストなど)を特定済みか。「なんとなく遅い」という理由でキャッシュを入れていないか。
- 対象データの更新頻度:キャッシュ対象のデータが1日・1時間・1分あたり何回更新されるかを把握しているか。読み取りと書き込みの比率が10倍以上かどうかを確認したか。
- 陳腐化の許容範囲:データが最大何分(または何秒)古くなっても業務・ユーザー体験・セキュリティに影響がないかをサービスオーナーに確認済みか。TTLの上限値に落とし込んでいるか。
- 整合性リスクの洗い出し:キャッシュが古い状態を返した場合の最悪シナリオを列挙済みか。認証・権限・金融・在庫など整合性要件が高いデータが混入していないか。
- 無効化の設計:TTLのみかpurgeとの併用か判断済みか。更新イベントとキャッシュキーの対応表が設計図に存在するか。
- キャッシュキーの粒度:更新単位とキャッシュキーの粒度が一致しているか。粗すぎる(芋づる削除)・細かすぎる(漏れが出やすい)どちらの問題も点検したか。
- フォールバックの設計:キャッシュが利用できない場合(サーバー障害・ミス時)にDBへのフォールバックが走るか。そのフォールバックが本番負荷に耐えられるかを負荷テストで確認済みか。
- デプロイとキャッシュの整合:コードのデプロイ時にキャッシュの内容と形式が食い違わないよう、flushまたはバージョニングの方針を決定済みか。
- マルチサービス環境での責任範囲:複数サービスが同一データを参照する場合、キャッシュの責任範囲とオーナーシップを明確にしているか。
- 監視・ログの準備:キャッシュのヒット率・TTL状態・purgeの実行履歴をモニタリングおよびログで追跡できる状態になっているか。
▶ 手を動かして本番まで到達する
その設計判断を、RHEL10の本番環境で手を動かして固めませんか
ここまでの設計判断は、読めば腹に落ちます。ですが「自分の手で本番まで通しきる自信」は、それとは別の話になります。
最大6名の少人数で、構築実務者の到達点まで引き上げます。座学で終わらせず、本番相当の環境で判断を検証する2日間です。
Linuxセミナー上級編(RHEL10)|最大6名の少人数ハンズオン