低頻度アクセスサイトのworkerプロセスのコスパが悪いよ問題にSolidQueueが使えるかも

背景

ActiveJob の backend は意外と悩ましい。

ActiveJob は本来 Rails 専用でも ActiveRecord 専用でもないはずだが、いざ adapter を選んでみると Rails + PostgreSQL 前提です、みたいなことが意外にあったりする。また人気のバックエンドだった Redis も近年はクラウドベンダー側と折り合いが悪かったり、意外と扱いやすくない。1 特に Rails 5, 6 が Webpacker をはじめゴテゴテしつつも変化の早い時期で、別にこれくらいなら Rails じゃなくても Hanami でも Sinatra でもいいんじゃないの?と思っていた頃は特に悩ましかった。

2024-02 現在、Rails 7 は十分シンプルになり、素の cold start の遅さがいわゆるサーバレスとイマイチ相性が悪いが、それ以外は概ね Rails でよいかもと思う状況に戻りつつある。

ActiveJobバックエンド選択時の課題

上記のような背景を踏まえ、現時点で自分が感じている ActiveJob 選択時の課題は以下のように整理できる。

  1. Redis 以外の安定したバックエンドが使えないか
    • Resid のホスティングは以前ほどお手軽価格ではなくなっている。特にメガクラウドに寄せようと思うと、中小規模にとってはだいぶコスパが悪い状態
  2. ActiveRecord を使うのであれば PostgreSQL, MySQL など特定の DB にはできれば依存したくない(細かいバージョンの違いなどはあまり気にしたくない)
  3. できれば Worker dyno, Worker コンテナを使わずに済むならその方がありがたい
  4. クラウド独自のツールは development 環境の再現で手間取るので、なんかもっと楽なものはないか

えーと簡単に言うと手間もお金も掛からず ActiveRecord 任せにできるとそれが最高だな、ですね😁

低頻度アクセスのサイトのバックグラウンドジョブの課題

特に上記 3, 4 が課題になってくるのが低頻度アクセスのサイトである。簡単に言うと

めったにジョブを処理しないのにサーバ(コンテナ)2つ分、ずっとリソースをキープしておくのもったいなくない?

という話だ。

基本的に ActiveJob でバックグラウンドジョブを処理する場合、ユーザーからのアクセスを受け付けるプロセスとは別にバックグラウンドジョブ用のプロセス(コンテナ)を用意して、ジョブ用のプロセス(コンテナ)で polling して処理する方法が一般的なのだが、こうなるとめったにアクセスがないのに24時間起きてる人を最低2人用意しなきゃいけない、という話になり、「なんかコスパ悪くない?」という状態になる。2

もちろんジョブの負荷自体がメモリ使用量的にも実行時間的にも軽いものであれば、全部 inline で処理してしまうというのも一つの手ではある。ちゃんと計測してリソースが足りるのであれば問題ないと思う。しかし現実には

  • 頻度は高くないがコストは一定以上大きい

ケースはままあるし、例えば簡単なメール1通送るだけだったはずの仕組みが、関係各所に送ることになりましたとか、コストの嵩むことを往々にしてやりたくなる。そうなると inline や async adapter で処理するのはインフラのリソースの制限上難しくなってくる3

いちばん確実な解決方法は 4 である。Google Cloud で言うところの Cloud Task を利用する4。これは完全にサーバレスで従量課金なので、バックグラウンドジョブが発生しない限り余計なコストは発生しない。

ただ今度は手元で完全に同じ環境を再現するのが難しいという新たな課題が生まれる。

そんなあなたにSolidQueue

SolidQueue は 37 Signals が新たに開発した ActiveRecord を利用した ActiveJob adapter.5

で、なぜ SolidQueue かと言うと SolidQueue には puma plugin があるので、SolidQueue 用に別途プロセス(コンテナ)を用意しなくても puma の worker の一つとして SolidQueue を管理させることができるから。

もちろんメモリ使用量や実行時間がシビアな場合に気にしなければいけないことは変わらないが、少なくとも揮発しないストレージに情報を残しつつ処理できるので、ジョブを細分化するジョブを作って緩和しつつ、万一メモリ不足でプロセスが異常終了するようなことがあってもジョブが失われないという安心感がある。

簡単に試してみた

wtnabe/example-rails7-sqlite3-solid-queue

上のリポジトリは Cloud Run で FUSE を使って SQLite を読み書きするというリポジトリなのでノイズが多いが、キモは

Procfile.dev の

web: mkdir -p storage/sqlite3 && ./bin/rails s

と ( worker なし ) puma の設定の

plugin :solid_queue

だけで6、solid_queue が polling してバックグラウンドジョブを処理できることである。

※ 今回は実験用に SQLite3 で動かしているが、どうも時々エラーが起きたりするので ActiveRecord-backed とは言っているが、基本的には PostgreSQL or MySQL が前提になっているのだと思われる。

メモ

  • 設定が Rails 本体側というか configure 側に断りなしに漏れていて、solid_queue.yml には一部の設定しか書けないのはちゃんとドキュメントにしてほしい
  • 一応 development では SQLite3 でも動くので手元でサクッと動かす分にはよさそう(InlineやThreadだと何らかの外部ストレージを経由する場合と挙動が変わってしまうので)
  • Rails 7 + SQLite 3 + FUSE ( Cloud Storage ) は基本的な機能だけなら動く
  • 同環境だと SolidQueue の動作の何かが FUSE の制限を踏み抜いてしまうのか、必ず SQLite3::BusyException: database is locked (ActiveRecord::StatementInvalid) か SQLite3::CorruptException: database disk image is malformed (ActiveRecord::StatementInvalid) で死ぬので Cloud SQL でないと無理かも
  1. メガクラウドは結局生のサーバを1台使う、みたいな感じになったりしてまったくお手頃価格にならなかったりする。Heroku や Redis 専門の低価格サービスと組み合わせるといった方法はあるが、今度は管理が煩雑になったりする。 

  2. オンプレならリソースさえ足りれば特に問題はないが、そうなると今度はインフラ管理のコストが乗ってくる 

  3. メモリ使用量の制限やWebプロセスの実行時間制限など 

  4. 実際には特定の routing の Web プロセスにジョブを投げる設定の場合、Web プロセスの実行時間制限に引っかかる可能性は残る。 

  5. 実際には恐らく PostgreSQL と MySQL の該当バージョン以降のもの以外は満足に動作検証されていないと思う。 

  6. リポジトリのものはコメントアウトされてしまっているので戻す必要アリ 

More