インメモリDBのアンチキャッシングと退避戦略
メモリが足りなくなった瞬間に性能が崩れる不安を、冷データだけをディスクへ逃がす仕組みで解けます。従来バッファプールと逆向きの「アンチキャッシング」の原理を押さえれば、容量超過に強い設計が見通せます。
- 1.アンチキャッシングはインメモリDBの容量超過対策で、主記憶を一次格納とし、冷たいタプルだけをディスクへ退避する。ページ単位で常にディスクを正本とする従来バッファプールと正本/二次の役割が逆転している。
- 2.退避はページではなくタプル粒度で行い、LRU連鎖などで冷データを選ぶ。退避済みタプルへのアクセスは「ミス」ではなく実行を中断させる例外として扱い、非同期で取り込んでからトランザクションを再実行する。
- 3.従来バッファプールが固定フレームへの間接参照とページ置換を毎アクセスに払うのに対し、アンチキャッシングは常駐部を直接ポインタで触れ、退避部だけに追加コストを集中させる。ホットセットがメモリに収まる限り性能劣化が出ない。
なぜインメモリDBに「退避」が要るのか
インメモリDB(H-Store/VoltDB、MemSQL、SAP HANA のインメモリ層など)は、データ全体が主記憶に収まる前提で設計されています。ディスク I/O を待たない分、従来のディスクベースDBにあったバッファプールの間接参照・ラッチ・ページ置換のオーバーヘッドを丸ごと省けるのが速さの源です。タプルは固定アドレスのメモリ上に置かれ、索引はそこへ直接ポインタで届きます。
ところが現実のデータは時間とともに増え、いつかは物理メモリを超えます。「全部メモリに載る」前提が崩れた瞬間、何もしなければプロセスは OOM で死ぬか、OS のスワップに頼ることになります。OS スワップはDBがアクセス頻度を知らないまま冷温無差別にページを追い出すため、ホットなタプルまでディスクへ飛ばされて性能が予測不能に崩れます。そこで「容量超過に強いインメモリDB」を作るための仕組みが、アンチキャッシング(anti-caching)です。
OS の仮想記憶はページ(典型 4KB)単位で、DBの論理的なアクセス頻度を一切知りません。ホットなタプルと、たまたま同じ OS ページに同居する冷たいタプルが一緒に追い出される(フォルスシェアリング的な巻き添え)うえ、メジャーフォールトが同期的にトランザクションをブロックします。DBが自分のアクセス統計に基づいて「冷たいものだけ」を退避するからこそ、性能が予測できるのです。
アンチキャッシングの基本発想――正本がメモリ側にある
名前のとおり、アンチキャッシングは従来のキャッシュ階層を逆さまにします。
従来のディスクベースDB(バッファプールとページ置換)では、**ディスク上のページが常に正本(プライマリ)**で、バッファプールはその一部を載せた揮発的なキャッシュにすぎません。プールから追い出してもディスクに本体が残るので、追い出しは「キャッシュからの除去」です。
アンチキャッシングでは逆に、主記憶が一次格納(プライマリ)で、ディスクは容量があふれた冷データだけを逃がす二次退避先になります。だから「ディスクへ書く」操作は、キャッシュからの追い出し(caching の逆=anti-caching)として働きます。退避したタプルはメモリ側から消え、その存在は「ここはディスクに退避済み」という小さな目印に置き換わります。正本がメモリ側にある、という一点が設計のすべてを規定します。
| 観点 | 従来バッファプール | アンチキャッシング |
|---|---|---|
| 正本(プライマリ) | ディスク上のページ | 主記憶上のタプル |
| 移動の単位 | 固定長ページ(8KB/16KB) | タプル(可変・行単位) |
| 常駐データへのアクセス | ページテーブル経由の間接参照+ピン | 索引から直接ポインタで到達 |
| 欠落時の意味 | キャッシュミス(同期で読み込み待ち) | 退避例外(トランザクション中断→再実行) |
| 全体が載るとき | なお間接参照コストを払う | 退避ゼロ・追加コストもゼロ |
退避はページではなくタプル粒度
従来DBがページ単位で動くのは、ディスク I/O の最小単位がブロックだからです。アンチキャッシングは正本がメモリにあり、退避は性能ではなく容量のために行うので、最小単位に縛られる理由がありません。むしろタプル(行)粒度で退避するほうが理にかなっています。
理由は局所性です。OLTP のアクセスはタプル単位で偏りが強く、「ホットな行とコールドな行が同じページに混在」しがちです。ページ粒度だと冷たい行を逃がすたびにホットな行を巻き込んでしまう(前述の巻き添え)。タプル粒度なら冷たい行だけを正確に選んで退避でき、ホットセットの密度を高く保てます。退避するタプルは、まとめてブロック(複数の冷タプルを束ねた退避単位)にパックしてディスクへ書き、取り込みもブロック単位で行うことで I/O をならします。
冷たさの判定には、グローバルな LRU 連鎖を各テーブルやパーティションごとに持ち、アクセスのたびにそのタプルを連鎖の先頭へ動かす方式が代表的です。ただし全アクセスでポインタを付け替えるとそれ自体が重いので、抽出を一定間隔でサンプリングする実装もあります。メモリ使用量が上限を超えたら、連鎖の末尾(最も長く使われていない冷タプル)から必要量ぶんを退避します。
退避(evict)の流れ:
if メモリ使用量 > 上限:
冷タプルを LRU 連鎖の末尾から K 件選ぶ
それらを1つの退避ブロックにパックしてディスクへ書く
各タプルの索引エントリを「退避済みマーカ(block_id, offset)」に張り替える
メモリ上のタプル本体を解放する
アクセス時の取り込み――「ミス」ではなく「中断と再実行」
ここがバッファプールとの最大の違いです。従来DBではページミス時、その場でディスク読み込みを待ち、読めたら処理を続けます(同期ブロッキング)。インメモリDBは「待たないこと」が前提のアーキテクチャ(多くは1スレッドがパーティションを占有して直列実行)なので、トランザクションの最中に同期 I/O でブロックされると、その間そのパーティションの他の全トランザクションまで止まってしまいます。
そこでアンチキャッシングは、退避済みタプルへのアクセスをミスではなく例外として扱います。
- 退避済みマーカに当たったら、必要なタプル(やブロック)を控えて、**トランザクションを中断(abort)**する(まだ何も書いていない pre-pass として走らせる実装もある)。
- 中断中に、必要なタプルを非同期でディスクから取り込み、メモリへ戻して索引を実体ポインタへ張り直す(unevict)。この間、当該パーティションは他のトランザクションを処理できる。
- 取り込みが済んだら、元のトランザクションを最初から再実行する。今度は対象タプルが常駐しているので、I/O 待ちなしで完走する。
この「中断→非同期取り込み→再実行」により、同期 I/O によるパーティション全体の停止を避けつつ、退避データへもアクセスできます。代償は、運悪く退避データに触れたトランザクションが一度やり直しになることですが、ホットセットが正しくメモリに収まっていればこの再実行は稀です。
1回の再実行で済ませるには、最初の実行でそのトランザクションが触れる退避タプルを全部見つけて一括取り込みしておく必要があります。1件取り込んで再実行→また別の退避タプルに当たって再々実行…とならないよう、初回はデータを書き換えない探索パスとして走らせ、必要な退避タプルを集めてからまとめて unevict する設計が使われます。取りこぼすと再実行回数が膨らみます。
なぜ常駐部に追加コストが乗らないのか
アンチキャッシングの設計上の旨味は、ホットセットがメモリに収まる限り、退避機構が一切顔を出さないことです。索引は実体タプルへ直接ポインタで届き、ページテーブル参照もピンもページ置換判定もありません。退避が一度も起きていなければ、純粋なインメモリDBとして全速で動きます。
対して従来バッファプールは、全データがメモリに載っていても毎アクセスでページテーブルの間接参照とピン操作、置換アルゴリズムの更新(参照ビットや使用回数の維持)を払い続けます。これはディスクが正本である以上避けられない構造的コストです。アンチキャッシングは追加コストを「退避が起きた冷データ」だけに局所化する点で、インメモリ前提に最適化されています。
LSM-Tree も新しいデータをメモリ(memtable)に置き、あふれたら下層へ流しますが、向きが違います。LSM は書き込みを吸って下層へ沈める書き込み最適化で、正本はあくまで永続層(SSTable)側にあります。アンチキャッシングは読み書きされる作業集合そのものをメモリに常駐させ、冷えたものだけを退避する読み中心の発想です。両者とも「メモリ+階層的にディスク」ですが、何を正本とし何を逃がすかが逆です。
耐久性との関係――退避とログは別問題
ここを混同しがちなので明確に分けます。アンチキャッシングの退避(eviction)は容量管理であって、耐久性(durability)とは別の関心事です。退避していないホットなタプルもクラッシュすれば消えるので、インメモリDBは別途、コミットの永続化機構を持ちます。典型はWAL(先行書き込みログ)とスナップショット(チェックポイント)の併用で、ログを順次ディスクに書き、定期的に全状態をスナップショットして復旧の起点にします。
つまりディスクには「退避された冷タプル(容量のため)」と「ログ+スナップショット(耐久性のため)」が別々の目的で書かれます。退避ブロックは正本なので失えませんが、その耐久性もログとスナップショットの枠組みで守られます。両者を切り分けて考えるのが、アンチキャッシングを正しく理解する鍵です。
「アンチキャッシングと従来バッファプールの違いは」には、正本がメモリ側かディスク側か(退避はキャッシュ追加でなくキャッシュ除去)、移動単位がタプルかページか、欠落時が中断・再実行か同期ミスか、の3点で答えます。「なぜ OS スワップではダメか」は、DBがアクセス頻度を知らず冷温無差別にホットページまで追い出し性能が予測不能になるから。「退避すれば耐久性も担保されるか」にはノー、退避は容量管理でありコミットの永続化は WAL とスナップショットが別途担う、と答えられると強いです。
まとめ
アンチキャッシングは、インメモリDBが容量超過に直面したときの答えです。従来バッファプールが「ディスクが正本・メモリはキャッシュ」だったのを逆さにし、主記憶を正本、ディスクを冷データの退避先とします。退避はタプル粒度で冷データだけを狙い、退避済みデータへのアクセスは同期ミスではなく中断→非同期取り込み→再実行として扱うことで、直列実行アーキテクチャを止めずに容量を超えたデータも扱えます。
最大の利点は、ホットセットがメモリに収まる限り常駐部に追加コストがまったく乗らないこと。間接参照とページ置換を毎アクセスに払う従来方式と対照的です。退避(容量管理)と耐久性(ログ・スナップショット)を切り分けて捉えるのも要点でした。比較対象となる従来方式はバッファプールとページ置換を、インメモリ前提でOLTPとOLAPを同居させる発展形はHTAP アーキテクチャを、退避とは別に動く永続化規律はWAL の仕組みを参照してください。
データベース Article
インメモリDBのアンチキャッシングと退避戦略を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
インメモリDB
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 5
導入後に効く点
退避はページではなくタプル粒度で行い、LRU連鎖などで冷データを選ぶ。退避済みタプルへのアクセスは「ミス」ではなく実行を中断させる例外として扱い、非同期で取り込んでからトランザクションを再実行する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 5
判断チェックリスト
- 自社の用途が「インメモリDB / アンチキャッシング」に近いか確認する。
- 強みである「アンチキャッシングはインメモリDBの容量超過対策で、主記憶を一次格納とし、冷たいタプルだけをディスクへ退避する。ページ単位で常にディスクを正本とする従来バッファプールと正本/二次の役割が逆転している。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。