Shoryuken を導入しようとして諦めた話

Rails で非同期処理といえば、 Sidekiq, Resque, DelayedJob あたりが有名かと思います。
DelayedJob は RDS をジョブキューとして利用できる[^1]ため、インフラの準備が不要で比較的ライトに導入できますが、数万件のジョブを登録しようと思うと RDS にかなりの負荷がかかりますし、Insert もそれほど早くないため、ジョブキューとしてはあまり適しているとは言えません。
Redis などのインメモリーデータベースを用いれば、RDS に負荷をかけず、Insert の高速化も見込めますが、万が一 Redis がクラッシュした際には登録されていたキューは全て無くなりますし、そういった障害に対応するためには幾らかの運用コストが発生します。

ところで、 AWS には SQS というキューイングサービスが提供されています。
安価で堅牢な造りになっており、運用コストも低そうだったのでジョブキューとしては最適なように感じましたが、SQS をジョブキューとして利用している非同期処理と言えば [Shoryuken](https://github.com/phstc/shoryuken) くらい[^2]です。

今回、DelayedJob からの移行先として、Shoryuken が使えないか検討したのですが、いくつかの理由により結局諦めた、という話を書いてみます。

[^1]: DelayedJob は RDS 以外のバックエンドも [サポートしています](https://github.com/collectiveidea/delayed_job/wiki/Backends)
[^2]: 他にも[DelayedJobSQS](https://github.com/kumichou/delayed_job_sqs)がありますが、４年ほど開発が止まっているようです。 

# 環境

- Ruby 2.4.1
- Ruby on Rails 5.0.2
- Shoryuken 3.0.6


# [Shoryuken](https://github.com/phstc/shoryuken) とは

SQS を使った Job queue worker。ActiveJob にも対応。
Sidekiq を意識して作られており、スレッドベース。

![しょーりゅーけん！](https://github.com/phstc/shoryuken/raw/master/shoryuken.jpg)
[GitHub Shoryuken](https://github.com/phstc/shoryuken)

# なぜ Shoryuken が良いと思ったのか？

- SQS は安い
    - 100万 requests / month まで無料
    - 1億 requests / month で $39.60 (およそ 4,300円)
- SQS だとインスタンスタイプの変更などの運用が発生しない
- SQS 側でリトライをサポートしている
    - 一定時間内に処理が正常に完了しなかった場合、再度 Queue に入る
    - 一定回数失敗した場合、Dead letter queue に移動される
    - Redis の場合、 Sidekiq の worker プロセスが吹っ飛んだ場合にジョブが失われる恐れがある
        - Redis 本体が死んでもデータは吹っ飛ぶ
        - Sidekiq Pro ($950 / year) ならリカバリ可能らしい
- ~~昇龍拳だから~~

# SQS の仕組み

## キューの種類

### 標準キュー

> Amazon SQS のデフォルトのキューの種類は標準です。標準キューでは、1 秒あたりのトランザクション数はほぼ無制限です。標準キューではメッセージが少なくとも 1 回配信されることが保証されます。ただし (高いスループットを可能にする、徹底して分散化されたアーキテクチャであるために) ときとして、 **メッセージのコピーが乱れた順序で複数配信されることがあります** 。標準キューでは、メッセージが通常は送信されたのと同じ順序で配信されるようにする、ベストエフォート型の順序付けが行われます。
[SQS よくある質問 - 標準キューと FIFO キューの違いは何ですか?](https://aws.amazon.com/jp/sqs/faqs/#fifo-queues)

![標準キュー](https://d0.awsstatic.com/AmazonSQS/sqs-what-is-sqs-standard-queue-diagram.png)

**乱れた順序で複数配信されることがあります。**

つまり、稀に複数の worker が同じ Job を実行してまう可能性があるということ。
また、Queue と言ってるけど、順番は保証しない（ベストエフォートなので一応は考慮してる）ということ。

複数回同じジョブが実行されてしまうのは困るけど、ジョブの処理の中で RDS などに処理の進行状態を保存して、複数回実行されないような実装をすれば回避可能。というか、 [そういう実装が必須](https://github.com/phstc/shoryuken/pull/351) だと思います。

一番の強みはトランザクション数がほぼ無制限なので、複数の worker から同時にアクセスしまくっても全然大丈夫です。

### FIFO キュー

> FIFO キューは、標準キューを補完するものです。このキュータイプの最も重要な特徴は、FIFO (先入れ先出し方式) 配信と 1 回限りの処理です。メッセージが送受信される順序は厳密に保たれます。メッセージは 1 回配信されると、消費者がそれを処理して削除するまでは使用できるままとなり、キューで重複が起きることはありません。FIFO キューはメッセージグループもサポートしているため、1 つのキュー内に複数の順序付けられたメッセージグループが可能です。FIFO キューは、1 秒あたり 300 トランザクション (TPS) に制限されていますが、標準キューの機能をすべて備えます。
[SQS よくある質問 - 標準キューと FIFO キューの違いは何ですか?](https://aws.amazon.com/jp/sqs/faqs/#fifo-queues)

![FIFOキュー](https://d0.awsstatic.com/AmazonSQS/sqs-what-is-sqs-fifo-queue-diagram.png)

キューの順序と１回限りであることを保証する代わりにトランザクション数が 300 TPS に制限されたバージョン。 EC サイトの決済処理とかで使う、みたいな例が書かれていました。

1 秒あたり 300 トランザクション = 1 分あたり 18000 トランザクション。
現在開発中のシステムでは要件として５分で 10000 件の処理を目標にしているジョブだったので、これでは物足りない感じ。

## 可視性タイムアウト (Visibility Timeout)

> コンシューマーがキューからメッセージを受信して処理しても、そのメッセージはキューに残ったままです。Amazon SQS では、メッセージは自動的に削除されません。これは分散システムであるため、コンポーネントがそのメッセージを実際に受信するという保証がないからです (接続が切断されたり、コンポーネントでメッセージの受信に失敗する可能性があります)。そのため、コンシューマーはメッセージを受信して処理した後、キューからメッセージを削除する必要があります。
[SQS 開発者ガイド - 可視性タイムアウト](http://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html)

![可視性タイムアウト](http://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/images/Visibility_Timeout.png)

引用の通りですが、SQS には可視性タイムアウトという設定値があって、Shoryuken の worker が SQS からメッセージを受け取ると、SQS 側はメッセージを一定時間、論理削除します。
worker はジョブが完了すると SQS 側にメッセージの削除要求を投げて、論理削除されていたメッセージを物理削除します。
もし、 worker 側で処理に失敗、または可視性タイムアウトの設定時間を経過しても処理が終わらなかった場合、SQS 側で論理削除を解除して、次のリクエストでメッセージを再配信します。
この仕組みがあるため、 Shoryuken 側でリトライを設定していなくとも、失敗した処理は再度実行されるようになります。
万が一 worker のプロセスが死んでしまっても、 SQS 上でメッセージはちゃんと生きている訳です。

worker の処理時間が長すぎて可視性タイムアウトの時間を過ぎてしまうのは問題なので、 2 分かかる処理なら可視性タイムアウトを４分に設定する、などがセオリーのようです。

なお、Shoryuken には `auto_visibility_timeout` というオプションがあって、これを `true` にしておくと、可視性タイムアウトに達する 5 秒前に可視性タイムアウトの延長をしてくれるようです。

## 遅延キュー

> 遅延キューを使用すると、キューにある新しいメッセージの配信を指定の秒数延期できます。遅延キューを作成した場合、そのキューに送信したすべてのメッセージが遅延期間の間コンシューマーに表示されなくなります。DelaySeconds 属性を 0 ～ 900 (15 分) の任意の値に設定することで、CreateQueue アクションを使用して遅延キューを作成できます。
(中略)
> 遅延キューは、メッセージを一定の時間コンシューマーが使用できなくするため、可視性タイムアウトと似ています。遅延キューと可視性タイムアウトの違いは、遅延キューの場合、メッセージが最初にキューに追加されたときに非表示になるのに対して、可視性タイムアウトの場合、メッセージがキューから取得された後のみ非表示になるという点です。
[SQS 開発者ガイド - 遅延キュー](http://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-delay-queues.html)

![遅延キュー](http://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/images/Delay_Queues.png)

`ActiveJob` ではオプションとして `#set(wait: 1.minute)` などが使用できますが、 Shoryuken でこの実現に用いられるのが遅延キューです。SQS 側で遅延時間に 0 ~ 15 分という制約があるので、 Shoryuken では 15 分以上の遅延実行はサポートされていません。

## デッドレターキュー (Dead letter queue)

> Amazon SQS では、デッドレターキューがサポートされます。デッドレターキューは、正常に処理できないメッセージの送信先として他の (送信元) キューが使用できるキューです。これらのメッセージは、処理が成功しなかった理由を判断するためにデッドレターキューに分離できます。
[SQS 開発者ガイド - デッドレターキュー](http://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html)

これも引用の通りですが、 SQS 側で設定しておけば一定回数失敗したメッセージが別のキューに自動的に移動されるようになります。失敗回数は 1～1000 で設定でき、これで失敗時のリトライ回数の上限が設けられるようです。

おそらく前述の可視性タイムアウトで失敗した回数だと思うのですが、 SQS のドキュメントでは記述が見つからなかったので未確認です。

# 結局使うのは諦めました

SQS の堅牢さには心惹かれるものがあったので、ぜひ利用したかったのですが、いくつかの理由により見送ることにしました。

## Shoryuken での設定値の共有がイマイチ

`config/shoryuken.yml` に AWS の設定などを記述するのですが、どうやらこれは worker が参照するための設定らしく、Rails アプリ側ではこの値を読み込んではくれないようです。なのでジョブの登録時に AWS の設定が反映されておらず、エラーになりました。

[Shoryuken の wiki](https://github.com/phstc/shoryuken/wiki/Configure-the-AWS-Client) によると、AWS クライアントのインスタンス作成処理を Shoryuken に任せずに自分で作るなどすれば良いようですが、せっかく `config/shoryuken.yml` が存在しているのだから、 Shoryuke　側で反映させて欲しいな、と思いました。
なお、[参考先のサイト](http://engineer.crowdworks.jp/entry/2016/02/12/182338) では、Rails の `config/initializers` で `Shoryuken::EnvironmentLoader.load(config_file: "config/shoryuken.yml")` を実行して Rails 側に設定を読み込ませる方法が紹介されていましたが、僕の環境ではキュー名の Prefix が二重に付いてしまったりして、うまく動作してくれませんでした。

また、 `Rails.env` によって設定を変える、といったことが Shoryuke 側ではサポートされていないので、 staging 環境では少し worker の thread 数減らそう、とかそういった設定がいちいち面倒なのも残念でした。

## 100万トランザクションは簡単に突破しそう

100万トランザクションまで無料、という SQS ですが、Queue が空でメッセージが配信されない場合もトランザクションとして計上されているようです（料金に計上されるかは未確認なので、もし違ったら教えてください。）
Shoryuken は 1 プロセスで標準 25 スレッド並列に処理が走るので、ローカルで１時間弱動かしただけでも 40000トランザクションを超えました。

ただし、Shoryuken 側のオプションで `delay` という項目があり、メッセージが空の場合は `delay` で指定した期間はリクエストをストップするので、この値を調整すればなんとかなりそうです。
しかし、あまり長い秒数 `delay` してしまうと Job の即時実行ができないので、要件に合わせた慎重な検討が必要です。

## 個人で開発された gem である

一番大きな点はここです。
現在も頻繁に開発されているのですが、割と作者本人が自分で PR 出して自分で merge という流れが多いようです。
あくまで作者が個人的に開発している gem であり、商用利用を考慮した慎重な開発はされていないような印象を持ちました。

ただ、ローカルで動かしてみただけですが、非常に軽快に動作しており、worker としての核はよくできている gem だと感じました。SQS についても Redis より堅牢なように感じたので、かなり本腰を入れて検討したのですが、ジョブで実行している処理は開発中のアプリの核になる機能なので、ちょっとここはリスクと考え、慎重になりました。

# 最後に

ジョブのキューに SQS を利用する、という観点は非常に良いように思いますし、他に SQS に対応した Job queue worker は存在しなさそうなので、個人的には Shoryuken を応援しています。
今回は商用利用のアプリ用だったので見送りましたが、個人で作成するアプリではぜひ使ってみたいと思います。

# 参考

- [Shoryuken，それと例外検知gemを作った話 - PartyIX](http://h3poteto.hatenablog.com/entry/2016/02/29/001010)
- [shoryukenの諸動作 - Qiita](http://qiita.com/h3_poteto/items/72c7360302c56a5f9434)
- [→↓:arrow_lower_right:+パンチ - クラウドワークス エンジニアブログ](http://engineer.crowdworks.jp/entry/2016/02/12/182338)
- [Shoryukenでつくるバッチ処理基盤 - トレタ開発者ブログ](http://tech.toreta.in/entry/2016/06/09/120610)