SQS標準キューとLambdaで発生するスロットリングエラーの原因と回避方法

## はじめに
こんにちは
株式会社BTMでエンジニアをしている島谷です。

AWSのSQS（Simple Queue Service）標準キューは、メッセージの順序保証は不要だが高いスループットが求められる場合に非常に便利です。しかし、下図のように、AWS LambdaのイベントソースにSQSを使い、大量のメッセージを処理する際には、スロットリングエラーが発生することがあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3791212/755dd1ec-fd8e-9acc-711e-541931ca4df2.png)

そこで、SQS標準キューとLambdaの組み合わせにおけるスロットリングエラーの原因と、それを回避するための方法について解説します。

## スロットリングエラーとは？
スロットリングエラーとは、AWS Lambdaが設定された同時実行数の上限に達した場合や、Lambdaが受け取るリクエストの量が過剰な場合に、新しいリクエストを処理できずにエラーが発生することです。

## 標準キューにおけるスロットリングエラーの原因
SQSの標準キューとLambdaを組み合わせて使用する際、以下の理由でスロットリングエラーが発生する可能性があります。

### 1. Lambdaの同時実行数制限
Lambdaには同時実行数の上限があります。これは1つのLambda関数が同時に処理できる最大のインスタンス数です。例えば、同時実行数が5に設定されている場合、Lambda関数は一度に5つのメッセージしか並行して処理できません。
SQS標準キューはメッセージの順序を保証しないため、同時に複数のメッセージがLambdaに配信されます。しかし、Lambdaの同時実行数制限を超えると、メッセージがキューに残り続けてしまい、新しいメッセージが処理されずにスロットリングが発生します。

実際に以下のケースで試しに行ってみます。
- キューにメッセージをまとめて11件送信する
- Lambdaの同時実行数を1とする
- バッチサイズを1とする
- 1メッセージあたりのLambdaが処理する時間を1秒とする
- 可視性タイムアウトを30秒とする
- メッセージ受信待機時間を10秒とする
- デッドレターキュー(DLQ)の設定は無しとする

結果は以下の通りで、左下のメトリクスからスロットリングエラーが14件発生していることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3791212/227571ab-6839-7f31-1a38-42c09f4c49fd.png)

メッセージの件数より多く発生していることから、スロットリングエラーが発生した後、複数回リトライされていることがわかります。
スロットリングエラー時のリトライは「バックオフ」戦略で管理されている様です。

>失敗した呼び出しに対するバックオフ戦略
>呼び出しが失敗すると、Lambda はバックオフ戦略を実装しながら呼び出しを再試行します。バックオフ戦略は、Lambda が失敗した原因が関数コードのエラーによるものか、スロットリングによるものかによって若干異なります。
・関数コードがエラーの原因である 場合、Lambda は処理を停止し、呼び出しを再試行します。その間、Lambda は徐々にバックオフし、Amazon SQS イベントソースマッピングに割り当てられた同時実行の量を減らします。キューの可視性タイムアウトがなくなると、メッセージはキューに再び表示されます。
・スロットリングが原因で呼び出しが失敗した場合、Lambda は Amazon SQS イベントソースマッピングに割り当てられた同時実行の量を減らして、再試行を徐々に減らします。Lambda は、メッセージのタイムスタンプがキューの可視性タイムアウトを超えるまでメッセージを再試行し続け、その時点で Lambda はメッセージをドロップします。

https://docs.aws.amazon.com/lambda/latest/dg/services-sqs-errorhandling.html

### 2. バッチサイズと処理時間の不一致
バッチサイズとは、1回のLambda呼び出しで受け取ることのできる最大のレコード数です。
例えば、バッチサイズを10に設定していると、Lambdaは一度に最大10件のメッセージを取得して処理します。
メッセージの処理時間が長い場合、Lambdaが次のメッセージを受け取る前に、処理中のメッセージで同時実行数がいっぱいになり、スロットリングが発生します。

## スロットリングエラーの回避方法
### 1. Lambdaの同時実行数の増加
最も簡単な回避方法は、Lambdaの同時実行数を増やすことです。同時実行数を増やすことで、複数のインスタンスが並列でメッセージを処理できるようになり、スロットリングの発生を防ぐことができます。
一方、同時実行数が増えると、Lambdaのインスタンス起動数が増加し、その分の実行時間に対する課金が増えます。そのため、必要以上に同時実行数を増やさず、スロットリングが発生しない最低限の数に調整する必要があります。

