AWS SQSの具体的・実用的な使用についての考察

## はじめに
SQSの概要や設定項目の説明記事はたくさんありますが、実用面に着目している解説記事が少なかったので、今の自分の理解の記録を残す意味でも記事にします。ここで、あえて「SQSとは…」や、「設定項目の説明」などはせずにスキップします。

## SQSのメリット
なぜSQSを使うか。SQSメッセージの生成(プロデューサ)はLambda, API Gateway, SNS, S3イベントあたりが主なところで、メッセージの使用先(コンシューマ)はLambdaであるケースが多いと思います。しかし、ここで挙げたプロデューサからLambdaを実行することも可能です。つまりSQSをスキップできるアーキテクチャとなっているケースも多いです。ではなぜ、SQSを使うのでしょうか。
SQSを使うメリットとして一般的に、柔軟性、冗長性を持たせて処理を疎結合化すると言われます。そう言われても少し抽象的なので、その柔軟性、冗長性とは何なのかを具体的に挙げていきます。また、SNSとSQSは用途が被ることが多いので、SQSのメリットをSNSと比較しながら見てみましょう。

- **SQSの強み、メリット**
1. 遅延性 (エラーのあった処理を数時間後に再処理、好きなタイミングでポーリング、等)
2. 優先度有りの処理 (有料ユーザの処理を優先実行、等)
3. 実行保証性 (メッセージは必ず配信されるという保証付き)
4. 保存性 (指定回数コンシューマ側でエラーになったメッセージをデッドレターとして保存)
5. 並列処理 (バッチ処理等、同じlambdaを同時実行することが可能)
6. 順序性 (FIFOキュー, 実行順序が大事なアプリの場合重要)

#### SNSとの比較
1. 遅延性はSQSでは遅延キュー、メッセージタイマー、可視性タイムアウトなどを設定することで、メッセージが利用可能になるまでの時間や、メッセージがエラーで戻ってきた際に次にいつ再実行するのか等を指定できます。SNSでも再配信ポリシーによってエラーで戻ってきた際の再実行タイミングはある程度カスタマイズできますが、何度も再実行する前提の場合は、キュー内のメッセージ個数を視覚的に確認できたり、自発的にポーリングすることもできるSQSの方がいいでしょう。また、SQSではメッセージをLambdaトリガーとせずに、コンシューマ側で好きなタイミングでポーリングしてメッセージを受信することもできますが、SNSでは基本即実行なので、タイミングは選べませんね。ここらへんを上手く使う必要があればSQSは必須と言えます。

2. 優先度付き処理はSNSにはなく簡単に実装するのであればSQS固有だと思いますが、あまり使うケースは多くないかと思います。有料ユーザの処理を優先実行したい場合などに有効です。

3. 実行保証性については、SNSでも相当なことが起こらない限りは問題無く全て実行してくれるでしょう。ただ、SNSをLambdaトリガーにしていると、同時実行回数などの細かいトリガー設定ができません。[^1]Lambdaのオートスケール説明によるとデフォルトの同時実行制限回数が1000となっているようです。試していないので分かりませんが、SNS->Lambdaとなっている状態で、SNSトピックに数千～数万の通知が発生するとLambdaの同時実行回数の上限に達して429エラーが発生する可能性があります。
対して、SQSの場合はLambdaの実行回数制限以上のメッセージは待機となり、既に実行中の関数が終わり次第に次が入る・・・と上手いことやってくれるので、大量リクエストがLambdaトリガーとなる見込まれる可能性がある場合はSNSよりSQSが優れていると言えます。スケーラビリティ的な観点で優れているのはSNSよりSQSと言えるでしょう。

4. 保存性についてはSNSでもサブスクリプションごとにDLキューを付けることが可能なので、各DLキューに処理できなかった内容を保存することは可能ではありますが、SNSよりもSQSの方が優れているケースもあります。それはコンシューマが存在しない場合(一時的にLambdaをトリガーから外す場合など)で、SNSの場合コンシューマが無いとDLキューに行くことも無くそのままメッセージは消えますが、SQSではメッセージは数日間は残り続けます。とはいっても、実運用上でそのようにトリガーを外すことは無いと思いますので、そこまで気にすることは無いと思います。

