layercache – Node.js向けマルチレイヤーキャッシュライブラリ
(github.com/flyingsquirrel0419)layercacheとは?
Node.jsで Memory → Redis → Disk を1つのAPIで束ねるマルチレイヤーキャッシュライブラリです。
キャッシュヒット時には最も高速なレイヤーから値を取り出し、上位レイヤーを自動で埋めます。ミス時には同時リクエストが100件入っても、fetcher はちょうど1回だけ実行されます。
なぜ作ったのですか?
Node.jsサービスを運用していると、キャッシュレイヤーを積み重ねるやり方はおおむね似たような手順をたどります。インメモリキャッシュから始めて、インスタンスが増えると Redis をつなぎ、その後 stampede 問題に直面し、インスタンス間でキャッシュの不整合が起きて……。それぞれは解決できる問題ですが、これを本番運用レベルで一度に組み合わせるのは、思った以上に手間がかかりました。
そこで、その作業を一度だけきちんとやっておこうという考えで作りました。
主な機能は何ですか?
コア動作
- レイヤード読み取り + 自動 backfill(L1 ミス → L2 参照 → L1 を補完)
- Stampede prevention: 同時リクエスト100件 → fetcher 実行1回
- Distributed single-flight: Redis 分散ロックでインスタンス間の重複実行を排除
- Redis pub/sub ベースの L1 invalidation bus(インスタンス間のメモリキャッシュ同期)
無効化 / 新鮮度
- タグベースの無効化、ワイルドカード/プレフィックス無効化
- Stale-while-revalidate、Stale-if-error
- Sliding TTL、Adaptive TTL、Refresh-ahead
耐障害性
- Graceful degradation: Redis 障害時にレイヤーをスキップして自動復旧
- Circuit breaker
- Strict / best-effort 書き込みポリシー
可観測性
- Prometheus exporter、OpenTelemetry フック
- レイヤーごとのレイテンシ測定、イベントフック
- Admin CLI (
npx layercache stats|keys|invalidate)
フレームワーク統合
Express、Fastify、Hono、tRPC、GraphQL、Next.js
ベンチマークの数値が気になります
単一コア VM + 実際の Docker Redis を基準にしています。
| シナリオ | 平均レイテンシ |
| L1 メモリのウォームヒット | 0.005 ms |
| L2 Redis のウォームヒット(1 KiB) | 0.193 ms |
| キャッシュなし(DB シミュレーション) | 5.030 ms |
- HTTP スループット:
/layered16,211 req/s vs/nocache158 req/s - Stampede: 同時75リクエスト → origin fetch 5回(キャッシュなしでは375回)
- Distributed single-flight: 同時60リクエスト → origin fetch 1回
ベンチマーク手法全体と raw 結果は docs/benchmarking.md にまとめてあります。
既存ライブラリとは何が違いますか?
node-cache-manager、keyv、cacheable はどれも良い選択肢です。違いを簡単にまとめると次の通りです。
- Stampede prevention / Distributed single-flight: 3つのライブラリはいずれも標準では提供していません。layercache はこの2つを中核として設計しました。
- Cross-instance L1 invalidation: Redis pub/sub によって、インスタンス間のメモリキャッシュを自動で同期します。マルチインスタンス環境でも安心してメモリキャッシュを使えます。
- Auto backfill: 下位レイヤーでヒットした際に上位レイヤーを自動で埋めます。
- Graceful degradation + Circuit breaker: Redis が落ちてもサービスは動き続けます。
インストールとリンク
npm install layercache
- GitHub: https://github.com/flyingsquirrel0419/layercache
- npm: https://www.npmjs.com/package/layercache
設計上の判断、特に single-flight の調整方式や graceful degradation の動作について気になる点があれば、気軽に質問してください。
4件のコメント
良いライブラリですね!
Redisが設計に含まれている理由はありますか? 読み取り用インスタンスが複数同時に立ち上がることを想定している状況でしょうか? そうであれば、(ローカルの)DiskはRedisより前のレイヤーに配置されるべきではないでしょうか?
Redis が入っているのは、サーバーが複数台あることを前提にしているからです。各サーバーのメモリはそれぞれ異なる値を持っている可能性があるため、Redis が「共有された真実」の役割を果たします。
Disk が Redis の後に来るのは、Redis が同じローカルネットワーク上にある前提ならそのほうが高速だからです。ベンチマークでは Disk は約 2ms、Redis は約 0.02ms です。ただし、Redis が遠くにあったりネットワークの状態が悪かったりする場合は、ローカル Disk のほうが速いこともあり、その場合は順序を入れ替えるのが適切です。ライブラリも順序を強制せず、ユーザーが直接決められる構造になっています。
Disk はどこにあるとしても、速度を競うというよりは、Memory と Redis がどちらも落ちたときに生き残る最後の保険としての役割が主な目的です。
設計意図のご説明ありがとうございます。
すべてのリモート呼び出しをローカルディスクへの書き込みとして保存しておき、リモート呼び出しが失敗したときにはディスクから読み込む、ということですよね? キャッシュレイヤーに Disk が本当に必要かどうかも検討してみるとよいかもしれません。
DiskLayer はそのようなパターンではなく、単なる通常のキャッシュレイヤーとして動作します。つまり、読み書きの両方を行い、上位レイヤーでミスした場合に順番にアクセスしていく構造です。混乱を招いてしまい、失礼しました。
ご指摘の「リモート呼び出しの結果をディスクに保存しておき、失敗時に読み出す」パターンは、実際には stale-if-error オプションのほうが近いのですが、それはメモリ上に保持しているため、プロセスを再起動すると消えてしまいます。
また、DiskLayer が本当に必要なのかというご指摘については、うーん、実際にはほとんどのマルチインスタンス環境では Memory → Redis だけで十分で、Disk がレイヤーとして入ってくると、シリアライズのコストやファイル管理の複雑さが付いてくるんです。