【新機能】Amazon SQSにFIFOが追加されました!(重複削除/単一実行/順序取得に対応)

SQSの大型アップデートです!
オンプレでエンタープライズな開発を行ったことがある方であれば、分散キューシステムの設計が大変だったと思います。実際のところは高額ライセンス商品を買うしか選択肢はなかったのではと。Amazon SQSの登場によって、今まで実装が大変だったノンコア機能のキューが、超安価に簡単に使えるようになったのは衝撃でした。これだけでクラウドを使う理由になりました。
そして、年月は流れ、この度SQSが進化しました!まずは、今までのSQSの課題についておさらいしたいと思います。
標準キュー
今までのSQSは、メディアエンコーディングや大量タスクの分散処理などに適していましたが、いくつかの用途においてフィットしなかったり、独自実装をする必要がありました。
順番が保証されない
SQSは高可用性を持った分散キューシステムですので、1つのエンドポイントに投げられたメッセージは複製され蓄積されます。そのため、メッセージを取り出した際に、一番古いキューデータを取るとは限りませんでした。メディアエンコーディングであれば実行順番は関係ありませんが、商品をカートに入れて購入して配送といった順番を持つことが必須のワークフローではキューの活用は難しかったです。
※できるだけFIFOになるように努力をしていますが保証していません。
メッセージが重複する可能性がある
SQSは高可用性を持った分散キューシステムですので、メッセージを取得するプログラムが複数同時に実行されると、タイミングの問題で、同じメッセージを取得してしまう可能性があります。もちろん、そうならないようにメッセージを取得すると取得マークが付いて、処理が終わったら削除、失敗したら戻すといった機能は以前から入っていますが、重複しないことを保証して居ませんでした。ですから、メディアエンコーディングであれば、2回実行しても結果は変わらないですが、商品購入プロセスを考えた場合、1回の購入処理を2回実行してしまう可能性があっては困るわけです。
呼び出し側が2回呼んだら2回実行
これはSQSの問題では無いのですが、例えばモバイルアプリなど通信が安定しない環境で何かしらSQSにメッセージを入れようとした場合、たまたま通信が切ればアプリ側で再実行してしまうことがあります。そうすると、2回実行されてしまうことがあります。これはソシャゲの設計などで良く考慮されていることで、Re-runnable設計といって、2回トランザクションが呼ばれても状態として異常にならないようにする設計方法です。
標準キューは、"必ず最低1回は実行されることを保証し、高速にタスクを実行したい時に使うサービス"と憶えておいてください。
FIFOキュー
今回の大型アップデートによってFIFOがサポートされました。つまり、メッセージを入れた順番で処理することが保証されます。
順番が保証されます
もう狂喜乱舞です。1,2,3の順番でメッセージを入れたら、必ず1,2,3の順番が保証されます。
複数回実行されません
もう狂喜乱舞です。例えば、商品の購入という手続きを2回実行しないことを保証します。
間違って同じ呼び出しをしたら削除します
もう狂喜乱舞です。呼び出す側が意図せずに2回呼んでしまうことって結構あるんですよね。これを排除するためにRe-runnable設計が必要でしたが、全てのシステムにおいてこれを考慮することは大変です。メッセージの重複をチェックする期間はデフォルトで5分ですので、あくまでも意図せず連続して2回送ってしまった場合の削除と考えておいてください。
FIFOキュー使ってみる
それでは、FIFOキューを使ってみましょう。とりあえずオレゴンあたりを選んでSQSを作成します。
FIFOキューの名前は接尾語として.fifoを書く必要があります(.lifoの登場の予感w)。
ここで注目するのは、"メッセージグループID"と、"メッセージ重複削除ID"です。メッセージグループIDは、キューからメッセージを取り出す1つのワーカーが専有する単位です。そして、そのワーカーにおいては、FIFOが保証されます。例えば、特定のユーザーのセッションIDを入れるなどして特定のユーザーの実行順番を保証したりできます。メッセージ重複削除IDは、UUIDやSHA1ハッシュなどを入れることで、意図せずに同じメッセージが送られたとしてもFIFOキュー内で削除します。
"メッセージグループID"と、"メッセージ重複削除ID"を使ってみて最初に、このIDってマネージドで自動付与で良いのでは無いかと思いました。しかし、よく考えてみると理にかなっていました。
例えば、コンサートのチケット予約を考えてみます。コンサートでは、座席エリアが分けられているとします。座席数には限りがあり、早い者勝ちです。この場合、FIFOキューを使うと簡単に実現できます。座席IDをメッセージグループIDとすることで、ある座席について予約の注文が入った順番にワーカーで処理をすることができます。あるワーカーが処理をしている間は、同じメッセージIDについて他のワーカーが次のメッセージを処理をすることはできません。先に予約した人が決済まで完了すれば確定として、メッセージグループIDを開放し、次の座席IDを処理するために空いているメッセージグループIDを取得します。1つのワーカーが同じグループIDのメッセージを取り続けるわけではありません。
例えば、全く同じメッセージを受け付けたくないサービスがあったとします。この場合、"メッセージ重複削除ID"は、メッセージボディのハッシュ値にすると良いかもしれません。例えば、全てのメッセージは固有であるとしたいのであれば、UUID等も用いれば良いかもしれません。
メッセージを取り出す時の工夫もあります。ワーカーがReceive Request Attempt IDを指定して受信することで、同じメッセージを受信することができます。ネットワークの問題などが発生した際に、もう1回取れるようにするためにIDを指定します。これは実際のところはUUIDで良いかと思います。
コマンドラインでFIFOキュー使ってみる
概念が分かりましたので実際にCLIを使ってやってみましょう。FIFOキューを作成する際に属性を指定しますので、属性情報を書いたファイルを用意します。
$ cat create-queue.json
{
"FifoQueue": "true",
"ContentBasedDeduplication": "true"
}
次に、FIFOキューを作成して、URLを変数に入れておきます。
$ QURL=$(aws sqs create-queue --region us-west-2 --queue-name myq.fifo \ --attributes file://create-queue.json | jq -r .QueueUrl) $ echo $QURL https://us-west-2.queue.amazonaws.com/XXXXXXXXXXXX/myq.fifo
FIFOキューを作成できました。次に、1件メッセージを送ります。
$ aws sqs send-message --queue-url $QURL --region $REGION \
--message-body hello --message-group-id group1 --message-deduplication-id 11111111111
{
"MD5OfMessageBody": "5d41402abc4b2a76b9719d911017cZZZ",
"SequenceNumber": "18825507834392815111",
"MessageId": "8f190a7c-d85f-4246-bb19-42cc2fdc8zzz"
}
ここでmessage-deduplication-idを前回と同じで時間を置かずに送ってみます。
$ aws sqs send-message --queue-url $QURL --region $REGION \
--message-body hello --message-group-id group1 --message-deduplication-id 11111111111
{
"MD5OfMessageBody": "5d41402abc4b2a76b9719d911017czzz",
"SequenceNumber": "18825508188058863111",
"MessageId": "b78be0b9-d026-44de-bd28-31d918b96bbb"
}
そしてキュー内メッセージの数をカウントしてみます。
$ aws sqs get-queue-attributes --queue-url $QURL --region $REGION \
--attribute-names ApproximateNumberOfMessages
{
"Attributes": {
"ApproximateNumberOfMessages": "1"
}
}
メッセージを2件入れたのですが、message-deduplication-idが同じだったので後から入ったものが削除されています。なお、同じmessage-deduplication-idだったとしても、5分経過していれば別物として扱いますので注意してください。
続いて、同じmessage-group-idで、異なるmessage-deduplication-idを入れます。
$ aws sqs send-message --queue-url $QURL --region $REGION \
--message-body hello --message-group-id group1 --message-deduplication-id 22222222222
{
"MD5OfMessageBody": "5d41402abc4b2a76b9719d911017c592",
"SequenceNumber": "18825508280833007616",
"MessageId": "0df51fae-17c7-44b9-b60f-f667416241d7"
}
$ aws sqs send-message --queue-url $QURL --region $REGION \
--message-body hello --message-group-id group1 --message-deduplication-id 33333333333
{
"MD5OfMessageBody": "5d41402abc4b2a76b9719d911017c592",
"SequenceNumber": "18825508282470639616",
"MessageId": "2ec94b13-3aeb-48b3-beca-5aa87b3b1247"
}
$ aws sqs get-queue-attributes --queue-url $QURL --region $REGION \
--attribute-names ApproximateNumberOfMessages
{
"Attributes": {
"ApproximateNumberOfMessages": "3"
}
}
次に、異なるmessage-group-idで、異なるmessage-deduplication-idを入れます。
$ aws sqs send-message --queue-url $QURL --region $REGION \
--message-body hello --message-group-id group2 --message-deduplication-id 44444444444
{
"MD5OfMessageBody": "5d41402abc4b2a76b9719d911017c592",
"SequenceNumber": "18825508313601775616",
"MessageId": "020a5d1b-66a7-4735-b14a-10d6e9bdc667"
}
$ aws sqs send-message --queue-url $QURL --region $REGION \
--message-body hello --message-group-id group2 --message-deduplication-id 55555555555
{
"MD5OfMessageBody": "5d41402abc4b2a76b9719d911017c592",
"SequenceNumber": "18825508315499503616",
"MessageId": "da1cecb5-12ab-4603-880c-df10162c5b66"
}
$ aws sqs send-message --queue-url $QURL --region $REGION \
--message-body hello --message-group-id group2 --message-deduplication-id 66666666666
{
"MD5OfMessageBody": "5d41402abc4b2a76b9719d911017c592",
"SequenceNumber": "18825508317191407616",
"MessageId": "b2e7a84c-f666-49c1-b54a-baf285d58f1c"
}
$ aws sqs get-queue-attributes --queue-url $QURL --region $REGION \
--attribute-names ApproximateNumberOfMessages
{
"Attributes": {
"ApproximateNumberOfMessages": "6"
}
}
これで、2つのグループに3件ずつのメッセージが入りました。
次にワーカーとしてメッセージを取得してみます。
$ aws sqs receive-message --queue-url $QURL --region $REGION \
--attribute-names All --receive-request-attempt-id XXXXXXXX
{
"Messages": [
{
"Body": "hello",
"Attributes": {
"ApproximateFirstReceiveTimestamp": "1479522222159",
"SequenceNumber": "18825508189998863616",
"SenderId": "AIDAJWWW7RG7FZPNOUMRM",
"MessageDeduplicationId": "11111111111",
"SentTimestamp": "1479547321677",
"ApproximateReceiveCount": "22",
"MessageGroupId": "group1"
},
"ReceiptHandle": "AQEBh6GSQaTpYI+MwwdjmN1ppNVs17vXBqNjOaaDg7/Z7dGwWeFghyXTemSqbWusMpndsaRmW7kE9iNWuvgjY2UggoKhUJ9cKP0J8zu146FJBiTpAGathD/E/S7oum62MtSDDiy7WdrkfdULDUetnK/cmhgsp5G66E7CCCCCC6Pymws/MZcHAglumqFzd5hP0SFQYHuujMNmK9V6kzCwpK8ZbKVTFsmwa6is7A66Mw8v3qO+l498FXixTDhD+ni6eKrYY0sXtBQUoznmjI6+E7rN/g==",
"MD5OfBody": "5d41402abc4b2a76b9719d911017c592",
"MessageId": "b78be0b9-d026-44de-bd28-31d918b966fb"
}
]
}
receive-request-attempt-idを指定していますので、何度やっても同じメッセージが取得できます。(ただし5分の期限があります)
receive-request-attempt-idを別にしてメッセージを取得してみます。
$ aws sqs receive-message --queue-url $QURL --region $REGION --attribute-names All --receive-request-attempt-id YYYYYYYY
{
"Messages": [
{
"Body": "hello",
"Attributes": {
"ApproximateFirstReceiveTimestamp": "1479553270430",
"SequenceNumber": "18825509710634991872",
"SenderId": "AIDAJXUQ7RG7FZPNOUMRM",
"MessageDeduplicationId": "44444444444",
"SentTimestamp": "1479553269240",
"ApproximateReceiveCount": "3",
"MessageGroupId": "group2"
},
"ReceiptHandle": "AQEBQBWMrkJ5ic0479Qm+gNxkq/J5TxpjYzkIetJ58x/A4NLrdCx3zwWj7+cFJJ1bw7T+c5/f0w7hDM46XDOquOqeS/ggTxohzdikZEtwjRyd37tEXzbqp+GzfKy1kLAWgpDefkn62q7eRw/vxvGzw0gDbvvGrydU2X8vPMsnpOoEZihzN/UMYisLE0LaD3NO8V97EI4dTEZK8SU2bcwnTJk/9W2HqgFXfkM7bf4Pp4p9U8djgG0cCrSeju4H6bMNj3yvsvF942G88/10oitQrsQsA==",
"MD5OfBody": "5d41402abc4b2a76b9719d911017c592",
"MessageId": "3d40d023-7543-4a34-9088-9beb21b420c4"
}
]
}
今度は他のメッセージグループIDが取れました。
まとめ
Amazon SQSのFIFOキューは、今まで独自実装で実現していた業務的な流れの制御を、SQSの仕様でカバーできるようになり、システムをとてもシンプルに開発することができるようになりました。"メッセージグループID"と、"メッセージ重複削除ID"の活用によって、どのような単位で処理の順番を保証するか制御できるようになりました。世の中の多くの業務システムはSQS必須になると予想しています。ぜひご活用ください。







