Dev Study
AWSサービスの内部原理 コース

100. DAXはDynamoDB専用のライトスルーキャッシュである — 結果整合性しか返せない理由まで設計から導ける

レッスン1で並べた「ライトスルー・TTL・LRU・レプリケーション」という語彙が、DAXという1つの製品にどう全部実装されているか、そして「なぜDAXは強い整合性を自分では返せないのか」を、リクエストが流れる図で上から下へ追っていきます。

① DAXは透過プロキシ — 自分で答えるか、素通しするか

アプリケーションDynamoDB APIをそのまま話す
コードは普通のDynamoDBクライアントとほぼ同じ
APIの種類で3つの経路に分かれる
DAXクラスタVPC内・1個以上のノード
「このAPIを自分で処理できるか?」で分岐
自分で応答GetItem / BatchGetItem / Query / Scan(結果整合性の読みのとき)
素通しConsistentRead=trueの読み・TransactGetItems・CreateTable等の管理API
ライトスルー書き込みは本体へ送った上でDAX自身もキャッシュに書く(④で詳述)
  • 公式の言い回し: 「If DAX can process one of these API requests directly, it does so. Otherwise, it passes the request through to DynamoDB.」
  • DAXが読みで自分で応答できるのは GetItem / BatchGetItem / Query / Scan の4つ、ただし eventually consistent reads(既定)のときだけ。
  • 書き込みは純粋な素通しではない。公式は Read operations / Write operations (write-through) / Other operations の3分類で、書き込みはDynamoDBへ送った上でDAXもキャッシュに書く第3の経路。
  • テーブル管理API(CreateTable・UpdateTable等)はDAXが認識せず、アプリはDynamoDBに直接アクセスする必要がある。

DAXは「速いDynamoDB」ではなく、DynamoDB APIの前に立つ透過プロキシである。処理できるものだけ自分で答え、それ以外は素通しする——この「自分の守備範囲を持つプロキシ」という構造が、以降のすべての制約の出発点になる。API Gateway編で見たリバースプロキシと同じ骨格だが、DAXは「後ろに渡す前に手元のキャッシュで答える」層が足された形。

② クラスタの中身は2つの独立したキャッシュ

