見出し画像

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

概要

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

この記事は以下の前回記事(もう6ヶ月近く前になりますが)の続編です。

前回のおさらい

AWS SQSのFIFOキューを使ったSQS-Lambdaの構成の話でした。

タスクをLambdaで非同期処理

順番にメッセージ処理することが保証されるFIFOキューでは、例外発生によりメッセージが消化されないと、後続のメッセージは待ち状態になりメッセージが詰まってしまう問題がありました。

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

例外発生したメッセージが処理されないのはともかくとして、その後のメッセージもすべて待ち状態になるのは問題だったため、例外をキャッチしてスキップすることで、ひとまずメッセージが詰まらず流れるようになったのだが…という話でした。

新たな問題「タイムアウト」

どんな例外が発生してもキャッチしてスキップしてるので、メッセージが詰まることはなくなったはずでしたが、またメッセージの詰まりが発生しました。

その原因は「Lambdaの実行時間タイムアウト」です。メッセージの内容によって外部サービスのAPIにリクエストが送られていたのですが、そのメッセージがリトライを繰り返していました。

ある特定のメッセージのAPIリクエストのときだけ、処理時間がかかるという問題があり、例外発生せずに長時間待ち続けるうちにLambdaの実行時間タイムアウトとなっていました。

メッセージNo.5で時間がかかりすぎてLambda実行時間タイムアウト

Lambdaの実行時間タイムアウト(Task timed out)になるとプロセスが強制的に停止されるためアプリケーションでの例外処理はできません。そうなると以前と同じように非可視なメッセージが残り、その可視性タイムアウトが過ぎて、処理されるまで後続のメッセージは待たされます。

Task timed outで異常終了
可視性タイムアウトが起きて再び見えるようになる

FIFOキューは順番に取得されることを保証しているので、このメッセージを処理するときの「Lambda実行時間タイムアウト」に再現性があると、いつまでたっても後続のメッセージは処理できなくなります。

サンプルコード・設定値

Producer

メッセージを送信する側のコードは前回と同じです。10件のメッセージを送信します。

Consumer

メッセージをSQSから受け取るLambda関数です。No.5のメッセージを受信シたときに、あえて6秒間ウェイトするようにしています。Lambda実行時間のタイムアウトは5秒に設定しているため、6秒を待つことはできず強制的に終了される想定です。
前回はここで例外をわざとraiseしていましたが、今回は例外をraiseせず6秒のスリープ処理をするだけです。

SQS、Lambdaの設定

  • キュー名: my-queue

  • FIFOキュー

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

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

  • バッチサイズ: 5

  • Lambdaのタイムアウト5秒

実行結果

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

メッセージNo.5を処理するときに5秒の時間切れでTask Timed outとなっています。(10行目、18行目)可視性タイムアウトの30秒後に同じメッセージが取得されています。(13行目)

対策

外部サービスのAPIリクエストが延々と待ち続けるとTask timed outが発生してしまうので、APIリクエストにタイムアウト時間を設定し、一定時間応答がない場合は例外が発生するようにします。

Task timed outが発生してしまうと、アプリケーションではなにも処理できずに強制終了されてしまうので、これを発生させないような余裕をもったタイムアウト時間にします。

追加の対策

ここまでFIFOキューを使ってきた中で、FIFOキューを使った場合は順序が維持され、処理されないメッセージがあると後続のメッセージがすべて待たされるということが分かりました。そこから理解したのは、

何度やってもダメなメッセージは諦めて次を処理しなければならない

ということです。最初の例では例外をキャッチしてログ出力をして例外を発生させないようにしていました。これが「諦める」という処理だったのでした。しかしログ出力だけではメッセージが失われてしまいます。

もっと良い方法として、デッドレターキューを使って失敗メッセージを別のキューに分離する方法です。また、バッチサイズ5で5個のメッセージのうち一部が失敗したことを報告するために、「部分バッチレスポンス」(BatchItemFailures)を使うことにしました。

部分バッチレスポンス

バッチサイズを指定して複数のメッセージを受け取った際に、その一部だけをエラーメッセージとして返却する方法です。

こちらのサンプルコードのように、失敗したメッセージ(Record)のMessegeIdを失敗メッセージとしてレポートすることで、今回の例でいう1バッチ内5メッセージのうち、No.5だけ失敗として通知し、No2,3,4,6は正常処理として完了させることができます。(部分バッチレスポンスを使わない場合は全部失敗か全部成功しかできません

この対策を施したコードがこちらになります。

デッドレターキュー

一定回数受信されたメッセージを別のキューに分離するものです。FIFOキューでは失敗したメッセージは詰まりに直結するので、速やかにデッドレターキューに分離することで後続のメッセージへの影響を減らせます。

デッドレターキュー

今回は失敗メッセージを再試行せずに即座に分離したかったので、最大受信数を1にしています。このようにすることで、No.5の失敗メッセージは1度の失敗でデッドレターキューに移動し、後続のNo.6を処理できるようになりました。

部分バッチレスポンスで失敗メッセージを返却しデッドレターキューへの受信数を1とする

まとめ

以上の設計をまとめると以下の3点です。

  • Lambdaの実行時間タイムアウトはできるだけ避ける

  • 部分バッチレスポンスで一部のメッセージ失敗のみを返却

  • デッドレターキューで後続に影響させないタイミングでメッセージを分離

なかなか学びの多い、SQS + Lambdaの設計でした!


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