Amazon SQS とよくある非同期処理設計

[Shoryuken]:https://github.com/ruby-shoryuken/shoryuken
[Amazon SQS]:https://aws.amazon.com/jp/sqs/
[AWS SDK]:https://aws.amazon.com/jp/developer/tools/
[可視性タイムアウト]:https://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html
[auto_visibility_timeout]:https://github.com/ruby-shoryuken/shoryuken/wiki/Worker-options#auto_visibility_timeout

# はじめに

非同期処理って難しいですよね。
私の開発現場では [Shoryuken] gem (Ruby) を使って、[Amazon SQS] を用いた非同期処理を実現しています。

本記事では [Amazon SQS] 世間一般でのよくある仕組みを解説してみようと思います。

# よくある SQS を使った非同期処理

下図の様な、AWS 上に構築された Web アプリケーション (シングルページアプリケーション) を想定します。

1. ユーザーがブラウザで操作 (時間のかかる機能の実行指示) をする
2. Web サーバーは [Amazon SQS] にメッセージを投下し、(重い処理はここではせずに) 即座にレスポンスを返す
3. Worker サーバーは [Amazon SQS] 上のメッセージを受信し、処理を開始する

![7a2e4575-1106-87d3-1a64-b32e9f46c98f.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/34076477-a6ea-2a15-71ba-a52f69013f83.png)

:::note
以降、[Amazon SQS] の説明では上記サーバーをそれぞれ以下の様に表記します。
* Web サーバー →`Client`
* Worker サーバー → `Worker`
:::

# Amazon SQS

[Amazon SQS] を利用する際は [AWS SDK] や Ruby アプリケーションの場合は [Shoryuken] の様なライブラリを用いる事が多いと思います。

基本的に次の３つの操作が主になります。

1. メッセージを投入する
2. メッセージを受信する
3. メッセージを削除する

![Pasted_Image_2023_03_03_9_24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/f5afd0d6-0c32-8453-9ced-81f9bc16bbc9.png)

## キューにメッセージを投入

`Client` は非同期処理を `Worker` に依頼する為に、[Amazon SQS] にメッセージを投入します。

![df96bdba-a50c-63fb-64d6-eac71b62d421.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/792f5934-5698-d8ea-427b-d4bb17268cbc.png)

一般的に [Amazon SQS] を FIFO で構築した場合は、次に投入するメッセージは後ろに積まれます。
つまり **先に投入されたメッセージから順に処理** されます。

![6900b845-724d-cb85-1d82-8d1862e30ea1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/1868b4c6-c435-26e3-85ee-4bf3bb7bebc6.png)

## Worker サーバーがメッセージを受け取る

`Worker` は FIFO の場合は最も古い (先に入れた) メッセージを受信します。

この時、[Amazon SQS] では [可視性タイムアウト] という仕組みがあり、受信したメッセージは (Default では30秒間) 見えなくなります。

![c1093d99-9532-7d7b-f5f5-4ba1ebc37097.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/36087b2d-180a-b621-6f8a-baa157482db2.png)

下図では、別の `Worker` (2) が [Amazon SQS] からメッセージを受信していますが、 `message 1` は見えない為、`message 2` が受信されます。

![0854f5e2-62cc-7245-af6e-025f984cb901.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/8cb2f5d1-c0ba-d0ec-b6a3-f871c0733c2a.png)

:::note
一見すると *「メッセージ受信する際に削除してくれればいいじゃん！」* と思うのですが、その理由は後述する「リトライ」にあります。
:::

## 非同期処理を正常終了する

無事 `Worker` (1) は、非同期処理を終えました。

その際に `Worker` は必ず **受信したメッセージを削除** しなければいけません。

![a2f33db9-2a96-9848-c398-00d7e1034d23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/7c454ad7-3f1b-e82e-b55f-2b67767431e3.png)

その理由は...

## リトライ

前述の通り、[Amazon SQS] には [可視性タイムアウト] という仕組みがあり、メッセージが削除されずに一定時間が立つと、また見える様になります。

例えば下図の例では、`Worker` (1) は `message 1` を受信した筈ですが、 **処理中 (プログラムコード) に例外が発生して完了しなかった** と想定しましょう。

:::note alert
処理に失敗した場合は、メッセージを削除してはいけません。
:::

この場合は [可視性タイムアウト] により、メッセージがまた見える様になる事で `Worker` が再度メッセージを受信してくれる為、結果的に **処理のリトライ** が行われる事になります。

![4e81c9f0-7224-e7f1-6796-8b60fd86eca8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/d8bb8fc3-ab2b-b035-378f-e59482091c9c.png)

:::note alert
コード中で try-catch してリトライ、等はすべきではありません。
コード上でのリトライは、そのサーバーがフリーズした場合等に実施が保証されない為です。
:::

## 問題 (多重参照)

ここで一つ [可視性タイムアウト] のよくある注意点があります。

タイムアウト時間が、想定する処理にかかる時間 (※最大時間) より小さい場合、**まだ処理が終わっていないのに、別の `Worker` によるリトライが発生** してしまいます！

![4175a879-92c5-86c1-e691-a2f29506b9a1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/b71e1789-dfde-7618-cb2d-84ff44047924.png)

## タイムアウトの延長 (Heartbeat)

一般的に [Amazon SQS] では、アプリケーションの非同期処理の最大時間を計測し、それを超える [可視性タイムアウト] を設定する運用が望ましいです。

ただし、処理時間がどの位になるか想定できないケースも多々あるかと思います。
その場合は、受信したメッセージの [可視性タイムアウト] を延長 (※正確にはリセット) する方法もあります。

ハートビートという機能を使うと、まだ処理中のメッセージの [可視性タイムアウト] を延長できます。
[Shoryuken] 等では [auto_visibility_timeout] という Worker option もあります。

![e42a95a3-54e3-b1ac-8c22-577f244e568e.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/f57a6a40-13d8-aaf7-3ada-b1fdcf2d0a2a.png)