5. 並列処理については、基本的にはSNSでもSQSでも同じです。SNSでも複数のLambdaをサブスクリプションとして登録することもできます。また、複数の通知を同時にSNSトピックに送ると、Lambda関数は同時実行されます。しかし、逆に同時実行回数を制限することはSNSでは不可で、SQSでしかできません。また、複数のメッセージを1回の関数で処理することもSQSでしかできません。(Lambdaトリガーのバッチサイズの設定で可能です）。よって、基本的にはSNSでも大丈夫ですが、細かいことを設定するならSQSを使うのが望ましくなります。

6. 順序性については、SQSでもSNSでもFIFO機能がありますので、それを使えばどちらも同じように順番を確保した処理になります。順序だけで見ればSQSでもSNSでも同じ性能と考えて良いでしょう。

長くなりましたがまとめると、遅延設定を細かくする、優先度付き処理をする、大量リクエスト時も実行保証を持つ、並列処理の細かいカスタマイズもする、等をする場合はSNSよりSQSの方が有利です。
逆にSNSではできてSQSでは出来ない事としては、主なところでは、色んな種類(メール、API)のエンドポイントへの通知を種類ごとにメッセージを変えたりしながら送信できることが挙げられます。
そんなに高機能は使わないから、とりあえずサクっとアプリ間、コンポーネント間を疎結合化したい、という場合には以下で述べているとおり安上がりで済むSNSの方がいいでしょう。

#### SNSの方が安い？
疎結合なLambdaトリガーとして用途で、SQSとSNSのどちらでもいいような場合はSNSの方が安くなるケースが多いです。SQS->Lambdaトリガー、を設定するとLambdaは数秒(4秒~10秒)に1回SQSに対してポーリングすることになります。すると、メッセージがあろうがなかろうが1ヵ月当たり約50万回ほどメッセージを受け取ることになります。受信数はCloudwatch logのメトリクスで確認可能で、メッセージが無かった場合は空のメッセージ受信数となっています。SQSには月100万回受信の無料枠がありますが、この組合せを2つほど作ることで無料枠上限を超えてしまい、その後は1つにつき月数十円かかることになります。
対してSNSからLambdaへのトリガーは何回行っても無料です。なので、上で挙げたSQSとSNSを比較したSQSのメリットを使う必要が無いのであれば、SNSを使う方が良いということになります。

## 遅延に関するSQS設定項目
SQSの設定項目(パラメータ)をどのような観点で決めるかについて考えていきます。

- 可視性タイムアウト
こちらは非常に重要なパラメータです。まず前提として、メッセージがLambdaのトリガーとなってい場合、タイムエラーよりも長く設定する必要があります。Lambdaのトリガーとなった時点ではまだメッセージが消えたわけではなく、他のコンシューマから見えなくなっているだけです。Lambdaが実行されている最中に可視性タイムアウトが経過すると、もう一つLambdaが起動して同じメッセージに対して処理をしたりする可能性がある他、メッセージの識別子が変わることで処理が上手くいったとしても削除が上手くできず結果Lambdaがエラーになる可能性があります。[^2]公式では、可視性タイムアウトはLambdaのタイムエラー時間の6倍以上に設定することが推奨されています。タイムエラー以上の時間であれば十分かとも思いますが、何かの障害などが合っても大丈夫なようにかなり余裕を見ているものかと思います。
また、可視性タイムアウトを上手く使うことで、エラー時の再処理時間を設定することにもなります。例えばトリガーとなっているLambda関数で外部サーバと通信する関数の場合、外部サーバが落ちている場合はエラーとなり、その後すぐに再処理をしてもエラーとなる可能性が高いのである程度時間が経ってから再実行したいはずです。そんな時、可視性タイムアウトを3時間と設定すれば、3時間後に再処理されることになります。
- 遅延キュー
キューに設定する、全メッセージに適用される遅延時間です。あまり使うことは無いと思います。
- メッセージタイマー
個々のメッセージに適用される遅延時間です。こちらもあまり使うことは無いと思います。[^3]FIFOでは使えません。個々に遅延時間を設定されたらFIFO性は確保できませんからね。メッセージの内容に応じて処理・再処理までの時間を変えて設定したい場合は使えると思います。
- 受信待機時間(ポーリング時間)
ポーリングした際に「応答無し」を受け取るまでの時間です。こちらも特に意識するケースは少ないと思います。自発的にポーリングするときに関わってくるパラメータなので、SQSメッセージをLambdaトリガーの用途のみで使っている場合は0秒にしても20秒にしても挙動としては何も変わりません。自発的にポーリングをする場合で、ポーリング処理をループにしている場合には注意が必要で、0秒にすると高速な受信がずっと続くので重課金になってしまう可能性があるので適切に設定することが必要です。(そもそも、メッセージが無いにもかかわらずループしてポーリングを続けるという処理自体が良くないとは思いますが…。そうするということはメッセージに即応答したい場合だと思いますが、その場合はSNSを使う方が適しています。SQSで即応答をしたい場合はロングポーリングの方が適しています。)

## SQSに関する注意点
SQSを使う際の注意点は、個人的には結構多いとは思うのですがあまり解説記事が無かったのでここで解説します。やってはいけないこと一覧を挙げます。

- LambdaのトリガーをSQSとする場合で、エラー終了の可能性があり、可視性タイムアウトが非常に短く、DLキューがセットしないでしまっている。
-> 非常に速い無限ループが発生して重課金になる恐れがあります。この条件が合わさることはあまり無いと思いますが。DLキューさえセットされていれば、基本的にエラー処理ループが永遠に続くことは無いので安心できます。
- EventBridgeやAPI Gatewayなど、別トリガーのLambdaでポーリングしていて、その関数の成功時にメッセージを削除する処理が無い。
-> ポーリングしてメッセージを受信した場合は、しっかり削除しなければメッセージは残るので、可視性タイムアウト経過後に同じ処理が走ってしまいます。Lambdaトリガーとなっている場合は、処理が正常に終了したときに自動でメッセージが削除されますが、むしろそれが例外パターンです。普通にポーリングした場合は自発的に削除をしなければいけません。
- ポーリングをループする処理がどこかにあり、受信待期期間が非常に短く(ショートポーリング)なってしまっている。
-> 空のメッセージ受信数がすごいことになり重課金になる恐れがあります。SQSで即応答をしたい場合はロングポーリングの方が適しています。ショートポーリングは、ループでない処理の中でメッセージの有無を確認したい場合で、その処理をできるだけ短く実行したい場合に有効です。こちらの重課金パターンはエラー処理ループではなく空メッセージループによるものなので、可視性タイムアウトやエラーハンドリングとは関係ありません。
- Lambdaトリガーとなっている場合に、エラーのcatch内でSQSにリトライ用のメッセージを送った上でエラーをRaiseしてしまっている。
-> 見落としがちですが、Lambdaトリガーとなっている場合、そのLambdaがエラー終了した場合はメッセージは削除されません。なので、エラーをRaiseする場合であればリトライ用メッセージを送る必要はありません。送ってしまうと、重複することになり、メッセージが増えてしまいます。逆に言えば、catch内でraiseをしない場合(エラーは発生したが正常終了扱いする場合)は、メッセージは自動削除されてしまうのでリトライしたい場合は自発的にSQSヘリトライ用メッセージを送る必要があります。[^4]エラー終了した際にメッセージが残ったままになることは、公式にも小さくですが以下のように書かれています。
> By default, if your function encounters an error while processing a batch, all messages in that batch become visible in the queue again.
日本語で言うと、「関数側の処理がエラーとなった場合、バッチ内の全てのメッセージはキュー内で有効なメッセージとして残ります。」となります。

よく、可視性タイムアウトやポーリング時間は怖いから長めに設定するべきというような話もありますが、意味をしっかり理解してケアしていれば恐れる必要もありません。

## まとめ
SQSを実用的に運用するための、強み、注意点、パラメータ設定の考え方などを解説しました。サーバレスアプリではLambdaトリガーとしてSQSが登場する機会は多く、AWS公式でもSQSを使って柔軟性・冗長性を持たせた疎結合を推奨しているようにも思えます。ただ使用にあたっての注意点も割とあるので、気を付けながら便利に使いましょう。


[^1]:Lambdaのオートスケール説明によるとデフォルトの同時実行制限回数が1000回 https://docs.aws.amazon.com/ja_jp/lambda/latest/operatorguide/scaling-concurrency.html
[^2]:AWS公式では、可視性タイムアウトはLambdaのタイムエラー時間の6倍以上に設定することを推奨 https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-sqs.html
[^3]:メッセージタイマーはFIFOキューでは使えない https://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-message-timers.html
[^4]:Lambda関数の失敗時、キュー内のメッセージは残る https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-sqs.html