はじめに
Amazon SQS + Amazon Lambdaを利用して実装を行ったのですが、恥ずかしながらSQSの基本的な仕組みをきちんと理解せずに実装をすすめたため、途中でハマったり、当初の設計を変更するハメになりました。
僕と同じような状況に陥る人が今後出ないように、勘違いしていたところや、気をつけるべき点を説明します。
ドキュメントを読む
公式のドキュメントはこちら
全て取得できることを保証しない / 重複取得もありえる
超基本的なこと、その1です。
SQS APIからメッセージを取得する際は、最大で10件まで同時に取得可能ですが、
( 詳しくはこちら )
例えば、キューにA
,B
,C
というメッセージが3件だけある状態で、SQS APIから10件メッセージの取得を行った場合に、A
のメッセージのみを10件取得する、というケースも普通にありえるのです。
複数のサーバーにメッセージを保存 / 取得している関係上、キューにメッセージがあるにもかかわらず、受信リクエストを送ってもレスポンスが空のケースも十分おこりえるし、削除済みのメッセージを再度取得してしまうケースもまれにある、とドキュメントには書いてあります。
このように、メッセージの取得は結構ファジーです。
このような仕様に対応するためには、
ロングポーリングを使う
ReceiveMessage
のオプションのWaitTimeSeconds
を0秒以上にすると、ロングポーリングとなります。ロングポーリングは上記の空レスポンスを防ぎます。
重複実行しても問題のない処理を書く
さっき処理したはずのメッセージを再度取得してしまうケースもあるので、同じ処理が重複して実行されても問題の無いよう、処理を書きましょう。
メッセージの取得順は保証されない
上とかぶりますが、超基本的なこと、その2です。
メッセージを取得する際に、古いものから取得したり、新しいものから取得したり、条件を指定することはできません。
そのため、処理順に影響されないような処理に、SQSを用いましょう。
もし、処理順序が重要な処理にSQSを使う場合は、オレゴン、オハイオリージョンにて、FIFO(先入れ先出し)キューが提供されています。
処理が終わったら削除処理を行おう
これも基本的なことですね。考えてみれば当たり前の事です。
処理が正常に終了したのに、キューに残っていては、再度同じ処理が実行されてしまいます。
削除処理にて気をつけるべきことは2点あります。
削除するためには最新のReceiptHandleを使用すること
メッセージを受信した際に、同時にReceiptHandleというハッシュのようなものもついてきます。
メッセージを削除するためには、キューのURLとReceiptHandleにて、削除する対象のメッセージを特定しなければならないので、処理が終わるまで大切に保管しましょう。
また、削除するためには、ReceiptHandleは最新のものでなければなりません。
たとえ同一のメッセージを何度も受信したとしても、ReceiptHandleは受信するたびに変化します。
あるメッセージA
を受信した後に、削除するためにReceiptHandleA1
を保存していたとします。
しかし、削除を行う前に、再度メッセージA
を受信してしまうと、A1
ではもう削除できなくなってしまいます。この場合は、再度メッセージA
を受信してきた際に取得したReceiptHandleA2
を使用しないと削除できません。
複数のプロセスで、1つのキューを取得するケースなどは、十分注意する必要があります。
後述しますが、このようなケースを防ぐために、可視性タイムアウト(VisiblityTimeout)を必ず設定しておく必要があります。
メッセージの削除処理は、処理が正常に終わった後にすること
メッセージの削除に最新のReceiptHandleが必要なら、取得したらすぐ削除したらいいじゃん、と思うかもしれませんが、おすすめはしません。もし処理にエラーが発生して、正常に終了しなかった場合に、デバッグがしづらくなってしまうためです。
後述するデッドキューを設定することで、効率的にエラーハンドリングが可能です。
可視性タイムアウトを設定すること
可視性タイムアウトとは、一定期間、他のプロセスからメッセージを取得できないようにする仕組みです。
複数のプロセスで、1つのキューを取得するケースの場合は、削除処理を行わなければ、複数のプロセスで1つのメッセージを受信してしまい、複数同時に実行される、といったケースが考えられます。
このようなケースを防ぐために可視性タイムアウトを設定しましょう。
1秒から12時間まで指定が可能です。
メッセージを受信してから、指定された時間まで、他のプロセスがそのメッセージを取得することができなくなります。
- 例:
プロセス1
が、キューX
から、メッセージA
を取得した。メッセージA
の可視性タイムアウトは1分間に設定されているので、プロセス2
がキューX
へメッセージ取得のリクエストを投げたとしても、プロセス2
がメッセージA
を1分間のうちは取得することができない
可視性タイムアウトの制限時間内に、処理を完了し、メッセージの削除を行いましょう。
よって、可視性タイムアウトの制限時間は、最大の処理時間をセットしたほうが良いでしょう。
デッドレタードキューを設定すること
先ほど、メッセージの削除処理は、処理が正常に終わった後にすることをおすすめしましたが、
「じゃあ、正常に処理が終わらない場合は削除されないから、ずっとキューにメッセージが残り続けて、延々と処理が反復して行われるじゃないか」と思われるかもしれません。
このような自体を防ぐために、デッドレターキューを設定しましょう。
正常に処理がされなかったメッセージを、別のキューに移動させることができます。
デッドレターキューは、キューごとに設定が可能です。
デッドレターキューを設定する際に、最大の受信回数を設定することができます。ここで設定した回数以上、メッセージが受信された(つまり、削除されずに何度も受信処理が行われたもの)を、指定したキューに移動させることができます。
定期的にデッドレターキューの中身を取得し、メールやSlackへ内容を送信する仕組みを作れば、容易にエラーが発生したことに気づくことができ、デバッグも楽に行なえます。
#まとめ
- 取得する際はロングポーリングを使おう
- 重複実行しても問題のない処理を書こう
- 処理順がどうしても重要な場合はFIFOキューを使おう
- 処理がきちんと終わったらメッセージを削除しよう
- 可視性タイムアウトを、処理にかかる時間だけ設定しよう
- デッドレターキューを設定し、エラーハンドリングを行おう