DynamoDB の GSI が結果整合性である理由
アイテムを書き込み、すぐにそれをグローバルセカンダリインデックスでクエリすると、何も
返ってきません — 書き込みは成功し、ベーステーブルの GetItem はそのアイテムを問題なく
返すのに、です。
何も壊れていません。GSI の最も驚くべき性質に当たったのです。GSI のすべての読み取りは 結果整合性です。書き込みの後に、インデックスがまだ追いついていない短い窓があります。
DynamoDB の GSI は結果整合性ですか?
はい — グローバルセカンダリインデックスのすべての読み取りは結果整合性であり、それを無効化する方法はありません。書き込みはまずベーステーブルにコミットされ、それから非同期にインデックスへ伝播するため、書き込み直後に発行したクエリは古い、あるいは欠けた行を返すことがあります。DynamoDB は GSI 向けの ConsistentRead フラグを提供していません。
- GSI は別個の、非同期にレプリケートされるテーブル — 書き込みはまずベーステーブルに コミットされ、それからインデックスに伝播します。
- GSI には
ConsistentReadフラグが存在しない。 ベーステーブルと違い、ギャップを 閉じるために強い読み取りを強制できません。 - GSI ではなくベーステーブルから自分の書き込みを読む。 書き込み直後にはすでに プライマリキーを握っています。
- 一意性は GSI クエリではなく条件付き書き込みで強制する。 伝播のギャップは「これは 使われているか?」のチェックを競合に変えます。
症状:自分を「見つけられない」サインアップ
ユーザーアカウントサービスの Members テーブルを取り上げます。ベーステーブルは内部 id で
キーイングされていますが、ユーザーはメールでログインするため、メール参照の GSI があります。
| PK | SK | displayName | |
|---|---|---|---|
| ACC#a1f9c | PROFILE | ada@northwind.test | Ada L. |
| GSI1PK | GSI1SK |
|---|---|
| ada@northwind.test | ACC#a1f9c |
サインアップフローは2つのことを立て続けに行います。新メンバーを PutItem し、それから
他の誰もそのアドレスを請求していないか確認しプロフィールを読み込むために
Query EmailIndex WHERE GSI1PK = "ada@northwind.test" です。
その2つの呼び出しを数ミリ秒違いで実行すると、Query は 0件 を返しうります。1秒後に
もう一度やれば行はそこにあります。書き込みは失敗していません — インデックスがまだ更新
されていなかっただけです。
なぜこうなるのか:GSI は非同期にレプリケートされる
GSI は、自身のパーティションと自身のキースキーマを持つ 別個の、内部で管理されるテーブル です。あなたのベーステーブル書き込みと同じトランザクションの中で維持されているのでは ありません。
PutItem すると、DynamoDB はベーステーブルに永続的にコミットし、あなたの書き込みを承認し、
それから 各 GSI へ変更を非同期に伝播します。AWS の
GSI ドキュメント
は明快にこう述べています。GSI は結果整合性のある読み取りのみをサポートする、と。
ベーステーブル書き込みとインデックス更新の間の伝播遅延は、通常は1秒の何分の1かです — しかし負荷の下では 保証も上限もありません。上限があるかのように設計するのが罠です。
これはバグではありません。原典の Dynamo 設計のトレードオフです。2007 年の Amazon Dynamo 論文 は、強い整合性よりも可用性と分断耐性を選びました。
GSI はその系譜を受け継いでいます。緩い結合こそが、インデックスがベーステーブルとは独立して スケールし、書き込み可能であり続けることを可能にします。
200 OK と「変更をレプリケート」の間のギャップが、インデックス読み取りが古い窓です。
それを閉じる 整合性読み取りフラグはありません。
ベーステーブルとは違い — そこでは ConsistentRead = true を渡して強い整合性のある
GetItem/Query を強制できます — GSI はそのオプションをきっぱり拒否します。
LSI は できます、強く読むことが。ベーステーブルのパーティションを共有するからです。 その区別がなぜ存在するかは GSI と LSI を参照してください。
より巧妙な罠:欠けた新しい値だけでなく、古い値
行が欠ける場合は明白なものです。より静かなバグは 古い以前の値を読む ことです。
Ada がメールを ada@northwind.test から ada.l@northwind.test に変更したとします。
ベーステーブルはアトミックに更新されますが、一瞬の間、GSI はまだ 古い インデックス
エントリを返しうります。
新しい値に対する参照は外れる一方、捨てられた値はまだ解決されます。
さらに悪いことに:GSI をクエリして読んだものに基づいて書き戻すと、もはや存在しない値に 作用しかねません。あらゆる GSI 読み取りを、現実から遅れているかもしれないスナップショット として扱ってください。
それと戦うのではなく、それを前提に設計する
伝播の窓は実在するので、修正はあなたが切り替えるリトライのつまみではなく、アーキテクチャ 上のものです。4つのパターン、おおむね好ましい順に。
ベーステーブルから自分の書き込みを読む。 書き込み直後にはすでにプライマリキー (
ACC#a1f9c)を握っているので、GSI をクエリする代わりにベーステーブルに対して強い整合性のあるGetItemを行います。GSI は 別の アクセスパターン — 「メールを持っている、アカウントを探す」 — のためで あって、たった今行った書き込みを確認するためではありません。
一意性は GSI ではなくガードアイテムで強制する。 メールが未請求であることを証明する ために GSI クエリを決して信頼しないでください — 伝播のギャップが、それを2つの同時 サインアップが両方とも負けうる競合にします。
代わりに、メールそのものでキーイングした専用の一意性アイテム (
PK = "EMAIL#ada@northwind.test")を、attribute_not_exists(PK)のConditionExpressionを付けたTransactWriteItemsの中で書き込みます。アトミックに適用された強い整合性のあるベーステーブル条件こそが、実際に一意性を強制するものです。
TransactWriteItems: - Put member item (PK = ACC#a1f9c, SK = PROFILE) - Put uniqueness item (PK = EMAIL#ada@northwind.test) ConditionExpression: attribute_not_exists(PK)2つ目のサインアップが同じアドレスを競って取りに来たら、その条件が失敗しトランザクション 全体が拒否されます — GSI なし、伝播遅延なし、二重請求なし。
その
attribute_not_exists条件を、コードに組み込む前に DynamoDB Expression Builder で組み立ててプレビューしましょう。UX 上で遅延を許容する。 GSI 読み取りが本当に正しい道具のとき(既存 ユーザーの メールでのログイン)、窓は1秒未満で無害です — 確立されたアカウントはとうの昔に伝播 しています。
強い整合性のあるベーステーブルの道は、書き込み直後の読み取りの瞬間だけのために取っておき ましょう。
再クエリする、決めつけない。 ワークフローが出来たてのアイテムを GSI 越しに観測 しなければならないなら、空の結果を「存在しない」ではなく「まだ見えない」として扱い、 短いバックオフの後に再クエリします。
ただし、推測を完全に取り除くパターン 1 と 2 を優先しましょう。
伝播のギャップを自分で見る
直感を養う最速の方法は、それが起きるのを見ることです。DynoTable では、ベーステーブルに アイテムを put し、すぐに2つ目のタブで GSI をクエリします。
負荷のあるテーブルでは、インデックスがベースデータに遅れているのを時折捉え、次のリフレッシュ で収束するのを見られます。
遅延を自分のデータで見ることが、「ベーステーブルから自分の書き込みを読む」というルールを、 どんな図よりもはるかにしっかり定着させます。
落とし穴と次のステップ
- GSI の書き込み直後読み取りにロジックを依存させない。 一意性チェック、「書き込みは 着地したか」の確認、read-modify-write ループは、強い整合性のあるベーステーブルに属します。
- GSI で
ConsistentReadに手を伸ばさない — 許可されておらず、エラーになります。 - ベースキーがすでに答えるアクセスパターンを GSI としてモデリングしない。 プライマリ キーから読み取りを提供すれば、伝播の窓を完全にスキップできます。
正しいキーの形を選ぶことが シングルテーブル設計 のゲームの
すべてであり、Query が Scan に勝るときを知ることが、そもそもインデックスから遠ざけて
くれます(Query と Scan)。
一意性の ConditionExpression を DynamoDB Expression Builder
で構築してテストしましょう。そして DynoTable を試して ベーステーブルの
書き込みが GSI へ伝播するのをリアルタイムで見て、結果整合性の窓が噛まないようにキーを
設計してください。