SQS のメッセージは本当に「少なくとも 1 回」届くのか - At-Least-Once 配信の仕組みと落とし穴
SQS Standard キューの At-Least-Once 配信がなぜ重複を生むのか、可視性タイムアウトの内部動作、FIFO キューの Exactly-Once との違い、冪等性設計の必要性を解説します。
At-Least-Once 配信とは何か
SQS Standard キューは「At-Least-Once Delivery」(少なくとも 1 回の配信) を保証しています。これは、送信されたメッセージが少なくとも 1 回はコンシューマーに配信されることを意味しますが、同じメッセージが 2 回以上配信される可能性があることも意味します。なぜ重複が発生するのでしょうか。SQS はメッセージを複数のサーバーに冗長化して保存しています。コンシューマーがメッセージを受信し、処理を完了して削除リクエストを送信するまでの間に、別のサーバーから同じメッセージが別のコンシューマーに配信されることがあります。分散システムでは、ネットワークの遅延やサーバー間の同期のタイムラグにより、この種の重複は原理的に避けられません。重複配信の頻度は公表されていませんが、AWS のドキュメントでは「まれに」発生すると記述されています。本記事では、AWS 公式ドキュメントが At-Least-Once Delivery として説明しているこの挙動の内部の仕組みを、記事末尾の一次情報 (公式ドキュメント) へのリンクとあわせて解説します。
可視性タイムアウトはどのように重複処理を防ぐのか
SQS の可視性タイムアウト (Visibility Timeout) は、メッセージの重複処理を防ぐための仕組みです。コンシューマーがメッセージを受信すると、そのメッセージは他のコンシューマーから一定時間「見えなくなり」ます。この時間が可視性タイムアウトで、デフォルトは 30 秒です。コンシューマーが可視性タイムアウト内に処理を完了し、DeleteMessage API でメッセージを削除すれば、メッセージは正常に処理されます。可視性タイムアウト内に削除されなかった場合、メッセージは再びキューに戻り、別のコンシューマーに配信されます。ここに落とし穴があります。処理に 30 秒以上かかる場合、可視性タイムアウトが切れてメッセージがキューに戻り、別のコンシューマーが同じメッセージを受信して二重処理が発生します。対策は 2 つあります。第 1 に、可視性タイムアウトを処理時間より十分に長く設定すること。第 2 に、処理中に ChangeMessageVisibility API で可視性タイムアウトを延長すること (ハートビートパターン) です。SQS を Lambda のイベントソースとして使う場合、AWS はキューの可視性タイムアウトを関数タイムアウトの 6 倍以上に設定することを推奨しています。スロットリングが起きた際に Lambda が再試行する時間を確保するためです。Lambda が自動で設定してくれるわけではないため、キュー側で自分で設定する必要があります。
- 1. 受信 (ReceiveMessage)コンシューマーがメッセージを受け取った瞬間から可視性タイムアウト (既定 30 秒) が始まり、その間ほかのコンシューマーからはメッセージが見えなくなる。
- 2. 処理中タイムアウトのカウントダウンは処理の進み具合とは無関係に進む。処理が長引きそうなら ChangeMessageVisibility で期限を延長する (ハートビート)。
- 3. 分岐 A - 期限内に DeleteMessage を呼べたメッセージはキューから消え、処理は 1 回で完了する。重複は発生しない。
- 4. 分岐 B - 期限内に削除できなかったメッセージは再び可視になり、別のコンシューマーへ配信される。処理そのものは成功していても、ここで重複処理が生まれる。
- 5. 再試行の打ち切り受信回数が maxReceiveCount を超えると、メッセージはデッドレターキューへ移動し、際限のない再配信が止まる。
FIFO キューの Exactly-Once 処理
2016 年に導入された FIFO (First-In-First-Out) キューは、メッセージの順序保証と重複排除を提供します。FIFO キューでは、メッセージ送信時に MessageDeduplicationId を指定すると、5 分間の重複排除ウィンドウ内で同じ ID のメッセージは 1 回だけ配信されます。これにより、プロデューサー側の重複送信 (ネットワークタイムアウトによるリトライなど) が排除されます。コンシューマー側の重複処理も、MessageGroupId による順序保証で緩和されます。同じ MessageGroupId のメッセージは、前のメッセージが削除されるか可視性タイムアウトが切れるまで次のメッセージが配信されないため、同一グループ内での並列消費は発生しません。ただし、FIFO キューにはスループットの制限があります。バッチ処理なしで秒間 300 メッセージ、バッチ処理ありで秒間 3,000 メッセージが上限です。高スループットモードを有効にすると上限を大幅に引き上げられます (上限値はリージョンごとに異なり、現行値は SQS 公式の quotas ページに記載されています)。それでも Standard キューの事実上無制限のスループットと比較すると制限は残ります。順序保証と重複排除が不要なワークロードでは、Standard キューの方がスループットとコストの面で有利です。
| 観点 | 標準キュー | FIFO キュー |
|---|---|---|
| 配信保証 | At-Least-Once (同じメッセージが 2 回以上届き得る) | 重複排除ウィンドウ内は 1 回だけの Exactly-Once 処理 |
| 順序 | ベストエフォート (送信順と入れ替わることがある) | MessageGroupId 単位で送信順を厳守 |
| 重複排除の仕組み | なし。コンシューマー側の冪等性で吸収する | MessageDeduplicationId により 5 分間の重複排除ウィンドウ |
| 同一グループの並列消費 | 制限なし。複数ワーカーで同時に処理できる | 前のメッセージが削除されるまで次を配信しない |
| スループット | 事実上無制限 | 1 秒あたり 300 メッセージ (10 件バッチで 3,000)。高スループットモードで引き上げ可 |
| 提供開始 | サービス開始時から | 2016 年 |
冪等性設計 - 重複を前提としたアーキテクチャ
SQS Standard キューを使用する場合、コンシューマー側で冪等性 (Idempotency) を確保する設計が不可欠です。冪等性とは、同じ操作を複数回実行しても結果が変わらない性質です。たとえば、「ユーザー A の残高を 1,000 円に設定する」は冪等ですが、「ユーザー A の残高に 1,000 円を加算する」は冪等ではありません。後者を重複実行すると残高が 2,000 円になってしまいます。冪等性を実現する最も一般的なパターンは、処理済みメッセージの ID を DynamoDB に記録する方法です。メッセージを受信したら、まず DynamoDB に MessageId が存在するか確認し、存在すれば処理をスキップ、存在しなければ処理を実行して MessageId を記録します。DynamoDB の条件付き書き込み (ConditionExpression) を使えば、確認と記録をアトミックに実行できます。Lambda の Powertools ライブラリには、この冪等性パターンを簡単に実装できるデコレータが用意されています。関数に @idempotent デコレータを付けるだけで、DynamoDB ベースの冪等性チェックが自動的に組み込まれます。
デッドレターキュー - 処理できないメッセージの行き先
メッセージの処理が繰り返し失敗する場合、そのメッセージは永遠にキューに残り続け、コンシューマーのリソースを浪費します。デッドレターキュー (DLQ) は、指定回数の処理に失敗したメッセージを別のキューに移動する仕組みです。maxReceiveCount を 3 に設定すれば、3 回受信されても削除されなかったメッセージが DLQ に移動します。DLQ に移動したメッセージは、手動で確認して原因を調査し、修正後に元のキューに再送するか、破棄するかを判断します。DLQ の設計で見落としがちなのは、DLQ 自体のメッセージ保持期間です。SQS のメッセージ保持期間はデフォルト 4 日、最大 14 日です。DLQ のメッセージも同じ保持期間が適用されるため、14 日以内に対処しないとメッセージが自動的に削除されます。重要なメッセージを扱う場合は、DLQ に CloudWatch アラームを設定し、メッセージが到着したら即座に通知を受け取る体制を整えてください。2021 年に追加された DLQ リドライブ機能を使えば、DLQ のメッセージをコンソールから元のキューにワンクリックで再送できます。
参考資料 (AWS 公式ドキュメント)
本記事で解説した挙動の一次情報は、Amazon SQS 開発者ガイドの次のページです。Standard キューの配信保証を定義した Amazon SQS at-least-once delivery、処理中メッセージが他のコンシューマーから隠される仕組みを説明した Amazon SQS visibility timeout、FIFO キューの重複排除を説明した Exactly-once processing in Amazon SQS の 3 本です。本記事と公式ドキュメントの記述が食い違う場合は、公式ドキュメントを正としてください。