Amazon SQSを使う前に知っておきたい基本的なこと

# はじめに

Amazon SQS + Amazon Lambdaを利用して実装を行ったのですが、恥ずかしながらSQSの基本的な仕組みをきちんと理解せずに実装をすすめたため、途中でハマったり、当初の設計を変更するハメになりました。
僕と同じような状況に陥る人が今後出ないように、勘違いしていたところや、気をつけるべき点を説明します。

# ドキュメントを読む
公式のドキュメントは[こちら](http://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/Welcome.html)

# 全て取得できることを保証しない / 重複取得もありえる

超基本的なこと、その1です。
SQS APIからメッセージを取得する際は、最大で10件まで同時に取得可能ですが、
( [詳しくはこちら](http://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_ReceiveMessage.html) )

例えば、キューに`A`,`B`,`C`というメッセージが3件だけある状態で、SQS APIから10件メッセージの取得を行った場合に、`A`のメッセージのみを10件取得する、というケースも普通にありえるのです。
複数のサーバーにメッセージを保存 / 取得している関係上、キューにメッセージがあるにもかかわらず、受信リクエストを送ってもレスポンスが空のケースも十分おこりえるし、削除済みのメッセージを再度取得してしまうケースもまれにある、とドキュメントには書いてあります。

このように、メッセージの取得は結構ファジーです。
このような仕様に対応するためには、

### [ロングポーリング](http://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-long-polling.html)を使う

`ReceiveMessage`のオプションの`WaitTimeSeconds`を0秒以上にすると、ロングポーリングとなります。ロングポーリングは上記の空レスポンスを防ぎます。

### 重複実行しても問題のない処理を書く

さっき処理したはずのメッセージを再度取得してしまうケースもあるので、同じ処理が重複して実行されても問題の無いよう、処理を書きましょう。

# メッセージの取得順は保証されない
上とかぶりますが、超基本的なこと、その2です。
メッセージを取得する際に、古いものから取得したり、新しいものから取得したり、条件を指定することはできません。
そのため、処理順に影響されないような処理に、SQSを用いましょう。

もし、処理順序が重要な処理にSQSを使う場合は、オレゴン、オハイオリージョンにて、[FIFO(先入れ先出し)キュー](http://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues.html)が提供されています。

# 処理が終わったら削除処理を行おう
これも基本的なことですね。考えてみれば当たり前の事です。
処理が正常に終了したのに、キューに残っていては、再度同じ処理が実行されてしまいます。
削除処理にて気をつけるべきことは2点あります。

## 削除するためには最新のReceiptHandleを使用すること
メッセージを受信した際に、同時にReceiptHandleというハッシュのようなものもついてきます。
メッセージを削除するためには、キューのURLとReceiptHandleにて、削除する対象のメッセージを特定しなければならないので、処理が終わるまで大切に保管しましょう。
また、削除するためには、ReceiptHandleは**最新**のものでなければなりません。
たとえ同一のメッセージを何度も受信したとしても、ReceiptHandleは受信するたびに変化します。
あるメッセージ`A`を受信した後に、削除するためにReceiptHandle`A1`を保存していたとします。
しかし、削除を行う前に、再度メッセージ`A`を受信してしまうと、`A1`ではもう削除できなくなってしまいます。この場合は、再度メッセージ`A`を受信してきた際に取得したReceiptHandle`A2`を使用しないと削除できません。
複数のプロセスで、1つのキューを取得するケースなどは、十分注意する必要があります。

後述しますが、このようなケースを防ぐために、可視性タイムアウト(VisiblityTimeout)を必ず設定しておく必要があります。


## メッセージの削除処理は、処理が正常に終わった後にすること
メッセージの削除に最新のReceiptHandleが必要なら、取得したらすぐ削除したらいいじゃん、と思うかもしれませんが、おすすめはしません。もし処理にエラーが発生して、正常に終了しなかった場合に、デバッグがしづらくなってしまうためです。
後述するデッドキューを設定することで、効率的にエラーハンドリングが可能です。

# 可視性タイムアウトを設定すること
可視性タイムアウトとは、一定期間、他のプロセスからメッセージを取得できないようにする仕組みです。
複数のプロセスで、1つのキューを取得するケースの場合は、削除処理を行わなければ、複数のプロセスで1つのメッセージを受信してしまい、複数同時に実行される、といったケースが考えられます。

このようなケースを防ぐために可視性タイムアウトを設定しましょう。
1秒から12時間まで指定が可能です。
メッセージを受信してから、指定された時間まで、他のプロセスがそのメッセージを取得することができなくなります。

 * 例: `プロセス1`が、キュー`X`から、メッセージ`A`を取得した。メッセージ`A`の可視性タイムアウトは1分間に設定されているので、`プロセス2`がキュー`X`へメッセージ取得のリクエストを投げたとしても、`プロセス2`がメッセージ`A`を1分間のうちは取得することができない

可視性タイムアウトの制限時間内に、処理を完了し、メッセージの削除を行いましょう。
よって、可視性タイムアウトの制限時間は、最大の処理時間をセットしたほうが良いでしょう。


# デッドレタードキューを設定すること
先ほど、メッセージの削除処理は、処理が正常に終わった後にすることをおすすめしましたが、
「じゃあ、正常に処理が終わらない場合は削除されないから、ずっとキューにメッセージが残り続けて、延々と処理が反復して行われるじゃないか」と思われるかもしれません。

このような自体を防ぐために、デッドレターキューを設定しましょう。
正常に処理がされなかったメッセージを、別のキューに移動させることができます。

デッドレターキューは、キューごとに設定が可能です。
デッドレターキューを設定する際に、最大の受信回数を設定することができます。ここで設定した回数以上、メッセージが受信された(つまり、削除されずに何度も受信処理が行われたもの)を、指定したキューに移動させることができます。

定期的にデッドレターキューの中身を取得し、メールやSlackへ内容を送信する仕組みを作れば、容易にエラーが発生したことに気づくことができ、デバッグも楽に行なえます。

#まとめ

* 取得する際はロングポーリングを使おう
* 重複実行しても問題のない処理を書こう
* 処理順がどうしても重要な場合はFIFOキューを使おう
* 処理がきちんと終わったらメッセージを削除しよう
* 可視性タイムアウトを、処理にかかる時間だけ設定しよう
* デッドレターキューを設定し、エラーハンドリングを行おう