### 2. バッチサイズの調整
バッチサイズを小さくすることで、1回のLambda実行ごとの処理負荷を軽減し、Lambdaの処理が詰まることを防げます。これにより、Lambdaが頻繁にメッセージを取得できるようになり、スロットリングが緩和されます。
例えば、バッチサイズを10から5に変更することで、1度に処理するメッセージ数が減り、メッセージ処理の時間が短縮されます。
``` hcl
resource "aws_lambda_event_source_mapping" "sqs_trigger" {
  event_source_arn = aws_sqs_queue.standard_queue.arn
  function_name    = aws_lambda_function.standard_lambda.arn
  batch_size       = 5  # バッチサイズを小さくして負荷を軽減
  enabled          = true
}
```
ただ、処理時間が特に短いメッセージの場合、バッチサイズを小さくすると今度はLambda起動によるオーバーヘッドが目立つようになります。
そのため、適切なバッチサイズを設定できるように、「モニタリング」→「バッチサイズの調整」のサイクルを回して、最適なバッチサイズを見つける必要があります。

### 3. メッセージ処理のリトライとデッドレターキュー(DLQ)の設定
スロットリングが発生した際に、メッセージを適切にリトライできるようにすることも重要です。SQSでは、Lambdaがメッセージの処理に失敗した場合、一定回数リトライを行い、それでも処理に失敗した場合にはデッドレターキュー(DLQ)にメッセージを送信することができます。
DLQを設定することで、処理に失敗したメッセージがSQS内で無限にリトライされることを防ぎ、キューの溢れを回避します。
``` hcl
resource "aws_sqs_queue" "standard_queue_dlq" {
  name                      = "standard-queue-dlq"
  visibility_timeout_seconds = 30
}

resource "aws_sqs_queue" "standard_queue" {
  name                      = "standard-queue"
  visibility_timeout_seconds = 30

  redrive_policy = jsonencode({
    deadLetterTargetArn = aws_sqs_queue.standard_queue_dlq.arn
    maxReceiveCount     = 5  # 5回失敗したメッセージをDLQに送信
  })
}
```
DLQを設定する場合は、再処理に対応した設計を考慮する必要があります。ただし、再処理やエラー対応が不要で、システム全体への影響が小さいメッセージの場合は、DLQを設定しなくても問題ありません。メッセージの重要性を踏まえて、DLQを設定すべきかどうかを検討する必要があります。


### 4. Lambda関数のコード最適化
メッセージの処理に時間がかかりすぎると、その間に新しいメッセージがキューに溜まり、結果としてスロットリングが発生します。Lambda関数の処理ロジックを最適化し、処理時間を短縮することでスロットリングのリスクを減らせます。

### 5. FIFOキューの検討
同時実行数1およびバッチサイズ1のような、直列処理を期待している場合にはFIFOキューを検討する余地があります。
FIFOキューは同じメッセージグループIDを持つメッセージであれば、1件ずつLambdaに配信されます。そのためスロットリングエラーを回避することができます。

``` hcl
resource "aws_sqs_queue" "fifo_queue_dlq" {
  name                      = "fifo-queue-dlq"
  visibility_timeout_seconds = 30
}

resource "aws_sqs_queue" "fifo_queue" {
  name                      = "fifo-queue"
  visibility_timeout_seconds = 30

  redrive_policy = jsonencode({
    deadLetterTargetArn = aws_sqs_queue.standard_queue_dlq.arn
    maxReceiveCount     = 5  # 5回失敗したメッセージをDLQに送信
  })
  fifo_queue = true
  content_based_deduplication = true
  deduplication_scope   = "messageGroup"  # 高スループットのFIFOキュー
  fifo_throughput_limit = "perMessageGroupId"
}
```
FIFOキューであっても、別々のメッセージグループIDを持つ場合、標準キューの時と同じように、並列でLambdaに配信されてしまいます。もしスロットリングエラーが発生する場合は、同時実行数を調整する必要があります。

## まとめ
SQS標準キューとLambdaの連携では、急激なメッセージの増加やLambdaの同時実行数制限により、スロットリングエラーが発生する場合があります。これを回避するためには、Lambdaの同時実行数の増加、バッチサイズの適切な調整、メッセージのリトライ設定、Lambdaの処理効率の改善、そしてFIFOキューの検討が鍵となります。

以上です、最後までご覧いただきありがとうございました。

::: note
株式会社BTMではエンジニアの採用をしております。
ご興味がある方はぜひ[コチラ](https://www.b-tm.co.jp/recruit/mid-career.html)をご覧ください。
:::