Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】How an Eventually Consistent DynamoDB GSI Let Mobile Retries Create 1,400 Duplicate Orders

2026年10月30日に「Dev.to」が公開したITニュース「How an Eventually Consistent DynamoDB GSI Let Mobile Retries Create 1,400 Duplicate Orders」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

モバイルアプリのリトライとDynamoDBのGSIの「結果整合性」が重なり、重複注文が1400件発生した。GSIはデータの反映が遅れるため、冪等性チェックが機能しなかった。解決策は、DynamoDBのベーステーブルへの「条件付き書き込み」で原子性を保証する冪等性チェックに変更することだった。これにより、確実に重複を防げるようになった。

ITニュース解説

ある日、オンラインストアで突然システム障害が発生した。普段の3倍以上もの注文が殺到し、アラートが鳴り響いた。担当者が急いで調査を始めると、システムが遅くなっているわけではなく、誰も意図していない重複した注文が大量に生成されていることがわかった。

最初に疑われたのは、ユーザーが利用するモバイルアプリの通信トラブル対策として、最近改良されたリトライ機能だった。通信が不安定なときに、アプリが同じ注文を何度も送ってしまうのではないかという仮説だ。しかし、ログを詳細に調べると、アプリからは一度の操作に対して一つの注文リクエストしか送られていないことが判明した。リトライは行われていたが、それは正しい方法で、重複した注文を生み出す原因ではなかったのだ。

次に、API Gatewayと呼ばれる、アプリからのリクエストを受け付けるサーバーが原因ではないかと疑われた。API Gatewayがバックエンドの処理がタイムアウトしたと誤解し、同じリクエストを再送してしまうケースがあるからだ。しかし、これもログを確認すると、API Gatewayからも各注文リクエストは一度しかバックエンドに送られていなかった。ここでも重複の原因は見つからなかった。

調査が進むうち、奇妙な事実が浮かび上がってきた。重複して作成された注文のペアは、どちらも「冪等性キー」という同じ識別子を持っていたのだ。冪等性キーとは、同じリクエストが複数回送られてきても、一度の処理として扱うための重要な仕組みだ。例えば、「この商品を注文する」というリクエストを一度処理したら、同じ冪等性キーを持つリクエストが後から来ても、それは既に処理済みとして新しい注文を作らない、というように機能する。もしこの冪等性チェックが正しく機能していれば、同じ冪等性キーで複数の注文が作られることはありえない。つまり、問題は冪等性チェックそのものにあると結論付けられた。

オンラインストアの注文作成処理のコードを見てみると、冪等性チェックは教科書通りに実装されているように見えた。具体的には、データベース(ここではAWSのDynamoDB)に対して、冪等性キーを元に既存の注文がないかを確認し、もし見つかればその注文IDを返し、見つからなければ新しい注文を作成するという流れだ。このとき、既存の注文を検索するために、「Global Secondary Index(GSI)」という特別なインデックスが使われていた。GSIは、特定の条件でデータを高速に検索できるようにするための仕組みだ。

しかし、このGSIを使った検索には、重要な落とし穴があった。DynamoDBのGSIは、「結果整合性(Eventually Consistent)」という特性しか持たないのだ。結果整合性とは、データが更新されても、その変更がGSIに反映されるまでに一時的な遅延がある可能性がある、という意味だ。GSIは、ベースとなるテーブルのデータとは独立して更新されるため、常に最新の状態を反映しているとは限らない。一方、ベーステーブルからの読み取りは「強整合性(Strongly Consistent)」という、常に最新のデータが読み取れる保証を選択できる場合があるが、GSIではその選択肢は存在しない。この事実はAWSのドキュメントにも明記されているが、これまでチームの誰もがこの遅延を意識する必要がなかったため、見過ごされてしまっていた。

問題が発生した時間帯に、特定の地域でモバイルキャリアの通信障害があったことが判明した。これにより、多くのユーザーのモバイルアプリで通信が不安定になり、アプリはリトライ処理を積極的に実行した。アプリのリトライ間隔は300ミリ秒だった。一方、DynamoDBのGSIの伝播遅延(ベーステーブルの更新がGSIに反映されるまでの時間)は、通常は100ミリ秒未満だったが、この通信障害と大量リクエストが重なったことで、一時的に400~600ミリ秒にまで跳ね上がっていたのだ。

