何が起きたか?!🐰
Amazon Connect Contact LensのログをOpenSearchに転送するETLパイプラインで、Lambda関数にバグが入った。修正してデプロイしたら、エラー期間中のデータが勝手にOpenSearchに登録されていた。手動復旧は一切していない。
答えはSQS + Lambdaの自動リトライの仕組みにあった。
構成
S3(Contact Lensログ)
↓ イベント通知
SNS → SQS → Lambda(ETL処理) → OpenSearch
なぜ自動で復旧したのか
SQSとLambdaの組み合わせには、こういう挙動がある。
Lambdaが成功したら → SQS(正確にはEventSourceMapping)がメッセージを自動で消す
Lambdaが失敗したら → メッセージは消えずにキューに残る
残ったメッセージは → VisibilityTimeout(後述)が過ぎると再びLambdaに渡される
つまりこういう流れだった。
- Lambda関数にバグがあり、処理が失敗し続ける
- メッセージは消えないので、SQSにどんどん溜まっていく
- バグを修正してデプロイ
- 溜まっていたメッセージが順番にLambdaに渡され、今度は成功する
- 結果、エラー期間中のデータもOpenSearchに登録された
メッセージの保持期間(MessageRetentionPeriod)はデフォルト4日間、最大14日間まで設定できる。この期間内に直せばデータは失われない。
VisibilityTimeoutとは
ここが少しややこしい。SQSのメッセージは「取り出したら無くなる」わけじゃない。Lambdaがメッセージを受け取っても、キューから消えるのはあくまで処理が成功した後。
じゃあ処理中に同じメッセージを別のコンシューマーが拾ったら二重処理になる。それを防ぐのがVisibilityTimeoutで、要は「今ウチが処理中だから他には見せないで」というロック期間。メッセージが消えたんじゃなくて、一時的に触れなくしているだけ。
処理が失敗した場合、このロックが切れるとメッセージが再び見える状態に戻り、Lambdaが再度呼び出される。これがリトライの仕組みの正体。
AWSの推奨は Lambda関数のタイムアウト × 6 に設定すること。スロットリング時のリトライ猶予を確保するためらしい。
ContactLensLogsQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: !Sub ${ProjectName}-${Environment}-contact-lens-logs-queue
VisibilityTimeout: 60 # Lambdaタイムアウト10秒 × 6
MessageRetentionPeriod: 345600 # 4日間
注意点として、Lambda関数のタイムアウトはVisibilityTimeout以下じゃないとEventSourceMappingの作成・更新時にエラーになる。
バッチ処理の落とし穴
BatchSize: 10にしていると、10件まとめて処理される。デフォルトでは1件でも失敗するとバッチ全体がキューに戻る。つまり成功した9件も再処理される。
これが嫌ならReportBatchItemFailuresを設定する。
LambdaFunctionEventSourceMapping:
Type: AWS::Lambda::EventSourceMapping
Properties:
BatchSize: 10
EventSourceArn: !GetAtt ContactLensLogsQueue.Arn
FunctionName: !GetAtt LambdaFunctionForContactLens.Arn
FunctionResponseTypes:
- ReportBatchItemFailures
Lambda側では失敗したメッセージIDだけを返すようにする。
def lambda_handler(event, context):
failures = []
for record in event['Records']:
try:
process_message(record)
except Exception as e:
failures.append({"itemIdentifier": record['messageId']})
return {"batchItemFailures": failures}
DLQ(Dead Letter Queue)も入れておこう
何度リトライしても失敗するメッセージがあると、キューが詰まる。DLQを設定しておけば、指定回数失敗したメッセージを別のキューに逃がせる。
ContactLensLogsQueue:
Type: AWS::SQS::Queue
Properties:
VisibilityTimeout: 60
MessageRetentionPeriod: 345600
RedrivePolicy:
deadLetterTargetArn: !GetAtt ContactLensLogsDLQ.Arn
maxReceiveCount: 5 # 5回失敗でDLQ行き
ContactLensLogsDLQ:
Type: AWS::SQS::Queue
Properties:
QueueName: !Sub ${ProjectName}-${Environment}-contact-lens-logs-dlq
MessageRetentionPeriod: 1209600 # 14日間
AWSの推奨はmaxReceiveCount: 5以上。
リトライされるなら冪等性は必須
同じメッセージが複数回処理される可能性があるので、何回処理しても同じ結果になるように作る必要がある。今回のケースではOpenSearchの_idにユニークなキーを指定して、同じドキュメントが重複登録されないようにしていた。
まとめ🐰🚀
SQS + Lambdaの構成では、Lambdaが失敗してもメッセージはキューに残り続ける。バグを直してデプロイすれば、溜まったメッセージは自動で再処理される。保持期間内に直せば手動復旧は不要。
ただし、この仕組みに頼るなら以下は押さえておきたい。
- VisibilityTimeoutはLambdaタイムアウトの6倍に
-
ReportBatchItemFailuresで無駄な再処理を防ぐ - DLQで永久に失敗するメッセージを逃がす
- 冪等な実装で重複処理に備える
参考ドキュメント