エラー
突然、Amazon SQS の処理で、エラーが発生するようになった。
Error executing "DeleteMessage" on "https://sqs.ap-northeast-1.amazonaws.com" AWS HTTP error: Client error: `POST https://sqs.ap-northeast-1.amazonaws.com` resulted in a `400 Bad Request` Value XXXXX for parameter ReceiptHandle is invalid. Reason: The receipt handle has expired.
これは、 PHP のフレームワーク Laravel で AWS SQS を使っている時のエラーだが、PHP 以外でも似たようなエラーが起こることがある。 The receipt handle has expired.
とはなんぞや?と思い、調べてみると、「可視性タイムアウト」の設定と関係しているらしい
可視性タイムアウトとは
AWS SQS の設定画面を確認すると、いくつかの設定があり、その中に可視性タイムアウトの設定がある。
可視性タイムアウトを理解するために、SQS やキューの説明をしたうえで、可視性タイムアウトについて説明する。
説明のための用語
- キュー
- 今回は Amazon SQS のこと
- ワーカー
- 今回は、SQS からメッセージを受け取って処理するプログラムのことをこう呼ぶことにする
- 僕のケースでは Laravel の「キューワーカー」がこれに相当する
- メッセージ
- キューに積まれるやつ
キューの処理の仕組みと可視性タイムアウト
ワーカーがキューからメッセージを受け取って処理をする場合、1つのメッセージを処理する流れは、ざっくり以下のようになる*1
- ワーカーがキューからメッセージを受け取る
- この時、キューからメッセージは消えないが、一時的に非表示になる(= 処理中のメッセージという扱いになるイメージ)
- ワーカーが処理をする
- ワーカーの処理が完了したら、ワーカーからキューに "DeleteMessage" を送る
- ここでキューからメッセージが完全に消える
ワーカーがメッセージを受け取った時、キューからメッセージは消えないが、一時的に非表示になるのが重要なポイントだ。
メッセージが非表示になるのはなぜなのか?
ワーカーが突然落ちた場合、メッセージの処理をやり直す必要がある。このとき、キューからメッセージが消えてしまっていると、メッセージの情報が失われてしまう。
ただし、メッセージを残ったままにして重複して処理されるのは避ける必要がある。そこで、処理中のメッセージは非表示状態(ワーカーから見えない状態)になる。
非表示になったメッセージはどうなるのか?
メッセージが非表示になったあと、ワーカーから「処理成功」のレスポンスがあれば、キューからそのメッセージを完全に削除する。正確には、ワーカーから DeleteMessage のリクエストを送信し、キューからメッセージが消える。
失敗のレスポンスがあれば、メッセージは表示状態に戻り、再度ワーカーによって処理される。
ワーカーからのレスポンスが無かった場合
ワーカーがメッセージの処理途中で突然落ちたりして、成功のレスポンスも失敗のレスポンスも来ない場合がある。この時、メッセージが非表示のままだと一生処理されずに残ってしまうので、一定時間が経つとタイムアウトして、メッセージが表示状態にもどる。メッセージは再度ワーカーによって処理される(死んだプロセスが再起動して処理を再開したり、別のワーカーが処理したり)。
この時のタイムアウトが「可視性タイムアウト」になる、
ワーカーの処理が遅くて可視性タイムアウトを超えてしまうと、処理が失敗したとしてメッセージは表示状態にもどる。可視性タイムアウトを超えたあとにワーカーの処理が成功し、ワーカーからキューに DeleteMessage が送られると、エラーになる(メッセージは消せない)。
これが今回発生していたエラーである。
ワーカーは、可視性タイムアウトの前にメッセージの処理を完了させる必要がある。
Amazon SQS ではデフォルトで可視性タイムアウトが30秒になっているため、この設定を意識せずに実装していると、いつの間にか可視性タイムアウトを超えてしまうことがある。特に terraform で構築していると、指定しなくても構築できてしまうので、意識して指定するようにすること。
対応方法
今回の問題は、サービスを運用しているうちにワーカーが処理が重くなり、処理時間が可視性タイムアウトを超えてしまっていたために発生していた。可視性タイムアウトを超えたメッセージを Delete しようとすると、The receipt handle has expired.
のエラーが発生するようだ。
対応方法としてはどちらかである
- 可視性タイムアウトを伸ばす
- ワーカーのスペック強化や、処理の修正によって、可視性タイムアウト以内に処理が終わるようにする
可視性タイムアウトを伸ばすと、失敗時のリトライまでが長くなったり、キューが詰まるなどのデメリットはあるが、ワーカーの高速化が簡単にできるとは限らないので、状況によって適切な手段を選んでほしい。
参考
*1:細かい部分は省略している