八 月
8
土曜日
2020

AWS SQS + Lambdaの同時実行数の挙動について

SQSをイベントソースとするLambdaに同時実行数の制限をつけた場合どのような動きになるのか確認した。


Lambdaの同時実行数について

AWS Lambda開発者ガイド 『Lambda 関数の同時実行数の管理』 によると

同時実行数とは、ある時点で関数が処理しているリクエストの数を指します。

とある。

この同時実行数には、

  • AWSから課されるアカウント(かつリージョン)単位の同時実行数の制限
  • ユーザが任意で関数単位に課す同時実行数の制限

の二つの制限がある。

後者は予約された同時実行数と呼ばれ、アカウント単位の同時実行数を関数毎にどう分け合うかというリソース配分としての意味の他、Lambdaの下流にいるリソースへのアクセス数を制限し過負荷から保護するため用途でも使用される。

同時実行数を超えた場合、関数の呼び出しは行われずスロットリングによるエラーが返される。

SQS + Lambdaで同時実行数を超えた場合の挙動

SQSをイベントソースとしたLambdaにスロットリングが発生した場合どうなるのか確認する。

AWS Lambda開発者ガイド 『AWS Lambda を Amazon SQS に使用する』によると、

関数がスロットリングされた、エラーを返した、または応答しなかった場合は、メッセージが再び表示されるようになります。失敗したバッチのすべてのメッセージはキューに戻るため、関数コードが副作用なしで同じメッセージを複数回処理できることが必要となります。

とありLambdaの関数がエラーが返した際と同じように振舞うとのこと。

例えばRedrive ポリシー有りのキューを同時実行数が指定されたLambdaで処理した場合、以下のような挙動になるはずである。

  • SQS側のイベント発生がLambdaの同時実行数を上回った場合、関数のスロットリングが発生する
  • スロットリングが発生したメッセージの受信回数(ReceiveCount) はカウントアップされる
  • メッセージの受信回数が最大受信回数(maxReceiveCount)を超えた場合、そのメッセージはDead Letter Queueへ移動される。
  • 受信回数が最大受信回数以内である場合は可視性タイムアウトが終了しメッセージはLambdaから再度から処理可能状態に戻る

実際に動かして挙動を検証してみる。

検証環境準備

検証のため

  • キュー
  • デッドレターキュー
  • 同時実行数を指定したLambda

を用意する。

Terraform

resource "aws_sqs_queue" "test_dlq" {
  name = "test-dlq"
}

resource "aws_sqs_queue" "test_queue" {
  name                      = "test-queue"
  delay_seconds             = 90
  receive_wait_time_seconds = 10
  redrive_policy = jsonencode({
    deadLetterTargetArn = aws_sqs_queue.test_dlq.arn
    maxReceiveCount     = 1
  })
}

data "archive_file" "test_lambda" {
  type        = "zip"
  source_dir  = "lambda"
  output_path = "lambda.zip"
}

resource "aws_lambda_function" "test_lambda" {
  function_name                  = "test-lambda"
  role                           = aws_iam_role.test_lambda.arn
  handler                        = "index.handler"
  runtime                        = "nodejs12.x"
  filename                       = data.archive_file.test_lambda.output_path
  reserved_concurrent_executions = 1
  timeout                        = 30
  source_code_hash               = filebase64sha256(data.archive_file.test_lambda.output_path)
}

data "aws_iam_policy_document" "test_lambda" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = [
        "lambda.amazonaws.com"
      ]
    }
  }
}

resource "aws_iam_role" "test_lambda" {
  name               = "test-lambda-role"
  assume_role_policy = data.aws_iam_policy_document.test_lambda.json
}

resource "aws_iam_role_policy_attachment" "test_lambda" {
  role       = aws_iam_role.test_lambda.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole"
}

resource "aws_lambda_event_source_mapping" "test_lambda" {
  event_source_arn = aws_sqs_queue.test_queue.arn
  function_name    = aws_lambda_function.test_lambda.arn
  batch_size       = 1
  enabled          = false
}

Lambdaソースコード

nodejsでサンプル lambda/index.js を用意。
Atomics.wait でsleepを入れて関数のスループットを調整する。

exports.handler = async function(event, context) {
  console.log(`Num of msgs = ${event.Records.length}`)
  event.Records.forEach(record => {
    const { body } = record;
    console.log(body);
  });
  console.log("zzz...");
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1000);
  return {};
}

検証

Lambdaは同時実行数1、バッチサイズ1で固定。
検証は キューの最大受信回数 ✖︎ 関数のスループット(スリープ時間) のパターンで実施し、キューにメッセージが100件溜まっている状態でLambdaのイベントソースマッピングを活性化させ、

  • 処理成功メッセージ件数
  • DLQに落ちたメッセージ件数
  • Invocationsの合計(CloudWatchメトリクス)
  • Throttlesの合計(CloudWatchメトリクス)

を確認する。

リトライなし(最大受信回数1)

関数スリープ(ms) 成功件数 DLQ落ち件数 Invocations Throttles
なし 94 6 94 6
1000 38 62 38 62
3000 20 80 20 80
5000 13 87 13 87

リトライが無いのでスロットリングが発生した分がそのままDLQに落ちている。
同時実行数が1だと関数側のスループットに問題が無くとも容易くスロットリングが発生する事が分かる。

リトライ1 (最大受信回数2)

関数スリープ(ms) 成功件数 DLQ落ち件数 Invocations Throttles
なし 100 0 100 10
1000 60 40 60 106
3000 47 63 47 143
5000 27 73 27 160

DLQ落ちの件数が減ったがその分スロットリングの発生が増えている。
これはリトライにより、
スロットリング → リトライ → 成功 のケースの分、DLQ落ち件数が減り、
スロットリング → リトライ → 再びスロットリング のケースの分、スロットリング件数が増えている。

リトライ2 (最大受信回数3)

関数スリープ(ms) 成功件数 DLQ落ち件数 Invocations Throttles
なし 100 0 100 7
1000 83 17 83 119
3000 53 47 53 190
5000 48 62 48 218

さらにリトライが追加された分DLQ落ちの件数が減っているが同様にスロットリングの発生件数も増え効率が悪くなっている。

まとめ

メッセージが100件程度滞留したただけでスロットリングは容易く発生する。
平時、キューの滞留が無いとしても下流のスローダウンや障害、メンテナンス時のコンシューマー停止による滞留は十分ありうるため、 同時実行数を指定する際は常にスロットリング発生のトレードオフを勘定にいれておく必要がある。
また規模の如何にかかわらずコンシューマーの処理を直列化したいがために同時実行数を1にするのは辞めておいた方が良さそうだ。