見出し画像

AWS SQS FIFOキューのハマり事例-前編

概要

AWSのSQS (Simple Queue Service)を使うと、AWSの様々なサービスを接続できます。メッセージを高い信頼性で分散処理するのに便利なメッセージングサービスです。
今回は、私がSQSについて学習を進めたときに、思ったように動かなかった例について整理してみました。

システム構成

AWS SQS (FIFOキュー)に入ったメッセージを、Lambdaで処理をするというシンプルな構成で考えます。

タスクをLambdaで非同期処理

アプリケーションは、何らかの処理タスクをメッセージとして生成する責務を持ちます。(Producer)
メッセージはSQS経由で順序どおりLambdaに通知され、非同期で実行します。(Consumer)

タスクの発生流量が一時的にConsumerの処理能力を超えても、平均してConsumerが消化しきれれば問題ありません。(ランチタイムの定食屋に長い行列ができても昼休みが終われば空いてきます。最終的に行列がなくなり、一日平均では処理能力を超えない感じです)
並行してタスクを処理可能であれば、Lambdaの実行数を増やすこともできるでしょう(定食屋2号店を隣に作る?)

SQSには標準キューとFIFOキューという2種類のタイプがあります。

キュータイプ

標準キューのほうがスケーラブルで安価なのですが、配信が1回以上届くなどアプリ実装側で考慮することが増えるため、ここではFIFOキューを使用します。FIFOキューは先入れ先出しで順序が保証されます。

サンプルコード・設定値

Producer

10件のメッセージを生成してSQSに送信するクライアントプログラムです。Pythonで記述しました。

Consumer (Lambda)

メッセージをSQSから受け取るLambda関数です。Pythonで記述しました。

SQSの設定

  • キュー名: my-queue

  • FIFOキュー

  • メッセージ保持期間: 3分

  • 可視性タイムアウト: 30秒

  • バッチサイズ: 5

実行結果(正常系)

10件のメッセージがアプリケーションから生成されSQSに届きます。
バッチサイズが5なので、最大5件のメッセージがまとまってLambdaに届きます。Pythonコードで言えば、1つのeventにrecordsがあり最大5件のメッセージが入っていることになります。

バッチサイズ5のeventとrecord

タイミング次第ですが今回の実験では1件, 5件, 4件のまとまりとしてLambdaに届きました。

バッチサイズ指定によりメッセージがまとまって届く

Producerの出力

Consumerの出力 (一部加工あり)

RequestIdのREQ**1 が1件のメッセージを、REQ**2が5件、REQ**3が4件のメッセージを処理しています。

実行結果(例外発生時)

ここまでが前置きです。この正常系の状態から例外が発生したときにどうなるか確認するため、わざとConsumerが5番目のメッセージで例外発生するようにしてみます。

5番目のメッセージを処理するときに例外発生させる

Consumerのコードは以下のように5番目のときだけ例外発生するようにします。

Consumer (Lambda)

Consumerの出力 (一部加工あり)

SQSのメッセージはLambdaが処理中の間、キューに残っているのですが他のSQSのConsumerからは見えない状態になり、同じメッセージを取らないようになります。(※標準キューでは取れることもあります)そして正常終了したときにメッセージは消えます。

非可視状態からメッセージ削除(バッチサイズ1のとき)

さて、例外発生しLambdaが異常終了すると、メッセージは見えない状態のまま削除されずに残ってしまいます。
FIFOキューはメッセージを順番通りに処理するため、見えない状態のまま残ったメッセージがあると、後続のメッセージも取り出せないまま詰まった状態になります!

非可視なメッセージが残ったまま

見えない状態のメッセージは、可視性タイムアウトの時間(今回は30秒)が経過すると再び見えるようになります。それによりConsumerのLambdaは復活したメッセージを受けとって実行します。復活したメッセージはApproximateReceiveCountが2になっており2回目の受信であることがわかります。

可視性タイムアウトすると見えなくなったメッセージが再び見えるように

ただし、このメッセージの集合には先ほど異常終了するのと同じメッセージ(No.5)が含まれているため、またこれを処理するときに例外発生し、異常終了してしまいます。

以上の振る舞いが繰り返され、最終的にはメッセージ保持期間の設定(3分)があるため、アプリケーションがメッセージを登録してから3分経過するとメッセージは消滅します。

メッセージ保持期間終了

Producerは一気に10件のメッセージを送信しているため、メッセージの生存期間はほぼ同じです。No.5のメッセージ保持期間が終了して消滅し、No. 6以降を処理できるようになってもほぼ同時にそれらのメッセージも消滅してしまいます。

最終的な動作は以下のようになりました。

  • メッセージのNo.1は正常に処理できました

  • メッセージのNo.2, 3, 4は処理できましたが、複数回実行してしまいました(べき等なのどうか?)

  • メッセージのNo.5は例外発生し処理できませんでした

  • メッセージのNo.6, 7, 8, 9, 10は一度も実行されていません

例外が発生したために、繰り返し実行が行なわれ3分間キューにメッセージが詰まったままになりました。No.6以降のメッセージは実行されないまま消滅してしまった点が問題です。

例外を投げないように対策

例外が発生して処理できないのは仕方ないので諦めて、ひとまずログ出力だけしてスキップするようにしました。
こうすることで、たとえ1つのメッセージで例外が発生しても、ほかのメッセージは処理できるようになりました。

※もちろん、本来であれば無条件に例外を潰すのではなく、対処可能なものについては対処しそれでも想定外の例外が発生したときの対策になります。内容についてもログ出力は一例であって、保存や通知が必要になりますがここでは省略しています。このあたりは後半で触れたいと思います。

Consumer (Lambda)

Consumerの出力 (一部加工あり)

一件落着と思いきや……

これで一安心だと思っていたのですが、また別の問題が発生し、キューが詰まってしまう自体が発生しました。
長くなったので、続きは次回にさせていただきます!


この記事が気に入ったらサポートをしてみませんか?