前回は SQS × Lambda の基本的な連携構成と、イベントソースマッピング(ESM)を使ったメッセージ処理の実装を紹介しました。「動くものが作れた」状態から、今回はいよいよ「ちゃんと本番で動かす」ための話です。
正直、自分もしばらく「動いてるからまあいいか」でそのまま運用していたんですが、ある日突然 DLQ にメッセージが溜まり始めて冷や汗をかいた経験があります。そこから設定を見直して気づいた落とし穴や、今の自分なりのベストプラクティスをまとめています。
この記事でわかること
- Visibility Timeout の正しい設定と計算方法
- DLQ と maxReceiveCount の設計のコツ
- 部分バッチレスポンス(ReportBatchItemFailures)の実装方法
- SQS ESM のスケーリング動作と Provisioned Mode
- 本番運用に必須の CloudWatch メトリクス監視
落とし穴①:Visibility Timeout の設定ミス
SQS × Lambda 構成でまず引っかかりやすいのが Visibility Timeout の設定です。
Visibility Timeout は「あるコンシューマーがメッセージを受け取ってから、他のコンシューマーにそのメッセージが見えなくなる時間」のことです。Lambda の処理がこのタイムアウト時間を超えると、SQS はメッセージが処理されなかった可能性があるため、再びキューに戻します。その結果、同じメッセージが二重処理される可能性があります。
AWS の推奨は (Lambda のタイムアウト × 6)+ MaximumBatchingWindowInSeconds 以上 を Visibility Timeout に設定することです。たとえば Lambda のタイムアウトが 30 秒で、MaximumBatchingWindowInSeconds が 0 なら、Visibility Timeout は 180 秒以上にする。
# AWS CLI での設定例
aws sqs set-queue-attributes \
--queue-url https://sqs.ap-northeast-1.amazonaws.com/123456789/my-queue \
--attributes VisibilityTimeout=180
処理時間が毎回バラバラなケースでは、Lambda 内で change_message_visibility を呼んで動的に延長するアプローチもあります。ただし Visibility Timeout の上限は 12 時間なので、本当に長い処理が必要な場合は SQS + Lambda だけで完結させようとするのはそもそも設計を見直した方がいいかもしれません。
落とし穴②:DLQ と maxReceiveCount の設計
Dead Letter Queue(DLQ)は設定してあれば安心、というものでもなくて、maxReceiveCount の値をちゃんと考えないと痛い目を見ます。
これはメッセージが DLQ に移される前にメインキューに再配信される回数の上限です。一般的には、短期的なスロットリングや一時的なエラーが起きても即 DLQ 行きにならないように、ある程度の回数(たとえば 5 回以上など)を確保する設計が多い印象です。
低すぎる(例:2 や 3)と、一時的なエラーでも即 DLQ 行きになってしまいます。逆に高すぎると、本当に壊れたメッセージが延々と処理を試み続けてコストがかさむことも。障害の原因と推定リカバリ時間をもとに適切な値を決めるのがよさそうです。
# SQS キュー作成時に DLQ を関連付ける例(Python + boto3)
import json
import boto3
sqs = boto3.client('sqs', region_name='ap-northeast-1')
# 先に DLQ を作成
dlq = sqs.create_queue(QueueName='my-queue-dlq')
dlq_arn = sqs.get_queue_attributes(
QueueUrl=dlq['QueueUrl'],
AttributeNames=['QueueArn']
)['Attributes']['QueueArn']
# メインキューに RedrivePolicy を設定
sqs.create_queue(
QueueName='my-queue',
Attributes={
'RedrivePolicy': json.dumps({
'deadLetterTargetArn': dlq_arn,
'maxReceiveCount': '5' # ここ注意:文字列で渡す
})
}
)
DLQ に溜まったメッセージを本番キューにリドライブできる「DLQ Redrive」機能があって、コンソールからワンクリックで使えるので便利です。ただしリドライブしたメッセージが再びエラーになって無限ループしないように、事前に Lambda 側の原因を直してから実行するのが前提です。
部分バッチレスポンス(ReportBatchItemFailures)を使う
バッチ処理でよくある失敗パターンが「10 件のうち 1 件だけ失敗したのに、バッチ全体が再処理される」というものです。これを解決するのが 部分バッチレスポンス(Partial Batch Response) です。
イベントソースマッピングの設定時に FunctionResponseTypes リストに ReportBatchItemFailures を含めることで、失敗したメッセージのみを再試行するよう Lambda に伝えることができます。
Lambda 関数側の実装はこんな感じです。
import json
def lambda_handler(event, context):
batch_item_failures = []
sqs_batch_response = {}
for record in event['Records']:
try:
body = json.loads(record['body'])
process_message(body)
except Exception as e:
print(f"Failed: {record['messageId']} / {e}")
batch_item_failures.append({'itemIdentifier': record['messageId']})
sqs_batch_response['batchItemFailures'] = batch_item_failures
return sqs_batch_response
def process_message(body):
# 実際のビジネスロジック
print(f"Processing: {body}")
失敗したメッセージを識別する時は messageId ではなく eventID を使う場合もあるので、実装時は AWS ドキュメントで確認するのをおすすめします。
ESM 側にも設定が必要です。
# ESM に ReportBatchItemFailures を有効化(AWS CLI)
aws lambda update-event-source-mapping \
--uuid <event-source-mapping-uuid> \
--function-response-types ReportBatchItemFailures
Lambda 関数だけ直して ESM の設定を変え忘れるミスをやりがちなので注意です(自分がそれをやった)。
スケーリング設計:デフォルト挙動と Provisioned Mode
SQS + Lambda のスケーリング挙動は、意外と知らずに使っているケースが多い気がします。
デフォルトの挙動では、Lambda はメッセージが利用可能な場合に最初に最大 5 つのバッチを並列で処理し始め、その後は毎分最大 300 の同時実行数を追加していきます。上限は 1,250 同時実行です。
通常の用途ではこれで十分ですが、突発的な大量トラフィックへの対応が遅れることがあります。そこで登場したのが SQS ESM の Provisioned Mode です。
Provisioned Mode を使うと、デフォルトの ESM と比べてスケーリングが 3 倍速くなり(毎分 1,000 同時実行まで)、最大同時実行数も 16 倍の 20,000 に拡大されます。
Provisioned Mode では専用のポーラーを用意することで、最小・最大のスケールを設定できるらしいです。高トラフィックなキュー処理の場合、ここをちゃんと設定するかどうかでかなり違いが出そうです。
ただし Provisioned Mode はその分コストもかかるので、トラフィックが安定していてスケーリング速度が問題になっていないケースでは、正直デフォルトのままで十分だと思います。まずはデフォルトで運用してみて、ボトルネックを感じたら検討するくらいの感覚でいいんじゃないかなと。
最大同時実行数の制限も忘れずに
SQS トリガーの設定に最大同時実行数(MaximumConcurrency)を設定することで、Lambda 関数の実行数を一定に保ちつつスロットリングを抑制できます。ただし、スロットリングやエラーが続く状況では再試行が増えやすく、結果として受信回数が積み上がって DLQ 側に移動しやすくなるケースもあるので、DLQ 設計とセットで考えるのが無難です。
ダウンストリームの DB や API を守るためにあえて同時実行数を絞るのはよくある設計ですが、その場合は maxReceiveCount を多めにして DLQ への意図しない移動を防ぐセットで考える必要があります。
監視:最低限おさえておきたい CloudWatch メトリクス
SQS × Lambda 構成で見ておくべきメトリクスをざっくり整理します。
SQS 側のメトリクス
- ApproximateNumberOfMessagesVisible:キューに溜まっているメッセージ数。急増したら処理が追いついていないサイン
- ApproximateAgeOfOldestMessage:最も古いメッセージの滞留時間。処理遅延の深刻度を示す
- NumberOfMessagesSent(DLQ):DLQ 側の送信数。ただし注意点がある(後述)
Lambda 側のメトリクス
- Errors:エラー数。当然ながら監視必須
- Throttles:スロットリング発生数。同時実行数の設計を見直すサイン
- ConcurrentExecutions:実際の同時実行数の推移を確認するのに使う
DLQ のメトリクスには注意が必要で、NumberOfMessagesSent は状況によっては期待した増え方をしないことがあります。DLQ の状態を把握する目的では、ApproximateNumberOfMessagesVisible のような「今 DLQ に何件あるか」を見るメトリクスも合わせて使うのが無難です。
DLQ のアラーム設定例はこんな感じです。
import boto3
cloudwatch = boto3.client('cloudwatch', region_name='ap-northeast-1')
cloudwatch.put_metric_alarm(
AlarmName='my-queue-dlq-messages',
MetricName='ApproximateNumberOfMessagesVisible',
Namespace='AWS/SQS',
Dimensions=[{'Name': 'QueueName', 'Value': 'my-queue-dlq'}],
Statistic='Sum',
Period=300,
Threshold=1,
ComparisonOperator='GreaterThanOrEqualToThreshold',
EvaluationPeriods=1,
AlarmActions=['arn:aws:sns:ap-northeast-1:123456789:my-alert-topic']
)
DLQ に 1 件でも入ったらアラートを飛ばす、というのが基本中の基本です。しばらく放置して大量に溜まってから気づく、というのはなるべく避けたい。
※この記事にはプロモーションが含まれます
ちなみに、お名前.com レンタルサーバー(WordPressに特化した高速レンタルサーバー。月額990円〜、独自ドメイン実質0円)も気になっています。お名前.com レンタルサーバー![]()
まとめ
全 4 回にわたって SQS × Lambda の構成を一通り見てきました。改めて振り返ると「動く」状態と「本番で安定して動く」状態の間には、意外とたくさんの設定の隙間があるなと感じます。Visibility Timeout と Lambda タイムアウトのバランス、部分バッチレスポンスの有効化、DLQ のメトリクス監視あたりは最低限おさえておけば、かなり安心感が違います。
Provisioned Mode は個人プロジェクトにはオーバースペック感がありますが、仕事で高トラフィックな処理を任された場合は選択肢として頭に入れておくといいかもしれません。自分はまだ使ったことがないので、機会があれば試してみたいなとは思っています。

