はじめに
マスタデータを更新したのに、アプリに反映されたりされなかったりする。そんな現象に遭遇しました。
原因は サーバーのインスタンスごとに持っているインメモリキャッシュ でした。
「スケールするサーバー × プロセス内キャッシュ」という組み合わせはよくある構成だと思うので、仕組みと対応をまとめます。
※ 説明のため、コードや構成は簡略化しています。
環境
- 実行環境:Google App Engine(オートスケール)
- アプリ:Node.js(TypeScript)
- キャッシュ:Redis(メイン)+ 一部でインメモリキャッシュ
起きたこと
- 管理画面からマスタデータ(画面の文言や設定値など)を更新した
- DB の値は正しく更新されている
- しかしアプリの表示は 古いまま。しかもリクエストによって 新しい値が出たり、古い値が出たり する
原因:一部でインメモリキャッシュの仕組みが実装されていた
処理を追うと、Redis とは別に、一部で インメモリキャッシュ の仕組みが実装されていました。
Redis 導入より前から動いていた仕組みで、これまで問題が表に出ていなかった部分です。
実装をシンプルにすると、次のような形です。
class MasterService {
// プロセス内に保持するキャッシュ
private cache: Map<string, MasterData> | null = null;
// 初回アクセス時に DB から読み込む
async load() {
if (!this.cache) {
this.cache = toMap(await repository.findAll());
}
}
// マスタ更新時に呼ばれる
clear() {
this.cache = null;
}
}
更新時に clear() も呼ばれています。それなのに、なぜ反映されなかったのか……。
なぜ反映されないのか
インメモリキャッシュは「インスタンスの数だけ」存在する
インメモリキャッシュは、各インスタンスのメモリ上にある変数です。
オートスケール環境ではアクセスに応じてインスタンスが増減するので、インスタンスの数だけ別々のキャッシュ ができます。
clear() が届くのは1つのインスタンスだけ
マスタ更新のリクエストを処理するのは、どれか1つのインスタンスです。
clear() はそのインスタンスのキャッシュしか消せません。
| インスタンス | 状態 | 返す値 |
|---|---|---|
| A:更新リクエストを受けた |
clear() 後に再読み込み |
新 |
| B:更新前から動いている | 何も知らされない | 旧 |
| C:更新後に起動した | 初回アクセス時に DB から読み込み | 新 |
さらにキャッシュに有効期限がないため、インスタンス B は 終了するまで古い値を返し続けます。
どのインスタンスに振り分けられるかで結果が変わるため、「新しかったり古かったりする」状態になっていました。
Redis だと何が違うのか
Redis はアプリの外にある 1つの共有キャッシュ です。すべてのインスタンスが同じ場所を参照します。
Redis のキャッシュを1回消せば、どのインスタンスからでも同じ値を見られます。
インスタンスが増減したとしても、整合性が崩れません。
インメモリキャッシュと Redis の比較
| インメモリキャッシュ | Redis(共有キャッシュ) | |
|---|---|---|
| キャッシュの場所 | 各インスタンスのメモリ | アプリの外の1か所 |
| キャッシュの数 | インスタンスの数だけ | 1つ |
| 更新時に消す場所 | 全インスタンス分(1回の clear() では1台しか消えない) |
Redis の1か所だけ |
| 速さ | 最速(ネットワーク不要) | 高速(ネットワーク経由) |
| コスト | 追加なし | Redis の運用費がかかる |
一時対応:新しいバージョンにトラフィックを移す
恒久対応の前に、再デプロイで新しいバージョンにトラフィックを移しました。
新しいバージョンのインスタンスは初回アクセス時に DB から最新の値を読み込み、古いキャッシュを持つインスタンスにはリクエストが届かなくなります。
ただし「更新のたびに再デプロイが必要」な運用になるので、あくまで暫定対応としています。
恒久対応の選択肢
| 方法 | 概要 | 向いているケース |
|---|---|---|
| Redis などの共有キャッシュに寄せる | 全インスタンスで同じキャッシュを参照する | すでに Redis がある、または費用が過剰にならない場合 |
| TTL(有効期限)を設ける | 一定時間でキャッシュを捨てて読み直す | 有効期限の間だけ反映が遅れても許される場合 |
まとめ
- インメモリキャッシュは インスタンスごとに別物。スケールする環境では「1か所で消せば OK」にはならない
- 複数インスタンスで共有したいデータは、費用が見合えば Redis のような 共有キャッシュ に置くのが安全
- 新しいバージョンへのトラフィック移行は暫定対応として有効
- インメモリキャッシュはほとんど変わらないデータや、多少古くても困らないデータには有効