SQS Visibility Timeoutの完全解説:メッセージ二重処理の原因と対策
SQSキューから取得したメッセージが複数のコンシューマーに重複処理される——この問題はVisibility Timeoutの設定ミスが原因であることがほとんどだ。処理時間がタイムアウト値を超えた瞬間、SQSはそのメッセージを「未処理」と判断して再度可視化し、別のワーカーが同じメッセージを拾ってしまう。
TL;DR:Visibility Timeoutとは何か
| 項目 | 内容 |
|---|---|
| 定義 | コンシューマーがメッセージを受信してから、他のコンシューマーに見えなくなる時間 |
| デフォルト値 | 30秒 |
| 設定範囲 | 0秒〜43,200秒(12時間) |
| 二重処理の原因 | 処理時間がVisibility Timeoutを超えるとメッセージが再可視化される |
| 対策 | 処理時間に余裕を持たせた値を設定、または処理中に延長APIを呼ぶ |
| 削除タイミング | 処理完了後に明示的にDeleteMessageを呼ぶ必要がある |
Visibility Timeoutの仕組みを理解する
SQSはメッセージを受信した時点でそのメッセージを一時的に「不可視」にする。これはキューからメッセージを削除するわけではなく、他のコンシューマーから見えなくするだけだ。コンシューマーが処理を完了してDeleteMessageを呼べばメッセージは消える。しかしVisibility Timeoutが切れる前に削除されなければ、メッセージはキューに戻り、再度取得可能になる。
この設計は意図的なものだ。コンシューマーがクラッシュしたり応答しなくなった場合に、メッセージが失われないようにするためのフェイルセーフとして機能している。問題は、正常に動作しているコンシューマーの処理時間がタイムアウトを超えたときに、同じフェイルセーフが誤発動することだ。
(Visibility Timeout開始) Note over SQS: メッセージ不可視 alt タイムアウト内に処理完了 CA->>CA: メッセージ処理 CA->>SQS: DeleteMessage SQS-->>SQS: メッセージ削除 else タイムアウト超過 Note over CA: 処理継続中... Note over SQS: Visibility Timeout切れ SQS-->>SQS: メッセージ再可視化 CB->>SQS: ReceiveMessage SQS-->>CB: 同じメッセージ返却 Note over CA,CB: 二重処理発生! end
- 受信:コンシューマーAが
ReceiveMessageを呼び、メッセージを取得する。SQSはそのメッセージをVisibility Timeout期間だけ不可視にする。 - 処理中:コンシューマーAがメッセージを処理している間、他のコンシューマーはそのメッセージを取得できない。
- タイムアウト超過:処理がVisibility Timeout内に完了しなかった場合、SQSはメッセージを再可視化する。コンシューマーBが同じメッセージを取得してしまう。
- 正常完了:処理がタイムアウト内に完了した場合、
DeleteMessageでメッセージを削除する。これが唯一の正しい終了パスだ。
SQS Visibility Timeoutの設定方法
Visibility Timeoutはキューレベルで設定するか、ReceiveMessage呼び出し時にメッセージ単位で上書きできる。キューレベルの設定はすべてのメッセージに適用されるデフォルト値として機能する。
キューのVisibility Timeoutを確認する
aws sqs get-queue-attributes \
--queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-queue \
--attribute-names VisibilityTimeout
キューのVisibility Timeoutを変更する
aws sqs set-queue-attributes \
--queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-queue \
--attributes VisibilityTimeout=300
受信時にメッセージ単位で上書きする
aws sqs receive-message \
--queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-queue \
--visibility-timeout 120 \
--max-number-of-messages 1
メッセージ単位の上書きは、処理時間がメッセージの内容によって大きく変わる場合に有効だ。ただし、受信時点で処理時間を正確に見積もれないケースも多い。
処理中にVisibility Timeoutを延長する
処理時間が事前に予測できない場合、ChangeMessageVisibility APIを使って処理中にタイムアウトを延長できる。これは「ハートビート」パターンとして実装されることが多い。
Visibility Timeoutの延長は、現在のタイムアウト残り時間に加算されるのではなく、呼び出した時点から新たにカウントが始まる。残り時間が少なくなってから延長しても問題ない。
aws sqs change-message-visibility \
--queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-queue \
--receipt-handle "AQEBwJnKyrHigUMZj6reyasLE56nd8oa..." \
--visibility-timeout 300
--receipt-handleはReceiveMessageのレスポンスに含まれる値だ。メッセージIDではないので注意が必要だ。同じメッセージでも受信のたびに異なるreceipt handleが発行される。
ハートビートパターンの実装例(Python)
🔽 クリックして展開:ハートビートによるVisibility Timeout延長
import boto3
import threading
import time
sqs = boto3.client('sqs', region_name='us-east-1')
QUEUE_URL = 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue'
def extend_visibility(queue_url, receipt_handle, interval, timeout_extension, stop_event):
while not stop_event.is_set():
time.sleep(interval)
if not stop_event.is_set():
sqs.change_message_visibility(
QueueUrl=queue_url,
ReceiptHandle=receipt_handle,
VisibilityTimeout=timeout_extension
)
response = sqs.receive_message(
QueueUrl=QUEUE_URL,
MaxNumberOfMessages=1,
VisibilityTimeout=60
)
if 'Messages' in response:
message = response['Messages'][0]
receipt_handle = message['ReceiptHandle']
stop_event = threading.Event()
heartbeat = threading.Thread(
target=extend_visibility,
args=(QUEUE_URL, receipt_handle, 45, 60, stop_event)
)
heartbeat.start()
try:
# ここで実際の処理を行う
process_message(message)
sqs.delete_message(
QueueUrl=QUEUE_URL,
ReceiptHandle=receipt_handle
)
finally:
stop_event.set()
heartbeat.join()
延長間隔(上記では45秒)はVisibility Timeout値(60秒)より短く設定する。タイムアウトが切れる前に確実に延長できるよう、余裕を持たせることが重要だ。
二重処理が発生するパターンと診断
推奨: VT >= Lambda TL x 6] G -->|No| I[処理時間の変動を確認] I --> J[ハートビートパターンを実装] D --> K[冪等性の実装を確認] H --> K J --> K F --> K K --> L[DLQの設定を確認]
- 処理時間超過:最も一般的なケース。バッチ処理や外部API呼び出しで処理時間が伸びた場合に発生する。
- コンシューマークラッシュ:処理中にプロセスが落ちた場合、タイムアウト後にメッセージが再可視化される。これは意図した動作だ。
- ネットワーク遅延:
DeleteMessageの呼び出しが遅延し、タイムアウトが先に切れるケース。 - Lambda関数のタイムアウト:Lambda関数のタイムアウトがSQSのVisibility Timeoutより長い場合、関数がタイムアウトしてもメッセージは再処理される。
現在のキュー属性を確認して問題を特定する
aws sqs get-queue-attributes \
--queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-queue \
--attribute-names All
出力のApproximateNumberOfMessagesNotVisibleが継続的に増加している場合、処理中のメッセージが積み上がっていることを示す。これは処理速度がメッセージ到着速度に追いついていないか、処理がハングしているサインだ。
実際の障害事例:「処理済みのはずが再処理された」
本番環境でこんな状況に遭遇したことがある。注文処理システムで同じ注文IDに対して決済が二重に実行されていた。ログを見ると、最初のコンシューマーは処理を完了してDeleteMessageも呼んでいた。なぜ二重処理が起きたのか。
調査してみると、Visibility Timeoutは30秒(デフォルト)のままで、外部決済APIの応答に平均25〜35秒かかっていた。つまり、処理は完了していたが、タイムアウトが切れるギリギリのタイミングで別のコンシューマーがメッセージを取得し、最初のコンシューマーのDeleteMessageより先に処理を開始していた。
CloudWatchメトリクスのApproximateAgeOfOldestMessageは正常に見えたが、NumberOfMessagesReceivedがメッセージ数の約1.8倍になっていた。これが二重受信の証拠だった。
修正は単純だった。Visibility Timeoutを300秒に変更し、決済API呼び出しのタイムアウトを60秒に明示的に設定した。それだけで二重処理は完全に止まった。
Visibility Timeoutは「処理にかかる最大時間」ではなく「処理にかかる最大時間+十分な余裕」で設定する。AWSは処理時間の6倍を推奨しているが、実際には最大処理時間の2〜3倍でも十分なケースが多い。
LambdaとSQSを組み合わせる場合の注意点
LambdaをSQSのイベントソースとして使う場合、Visibility TimeoutはLambda関数のタイムアウト値と連動させる必要がある。AWSのドキュメントでは、SQSのVisibility TimeoutをLambda関数タイムアウトの少なくとも6倍に設定することを推奨している。
# Lambda関数のタイムアウト確認
aws lambda get-function-configuration \
--function-name my-sqs-processor \
--query 'Timeout'
# SQSイベントソースマッピングの確認
aws lambda list-event-source-mappings \
--function-name my-sqs-processor
Lambda関数のタイムアウトが30秒なら、SQSのVisibility Timeoutは最低でも180秒に設定する。Lambda関数がタイムアウトした場合、SQSはメッセージを再処理キューに戻す。この動作はDead Letter Queueの設定と組み合わせて、無限ループを防ぐことが重要だ。
Dead Letter Queueの設定確認
aws sqs get-queue-attributes \
--queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-queue \
--attribute-names RedrivePolicy
RedrivePolicyが設定されていない場合、処理に失敗し続けるメッセージがキューに残り続ける。maxReceiveCountを適切に設定してDead Letter Queueに転送する構成を必ず入れておくべきだ。
Visibility Timeout設定のベストプラクティス
P99値を取得] --> B[Visibility Timeout設定
P99 x 余裕係数] B --> C{処理時間が
可変?} C -->|Yes| D[ハートビート実装
ChangeMessageVisibility] C -->|No| E[固定値で設定] D --> F[冪等性を実装] E --> F F --> G[DLQ設定
maxReceiveCount指定] G --> H[CloudWatchアラーム
二重受信を監視]
- 処理時間を計測してから設定する:推測で設定しない。実際の処理時間のP99値を計測し、その値に余裕を加えた値を設定する。
- 処理時間が可変な場合はハートビートを使う:ファイルサイズや外部依存によって処理時間が大きく変わる場合、固定値では対応できない。
- 冪等性を実装する:SQSはAt-Least-Once配信を保証している。Visibility Timeoutを適切に設定しても、ネットワーク障害などで二重配信が発生する可能性はゼロにはならない。処理側で冪等性を担保することが根本的な対策だ。
- Dead Letter Queueを必ず設定する:処理失敗メッセージの無限ループを防ぐ。
- FIFOキューの検討:厳密に一度だけの処理が必要な場合、SQS FIFOキューのExactly-Once Processing機能を検討する。ただしスループット制限があるため、ユースケースを確認すること。
SQS Visibility Timeoutに関するまとめと次のステップ
SQSのVisibility Timeoutは単純な設定値に見えるが、二重処理の根本原因になりやすい。処理時間の計測、適切なタイムアウト値の設定、ハートビートパターンの実装、そして冪等性の担保——この4つを組み合わせることで、SQSを使ったシステムの信頼性は大きく向上する。
次のステップとして以下を確認することを推奨する:
- AWS公式ドキュメント:SQS Visibility Timeout
- Dead Letter Queueの設定ガイド
- CloudWatchで
NumberOfMessagesReceivedとNumberOfMessagesSentの比率を監視するアラームを設定する
用語集
| 用語 | 説明 |
|---|---|
| Visibility Timeout | メッセージ受信後、他のコンシューマーからそのメッセージが見えなくなる期間。デフォルト30秒、最大12時間。 |
| Receipt Handle | メッセージ受信時に発行される一時的な識別子。DeleteMessageやChangeMessageVisibilityの呼び出しに必要。 |
| Dead Letter Queue (DLQ) | 指定回数の処理失敗後にメッセージを転送するキュー。無限再処理ループを防ぐ。 |
| At-Least-Once配信 | SQS標準キューの配信保証。メッセージは少なくとも1回配信されるが、稀に複数回配信される可能性がある。 |
| 冪等性(Idempotency) | 同じ操作を複数回実行しても結果が変わらない性質。SQSの二重配信に対する根本的な対策。 |
コメント
コメントを投稿