DAXクラスタ性質の違う2つのキャッシュが同居
2つの関係は
アイテムキャッシュGetItem / BatchGetItem の結果。キー=プライマリキー
既定TTL 5分・LRU常時有効
クエリキャッシュQuery / Scan の結果セット。キー=パラメータ値(どのQueryを投げたか)
TTLは作成時に指定・LRU常時有効
互いに独立一方への書き込みは他方に影響しない
  • アイテムキャッシュの既定TTLは5分: 「The item cache has a Time to Live (TTL) setting, which is 5 minutes by default.」
  • LRUは両キャッシュとも常に有効・ユーザー設定不可。満杯なら「older items (even if they haven't expired yet)」を追い出す=期限前でも古いものから捨てる。TTL(時間切れ)とLRU(場所切れ)は別々の追い出し理由。
  • TTL=0の意味は2つのキャッシュで違う。アイテムキャッシュはLRUとライトスルーでしか更新されなくなり、クエリキャッシュは結果がキャッシュされなくなる。
  • 公式の明言: 「the DAX item cache and query cache serve different purposes, and operate independently from one another.」

DAXの中には別々の生き物が2匹いる。プライマリキーで引く「アイテムキャッシュ」と、投げたクエリのパラメータで引く「クエリキャッシュ」。この2匹が独立していることが、③の最大の罠の正体になる。DynamoDB編でパーティションキーが「アイテムを一意に置く座標」だったのと同じ原理でアイテムキャッシュもプライマリキーを座標に使うが、クエリキャッシュの座標は「キーの値」ではなく「クエリそのもの」である点が決定的に違う。

③ 最大の罠 — アイテムを更新してもクエリキャッシュは無効化されない

Step1: QueryDocId=101, RevisionNumber >= 5
テーブル: DocumentRevisions(PK=DocId, SK=RevisionNumber)
しかしクエリキャッシュには触れない
Step2: PutItemDocId=101, RevisionNumber=20 を追加
ライトスルーで本体とアイテムキャッシュには書かれる
キャッシュヒット
Step3: 同じQueryDocId=101, RevisionNumber >= 5 を再実行
古い結果が返る20番のリビジョンは含まれない
TTLが切れるまでこの古い結果が返り続ける
  • 公式の明言: 「Updates to the item cache, or to the underlying DynamoDB table, do not invalidate or modify the results stored in the query cache.」
  • 同: 「The PutItem operation from step 2 is only reflected in the DAX query cache when the TTL for the Query expires.」
  • 理由は②の「2匹は独立」。DAXは個々のアイテム更新を根拠にQuery/Scanの結果セットを無効化しない。
  • Scanでアイテムキャッシュを「温める」こともできない(結果はクエリキャッシュにしか入らない)。

「本体を更新したのに古いクエリ結果が返る」のはバグではなく設計。クエリキャッシュは「その結果セットを作った瞬間のスナップショット」であり、後からアイテムが変わっても自分では気づかない。これはキャッシュ無効化の難しさ(次のCloudFrontレッスン群の主題)の予告編そのもの——「何かが変わったとき、どのキャッシュエントリを捨てるべきか」をキャッシュ側から知るのは本質的に難しい。

④ 書き込みの順序 — ライトスルーは「本体が先、キャッシュが後」

アプリPutItem / UpdateItem / DeleteItem / BatchWriteItem
書き込みの成否で分岐
DynamoDB本体先に本体へ書き込み
成功(2) DAXがアイテムキャッシュに書く → (3) 成功を呼び出し元へ返す
両方成功して初めて成功
失敗スロットリング等。キャッシュには一切書かず例外を呼び出し元へ返す
  • 公式の明言: 「The operation is successful only if the data is successfully written to both the table and to DAX.」
  • 失敗時: 「If a write to DynamoDB fails for any reason, including throttling, the item is not cached in DAX.」=キャッシュだけ新しい、という事故を順序で防いでいる。
  • 補足: TransactWriteItems もライトスルー扱いだが処理順が特別。本体成功→呼び出し元へ成功を返す→その後背景で各アイテムを TransactGetItems で読み直してアイテムキャッシュへ格納する(直列化可能性の保証のため)。成功応答が先・キャッシュ格納が後、という点が上の4APIと逆。

「本体を先に、キャッシュを後に」という順序には意味がある。逆順(キャッシュ先)だと、本体書き込みが失敗したときにキャッシュだけ未来の値を持ってしまう。メッセージング編の配信保証で問うた「失敗したとき何が残るか」と全く同じ問い方で、DAXは「失敗時にキャッシュには何も残らない」を順序で保証している。

⑤ なぜ強い整合性を「自分では」返せないのか

プライマリノードクラスタ内で更新を受ける
だから強い整合性が要る読み(ConsistentRead=true)は
リードレプリカ×N完了前は古い値を返しうる
同じキーを2クライアントが読んで違う値を見ることがある
結果はキャッシュせずそのまま返すだけ
DAXは素通し自分では答えずDynamoDBへ渡す
DynamoDBが応答最新の値は常に本体だけが知っている
  • ノード間レプリケーションは結果整合性で「usually takes less than one second」。故に「two clients to read the same key ... but receive different values」が起こりうる。
  • ConsistentRead=true の GetItem / BatchGetItem / Query / Scan はDynamoDBへ素通しされ、結果はキャッシュされない。TransactGetItems も同じ扱い。
  • 公式の理由文: 「DAX can't serve strongly consistent reads by itself because it's not tightly coupled to DynamoDB.」——キャッシュはあくまで別プロセス・別ノード群で、本体の最新状態を常時同期しているわけではない。

DAXが結果整合性しか返せない理由は「怠慢」ではなく構造そのものにある。DAXは①で見た通りDynamoDBの手前に立つ独立したキャッシュ層で、本体と密結合していない。だから強い整合性が要る読みは自分で答えず素通しするしかない。DynamoDB編で見た結果整合性が、ここではさらに「キャッシュ層のレプリケーション遅延」という2段目として重なっている。

⑥ 不在も情報 — ネガティブキャッシュと、汚さないためのwrite-around

ネガティブキャッシュGetItem(存在しないキー) → DynamoDBに無い
write-around大量ロード時の選択肢。DAXを迂回する
両者に共通するのは
空の結果を保存エラーではなく「空」をキャッシュしTTLまで保持
再度同じGetItemはDynamoDBを見ずに「空」を即返す
本体へ直接書く書き込みでキャッシュを汚さない
読まれるデータだけが後のDAX経由の読みで自然にキャッシュに乗る
入れる/入れないを選ぶ何をキャッシュに入れ、何を入れないかは設計者が選べる
  • ネガティブキャッシュの公式説明: 「Instead of generating an error, DAX caches an empty result and returns that result to the user.」アイテムキャッシュ・クエリキャッシュ両方でサポート。
  • ネガティブエントリが消える条件: 「until its item TTL has expired, its LRU is invoked, or the item is modified using PutItem, UpdateItem, or DeleteItem.」
  • write-aroundの理由: 大量書き込みをDAX経由にすると、LRUが働いて「まだ読まれてもいないデータ」で読みキャッシュを圧迫し、読みキャッシュとしての有効性が下がる。
  • write-aroundの代償: アイテムキャッシュはDynamoDBと同期しなくなる(直接書いた分をDAXは知らない)。

「存在しない」という答えもキャッシュする価値のある情報である——DNSのネガティブキャッシュと同型の発想。write-aroundは逆に「書いてもキャッシュに乗せない」選択で、キャッシュを読み最適化のために綺麗に保つ。まとめれば、DAXは読みの速さを買って新しさの保証を支払っている——ライトスルー・結果整合性のレプリケーション・無効化されないクエリキャッシュ・素通しされる強い整合性は、全部「速く読むために本体と密結合しない」という1つの選択から導かれる帰結である。

公式ドキュメントで詳しく ↗