この二つの要素が重なることで、最悪のシナリオが実現した。ユーザーが注文ボタンをタップし、最初のリクエストが送信される。このリクエストはベーステーブルに注文を作成する。しかし、通信障害によりアプリは応答を受け取れず、300ミリ秒後に次のリトライリクエストを送信する。このとき、最初の注文はベーステーブルには書き込まれているものの、GSIにはまだ反映されていない可能性があった。そのため、二番目のリクエストがGSIを検索すると、「まだ注文は存在しない」と判断され、新たな重複注文が作成されてしまったのだ。つまり、冪等性チェックは機能しているように見えて、GSIの「結果整合性」という特性によって、チェックのタイミングで最新の情報が見えなかったために、誤った判断をしてしまったのである。

この問題の根本的な解決策は、GSIの速度を上げるのではなく、DynamoDBが確実に「アトミックな(これ以上分割できない、不可分な)操作」を保証する仕組みを利用することだった。それは、「ベーステーブルに対する条件付き書き込み」だ。具体的には、次のような新しい解決策が導入された。

まず、「idempotency-locks」という専用の新しいテーブルが作られた。このテーブルは、冪等性キーをプライマリキーとして持つ。注文作成処理の最初に、この「idempotency-locks」テーブルに対して、新しい冪等性キーを持つレコードを書き込む「PutItem」操作を実行する。このとき、「ConditionExpression: attribute_not_exists(idempotencyKey)」という条件を付ける。これは、「もし同じ冪等性キーのレコードがまだ存在しなければ書き込む」という意味になる。この条件付き書き込みは、DynamoDBのベーステーブルに対して行われるため、二つのリクエストが同時に同じ冪等性キーの書き込みを試みても、必ずどちらか一方だけが成功し、もう一方は「ConditionalCheckFailedException」というエラーを返すことが保証される。

もし書き込みが成功したら、それはその冪等性キーでの最初の処理なので、新しい注文を生成し、その注文IDを返す。一方、「ConditionalCheckFailedException」が発生したら、それは既に別のリクエストが同じ冪等性キーで処理を成功させたことを意味する。この場合、エラーを返したリクエストは、「idempotency-locks」テーブルから、成功した方のリクエストが書き込んだ注文IDを「強整合性」で読み取って返し、重複注文の生成を防ぐ。さらに、このロック用テーブルには「TTL(Time To Live)」という属性も追加され、一定時間が経過した古いロックレコードはDynamoDBによって自動的に削除されるようにした。

この問題が発覚した22分間のうちに、合計1,400件もの重複注文が作成され、約61,800ドルの損害が発生した。幸いなことに、ほとんどの重複注文は修正後に実行された調整スクリプトによって出荷前に検出され、キャンセルまたはマージされた。しかし、210件の注文は既にピッキング、梱包、または出荷済みで、返品送料や顧客への返金というかたちで実損となった。

この事例から得られた教訓は非常に多い。まず、DynamoDBのGlobal Secondary Indexは、設計上「強整合性」の読み取りをサポートしない。これはパフォーマンスを向上させるための設計であり、データの「正しさ」をチェックする目的で使うと、今回の再現のような競合状態(レースコンディション)を招く危険がある。冪等性チェックは、読み取りと書き込みが分離されたパターンではなく、DynamoDBがアトミック性を保証する「条件付き書き込み」のような単一の操作として実現されるべきだ。

また、今回の問題は、データベースとは直接関係のないネットワーク環境の変化と、数週間前に導入されたモバイルアプリのリトライポリシーが組み合わさることで初めて顕在化した。システムの各部分が正しく機能していても、それらの組み合わせによって予期せぬ脆弱性が生まれることがある。

監視体制の重要性も再認識された。当初のアラートは「注文キューの深さ」という最終的な症状を監視していたため、問題発生から検出までに約20分かかってしまった。もし、新しい「idempotency-locks」テーブルで「ConditionalCheckFailedException」の発生率を監視していれば、問題の兆候は1分以内に検出できたはずだ。これは、システムの「症状」だけでなく、「メカニズム」にもアラートを設定することの重要性を示している。

最後に、誰もが初めから不注意だったわけではない。多くのプログラミング言語やデータベースでは、「キーが存在しないことを確認してから書き込む」というパターンは、通常、正しさの保証となる。しかし、DynamoDBは「整合性」について明確な区別をドキュメントで説明している。このギャップは、ドキュメントに書かれている保証と、実際にコードが依存していた保証との間の不一致から生じたものだった。システム開発においては、利用するサービスや技術の基本的な特性、特に「保証」について深く理解し、それをコードに正確に反映させることが不可欠であると、改めて強く認識させられた事例である。

関連コンテンツ

関連IT用語