サーバーサイドエンジニアの本間です。
弊社では、Amazon SQSを使ったシステムがたくさん存在しています。 その中には、標準キューではなくFIFOキューを使っているシステムもあります。
今回、そのFIFOキューを使ったシステムにおいて、当初想定していなかった問題が発生しました。 この問題の調査からFIFOキューを使う時の気づきにくい仕様による注意点が見つかったので、共有しようと思います。
発生した問題
弊社には、下図のようにAPI Gatewayで受け付けたリクエストを一旦SQSに格納し、バックグラウンドでLambdaがメッセージを取り出し、結果を別のSQSに格納するという構成で動作している機能があります。
このシステムでは、リクエストを格納するSQSを標準キューではなくFIFOキューを使っていました。
このシステムにおいて、処理のピークでたくさんのリクエストがきてSQSに何十万というたくさんメッセージが積まれているのですが、LambdaのConcurrent executionsが10程度までしか上がらず、全然メッセージの処理が進まないという事象が発生しました。
FIFOキューとLambdaを組み合わせる形式は 公式ドキュメント にも記載があります。
異なるグループの同時処理: Lambda は、複数のインスタンスを使用して異なるメッセージグループ IDsからのメッセージを同時に処理できます。 つまり、Lambda 関数の 1 つのインスタンスが 1 つのメッセージグループ ID からのメッセージを処理している間、他のインスタンスは他のメッセージグループ IDs からのメッセージを同時に処理でき、Lambda の同時実行機能を活用して複数のグループを並行して処理できます。
このドキュメントに従うと、メッセージグループが分かれていればそのグループ数分は同時処理されるはずです。 メッセージグループは数百以上に分かれているはずで、同時実行数も100以上に高まる想定でした。
なぜ同時実行が進まなかったのか、原因を調査しました。
原因
この原因ですが、FIFOキュー独自の仕様が影響していました。AWSの 公式ドキュメント に以下の説明があります。
FIFOキューは、最初の 20k メッセージを検索して、使用可能なメッセージグループを判別します。 つまり、1 つのメッセージグループにメッセージのバックログがある場合、バックログからメッセージを正常に消費するまで、後からキューに送信された他のメッセージグループからのメッセージを使用することはできません。
実は、今回ある1つのメッセージグループに対して大量にメッセージを送られていたことがわかりました。 1つのメッセージグループ大量のメッセージが送信され、そのメッセージが先頭の20,000メッセージの大半を占めてしまうと、その後送信された他のメッセージグループIDのメッセージが存在しないように見えてしまう -> Lambdaから見ると他に処理するメッセージがなく、処理が間に合っているように見えるのでConcurrent executionsが増えない、という状況に陥っていました。
以下にイメージ図を記載しておきます。
他のグループのメッセージが見えるためには、メッセージの処理が進んで、他のメッセージグループのメッセージが先頭の20,000メッセージに入ってこないといけません。 しかし、FIFOキューは同じメッセージグループのメッセージは1つずつ順番にしか処理できないため、全てのメッセージが処理されるまでかなりの時間を要してしまいました。
確認してみる
ここで、今回問題となったFIFOキューの動作を確認してみようと思います。
まずメッセージ数が少ない場合を確認します。
$ export your_aws_account_id=xxxx # 3つのメッセージグループに1メッセージずつ送信 $ aws sqs send-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo --message-body 'test-message-1-1' --message-deduplication-id '1-1' --message-group-id 1 $ aws sqs send-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo --message-body 'test-message-2-1' --message-deduplication-id '2-1' --message-group-id 2 $ aws sqs send-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo --message-body 'test-message-3-1' --message-deduplication-id '3-1' --message-group-id 3 # メッセージは3通ある $ aws sqs get-queue-attributes --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo --attribute-names ApproximateNumberOfMessages { "Attributes": { "ApproximateNumberOfMessages": "3" } } # メッセージを3通連続で取得してみる $ aws sqs receive-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo { "Messages": [ { "Body": "test-message-1-1" } ] } $ aws sqs receive-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo { "Messages": [ { "Body": "test-message-2-1" } ] } $ aws sqs receive-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo { "Messages": [ { "Body": "test-message-3-1" } ] } # 3つのメッセージグループ、それぞれのメッセージが取得できた。
メッセージが少ない場合、それぞれのメッセージグループのメッセージを取得することができました。
次に1つのメッセージグループで20,000メッセージを送信し、その後他のメッセージグループにメッセージが送信されたパターンを試します。
# テスト用に1回メッセージをクリア $ aws sqs purge-queue --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo # メッセージグループ1に20,000通のメッセージを送信 $ for i in $( seq 1 20000 ) do aws sqs send-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo --message-body "test-message-1-$i" --message-deduplication-id "1-$i" --message-group-id 1 done $ aws sqs get-queue-attributes --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo --attribute-names ApproximateNumberOfMessages { "Attributes": { "ApproximateNumberOfMessages": "20000" } } # その後、メッセージグループ2と3に1通ずつ送信 $ aws sqs send-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo --message-body 'test-message-2-1' --message-deduplication-id '2-1' --message-group-id 2 $ aws sqs send-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo --message-body 'test-message-3-1' --message-deduplication-id '3-1' --message-group-id 3 $ aws sqs get-queue-attributes --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo --attribute-names ApproximateNumberOfMessages { "Attributes": { "ApproximateNumberOfMessages": "20002" } } # この状態でメッセージを3通連続で取得してみる $ aws sqs receive-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo { "Messages": [ { "Body": "test-message-1-1" } ] } $ aws sqs receive-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo $ aws sqs receive-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo # 2通目、3通目のメッセージが返却されない!
この状況でメッセージを2個消費して、再度確認します。
# メッセージを2通削除 $ ReceiptHandle=$(aws sqs receive-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo --message-attribute-names --output text --query Messages[0].ReceiptHandle) $ aws sqs delete-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo --receipt-handle $ReceiptHandle $ ReceiptHandle=$(aws sqs receive-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo --message-attribute-names --output text --query Messages[0].ReceiptHandle) $ aws sqs delete-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo --receipt-handle $ReceiptHandle # メッセージはちょうど20,000通 $ aws sqs get-queue-attributes --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo --attribute-names ApproximateNumberOfMessages { "Attributes": { "ApproximateNumberOfMessages": "20000" } } # ここで3連続でメッセージを取得してみる $ aws sqs receive-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo { "Messages": [ { "Body": "test-message-1-3" } ] } $ aws sqs receive-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo { "Messages": [ { "Body": "test-message-2-1" } ] } $ aws sqs receive-message --queue-url https://sqs.ap-northeast-1.amazonaws.com/$your_aws_ccount_id/homma-test-queue.fifo { "Messages": [ { "Body": "test-message-3-1" } ] } # 無事、メッセージが3通取得できた。
この場合、20,000メッセージ未満になったため他のメッセージグループのメッセージも見えるようになりました。 このことから、ドキュメントにもある通り先頭の20,000メッセージから処理できるメッセージグループがあるか判別しているようです。
今回の問題への対応
FIFOキューの動作が確認できたところで、初めに紹介した問題に対して弊社がどのように対応したかを紹介します。
はじめに同じメッセージグループで大量のメッセージが送信されないように、メッセージグループを細分化することができないか検討しました。 検討の結果、メッセージグループの細分化は難しいことがわかりました。
そこで、次になぜFIFOキューを使っているか調べたところ、Lambda内で一部排他制御が必要な箇所があり、その排他制御のために使っていることがわかりました。 FIFOキューには排他制御のほかに、「メッセージが追加された順序で実行される」、「重複配信されない」という特徴がありますが、それらの特徴は今回はなくても問題ないことが確認できました。 それであれば、Lambda内で何らかの形で排他制御ができれば、FIFOキューを使わずに標準キューに変更することで対応できそうです。
最終的には、「Dynamo DBの条件付き書き込みを使って、Lambda内で排他制御する」、「SQSはFIFOキューから標準キューに変更する」という修正を実施しました。 修正後のシステムのイメージ図を以下に記載します。
修正後、Lambdaの同時実行数がピーク時には数百まで高まるようになりました。 結果として、当初の問題を解決できた他、排他が必要な時間を最小限にできたためパフォーマンスを大きく向上させることができました。
まとめ
今回、FIFOキューとLambdaの組み合わせで発生した問題を起点に、FIFOキューの気づきにくい制約に関して知ることができました。 FIFOキューを使う場合、以下の点を確認すると良いと思います。
- FIFOキューを使う場合、同一のメッセージグループで大量のメッセージを送信することがないか確認する。もしある場合、今回の問題が起きる可能性があるため、メッセージグループを細かく分けることができないか検討する。
- 排他制御のためだけにFIFOキューを使うのは、最適な手段になっていない可能性がある。本当にFIFOキューが必要か、他の手段で代替できないか一度検討した方がよい。
- 上記以外にも、FIFOキューは標準キューに比べてスループットが低い(1秒あたり300件のトランザクション)ので問題ないか、高スループットモードを使って解決できるのか、確認する必要がある。
弊社では、今回のようにAWSのサービスをフル活用したサービスの開発も行っています。 興味がありましたら、下記採用ページからカジュアル面談のお申し込みをお待ちしています。
最後までお読みいただきありがとうございました。