🍙

サーバーサイドのインメモリキャッシュとスケールアウトの問題

に公開

背景

2025年にローンチした弊社プロダクトのバックエンド開発を担当しております。

当該サービスはまだ規模が小さく、限られた人的リソースでサービスの機能開発を最優先にしたいということもあり、サーバー構成は最小かつシンプルな形にとどめています。

そんな中、とある外部サービスから取得したデータをバックエンドサーバー側でインメモリキャッシュして使っていたところ、バックエンドサーバーを冗長化すると不定期に不具合が発生するようになりました。

構成

現状のサーバー構成は以下で、Webアプリケーションとして一般的なALBやECS Fargateを利用しています。

フロントエンドサーバーはService Connectを利用してバックエンドAPIサーバーにアクセスしています。

バックエンドAPIサーバーは外部サービスから取得したデータを一定時間メモリ内にキャッシュしています。その更新処理ではオリジンデータを更新した後にメモリ内のキャッシュデータを破棄しています。そして次の参照リクエストで新しいキャッシュデータが生成されます。

課題

バックエンドサーバーが1台だと問題ないのですが、複数台稼動していると意図しない挙動が発生するようになりました。

それはオリジンのデータを更新した後にキャッシュを明示的に破棄する処理において、更新処理が実施されたサーバーのキャッシュしか破棄できないという構成上の問題に関係します。

つまり、インメモリキャッシュがサーバーごとに独立しているため、更新処理を担当したサーバーしか自分のキャッシュを消せないことが原因です。

例えばバックエンドサーバーとしてサーバーAとサーバーBが動いていて、更新処理がサーバーAで行われたときにサーバーBのキャッシュは古いままになってしまう状態が考えられます。

更新処理がサーバーAで実行されても、サーバーBはそれを知る仕組みがないため、サーバーBのキャッシュは古いまま残ってしまいます。

更新処理後の参照リクエストが古いキャッシュのサーバーに来てしまうとアプリケーションの挙動的にデータに矛盾が生じて意図しない動作が発生してしまいます。

読み取り専用のデータだとこのような懸念はなくなるのですが、オリジンデータの更新が必要な場合はアプリケーションサーバー内のインメモリキャッシュと冗長構成は相性が悪そうです。

なお、一般的なロードバランサーにはSticky Session機能があり、このような構成のためにリクエストの処理を偏らせることができます。

クライアントのブラウザにクッキーで何らかの情報を保持しておき、同じクライアントからのリクエストは同じサーバーにルーティングさせることが可能です。

しかし我々のサーバー構成ではフロントエンドサーバーまでしか制御できず、その先のService Connect経由のバックエンドサーバーまではルーティングを固定できません。

また、上記以外の課題としてキャッシュ効率の低下が考えられます。

アプリケーションサーバーが停止するとキャッシュも消えてしまいますし、冗長化すると同じオリジンデータを複数箇所でキャッシュしていることになります。その上、キャッシュデータが増えてくるとアプリケーションサーバーをスケールアップさせる必要も出てきてしまいます。

対応

より一般的な構成としてキャッシュサーバーを別途構築して使う形が良さそうです。

候補としてはKVSが挙げられます。RDBMSでもイベント駆動のTTL機能を実現する方法はありますが、個人的にDBをあんまりキャッシュ用途に使いたくはないです。

ElastiCache (Memcached, Redis, Valkey) だとクライアントライブラリも充実していて扱い易そうです。しかしマネージドサービスとはいえエンジンのバージョンUP対応などのインスタンスのメンテナンス作業が少なからず発生します。サーバーレス版も存在しますが、クラスターモードを有効化する必要があって考慮ポイントが増えそうです。

DynamoDBだとオンデマンド課金で使った分だけコストが発生し、フルマネージドなのでインフラ面での運用も発生しません。ただ、特定のキーにアクセスが集中するとオンデマンド課金でもホットパーティションでスロットリングされる可能性があります。また、参照と更新操作それぞれで一度に指定できるキー数の上限があるため、大量のキーを扱う場合は実装に多少の工夫が必要です。

我々としては「運用コストをなるべく上げない」「社内での利用実績が多い」点を考慮して現時点でDynamoDBを選択しました。

最後に

アプリケーションサーバー内のインメモリキャッシュは手軽にライブラリで実現できてスモールスタートに合っている選択肢でした。

振り返ってみると初歩的な構成上の課題でしたが、今後もサービスのフェーズに合わせて最適な構成を取り続けられるよう改善していきます。

Discussion