ハクソク

世界を動かす技術を、日本語で。

RedisからSolidQueueに移行します

概要

  • Rails 8ではRedisが標準スタックから除外され、SolidQueueなどの新機能がDBベースで動作
  • SolidQueueはPostgreSQLなどのRDBMSを利用してジョブキューを管理し、Redis不要に
  • Redis導入・運用のコストや複雑さを削減し、Railsアプリのシンプル化が可能
  • SolidQueueは高機能(定期実行・同時実行制限・UI)を標準装備
  • Redisが必要なケースもあるが、大半のアプリはSolidQueueで十分対応可能

Rails 8でRedisが不要になった理由

  • Rails 8では、**SolidQueue(ジョブキュー)・SolidCache(キャッシュ)・SolidCable(ActionCableメッセージ)**が標準搭載
  • これらは既存のRDBMS(PostgreSQL/SQLite/MySQL)上で動作し、Redisの役割を完全に代替
  • ほとんどのRailsアプリでRedisを削除可能、運用コスト・複雑さの低減

Redis運用の隠れたコスト

  • Redisサーバのデプロイ・バージョン管理・パッチ適用・監視の必要性
  • 永続化戦略(RDB/AOF/両方)・メモリ制限・エビクションポリシーの設定
  • ネットワーク接続・ファイアウォール・クライアント認証・HA構成の維持
  • Sidekiqプロセスのオーケストレーション異なるデータストア(RDBMS/Redis)間のデバッグの煩雑さ
  • バックアップ戦略の二重管理も必要

SolidQueueの仕組み

  • PostgreSQL 9.5以降のFOR UPDATE SKIP LOCKEDを活用して、ロック競合せずにジョブを分配
  • ジョブの状態管理用テーブル:
    • solid_queue_jobs(全ジョブのメタデータ記録)
    • solid_queue_scheduled_executions(スケジュール待ちジョブ)
    • solid_queue_ready_executions(即時実行待ちジョブ)
  • ワーカーはreadyテーブルをポーリングし、SKIP LOCKEDで同時にジョブ取得
  • MVCC+autovacuumで大量のinsert/deleteにも耐性

定期実行ジョブ(Recurring Jobs)

  • Sidekiqではsidekiq-cron等が必要だが、SolidQueueはcron風定期実行を標準搭載
  • config/recurring.ymlでスケジュール記述
  • GoodJob由来の決定論的スケジューリングでクラッシュ耐性

ジョブ同時実行数制限(Concurrency Limit)

  • Sidekiq Enterpriseの有償機能だったが、SolidQueueは標準で対応
  • limits_concurrencyで単位ごとの同時実行数・期間を制御
  • solid_queue_semaphores/blocked_executionsテーブルで待機ジョブ管理
  • ジョブ終了時に自動で次のジョブをアンブロック

Mission Controlによる監視・管理

  • Mission Control JobsはSolidQueue専用の無料・OSSダッシュボード
  • リアルタイム状態・失敗ジョブ・リトライ・スケジュール可視化・メトリクスを提供
  • SQLで直接ジョブデータをクエリ可能、外部ツール不要

SidekiqからSolidQueueへの移行手順

  • queue_adapterを:solid_queueに変更
  • SolidQueue gem導入・マイグレーション実行
  • Sidekiq-cron等のスケジュールをrecurring.ymlへ変換
  • Procfileのjobsプロセスをsolid_queue:startに変更
  • Redis/Sidekiq関連gemを削除し、bundle clean

SolidQueueが適さないケース

  • 常時数千ジョブ/秒以上の高負荷
  • 1ms未満の超低レイテンシが必須
  • 複雑なpub/subやレート制限・カウンター用途
  • 例:リアルタイムビッディング、HFT、Shopify級の超大規模

実用的なSolidQueueセットアップ例

  • 新規Rails 8アプリ作成時、SolidQueue/SolidCache/SolidCableは自動設定
  • queue用DB接続をdatabase.ymlで分離推奨
  • Mission Controlの認証設定・マウント
  • Procfileでjobsプロセス追加
  • テストジョブで動作確認
  • 単一DB運用も可能だが、分離接続が推奨

よくある注意点・運用Tips

  • Mission Controlの本番運用時は認証強化必須
  • ポーリング間隔調整でレイテンシ最適化可能
  • ActionCable/Turbo Streams利用時はSolidCable用DB接続も設定
  • サーバ起動時にすべてのプロセスを同時起動

スケーラビリティとパフォーマンス

  • ほとんどのRailsアプリはSolidQueueで十分スケール
  • 例:37signalsはPostgreSQLのみで日2,000万ジョブ処理
  • Redis+Sidekiqに比べ、運用シナリオ・障害パターンが少なくシンプル
  • 99%以上のRailsアプリでSolidQueueが最適解

まとめ

  • Redis/Sidekiqの組み合わせは依然優秀だが、ほとんどのRails 8アプリはSolidQueueで十分
  • インフラのシンプル化と運用負荷の軽減が最大のメリット
  • 新しいRails 8の流儀としてSolidQueue導入を推奨
  • フィードバックや意見も歓迎

Hackerたちの意見

環境をシンプルにできるたびに、いいニュースだと思う。Railsの理想は、Redisに簡単に戻せる方法があればいいなって感じ。シンプルに始めて、SolidQueueを使っていて根本的な問題(たぶんスケーラビリティの問題だと思う)にぶつかったら、簡単にアップグレードできる道筋があるといいよね。だけど、99%のRailsアプリはそんなにトラフィックがないから、2つのシステムを維持するのは余計な複雑さになっちゃうかも。
ここでの問題は、バックグラウンドジョブやタスクプロセッサを生産システムの一部として扱っちゃうことだと思う(例えば、ウェブアプリのリクエストに応答するサーバーみたいに)。Railsはこの区別があまり明確じゃない。タスクプロセッサをpgデータベースでバックアップするのは全然問題ないけど(例えば、river[0])、間接的に指摘したように、生産データベースと同じにすべきじゃない。だからRedisが好まれたんだよね。タスクプロセッサの状態を保存するための軽量データベースだったし。この設定にはまだ大きな利点があると思う。今のところ、SolidQueueはこの分離をしてないみたい。 [0]: https://riverqueue.com/
ここでの主な問題点は、開発者がトランザクションに依存しすぎて、仕事が他のすべての処理と一緒に作られることだと思う。保証がなくなると、最終的な移行が難しくなるかもしれない。たとえそれがプライマリDBとは別のPostgresインスタンスへの移行でもね。
> Railsの理想的な状況は、Redisに戻る簡単な方法があることだね。 それはほぼその通りだよ。Railsはジョブ用に抽象化されたAPI(Active Job)を提供してるからね。もちろん、特定のキュー実装に依存してるアプリもあるけど、一般的には設定を更新するだけで切り替えられるし(もちろん古いキューを処理するのも忘れずに)。
Postgresは世界を飲み込む。
確かに、Postgresは世界を飲み込む。pg_kernel拡張が出るのを待ってるから、やっとLinuxをアンインストールできるかも :)
RDMSが世界を飲み込む。結局、機能セットの問題だね。
今はPGQMとPG_CRONを使ってるけど、もう戻れないね。MySQL + Redis + AWSのelasti-cron(とかなんとか)は、Postgresに比べたらゴミみたいなもんだった。
でもMySQLの方が扱いやすいかな。Postgres派として言うけど、MySQLはメンテナンスが少なくてパフォーマンスもいいよ。
せめて数年後には、人々が「すべてにPostgres」っていう流行が「すべてにMongoDB」とか「すべてにRedis」っていうのと同じくらい悪いアイデアだって気づくんじゃないかな。
スケールしないと思ってる人へ。Elixirの似たような実装はObanだよ。彼らのベンチマークでは、単一ノードで毎分100万ジョブを処理できるって。もっと最適化すればさらに増やせると思う。99.99999%のアプリは毎分100万件のバックグラウンドジョブには達してないだろうね。 https://oban.pro/articles/one-million-jobs-a-minute-with-oba...
このベンチマークは、アプリケーションがタスクキューを使う方法からかなりかけ離れてると思う。見出しは「1分間に100万ジョブ」って書いてあるけど、これは本当だよ。ただし… - これは5000ジョブのバッチをキューに入れることで達成されてるから、キュー側では実際には100万TPSじゃなくて200TPSなんだ。バックグラウンドジョブの作成でそんなにバッチ処理を見たことがない。 - ディスパッチも数百TPS(5ms…2ms)にバッチ処理されてる。 - 確認応答もバッチ処理されてる。だから、17kジョブ/秒に到達するために期待される~50-100k TPSではなく、SQL側ではおそらく数百トランザクション/秒しか処理してないと思う。それに、すべてをバッチ処理しないと(ジョブの提出、確認応答;ディスパッチは妥当)、スループットはそのレベルに落ちる可能性が高くて、期待にもっと合致すると思う。このベンチマークは、むしろ「for i in range(5000)」のループを1分以内に200回呼び出すことに近いと思うけど、ほとんどのDB(SQLiteでも)なら処理できると思うよ。
Obanの話が出るなんて面白いね。うちでも使ってるよ。Obanが最初に言うのは、Redisを通知用に使うか、ジョブのポーリングをするか、通知しないかってことだね。 https://hexdocs.pm/oban/scaling.html
無料ソフトウェアの作者は、自分のプロジェクトの範囲を完全にコントロールする権利があるのは当然だよね。でも、good_job(https://github.com/bensheldon/good_job)から移行したのは残念だ。理由は、BasecampがMySQLを使っていて、RDMSエンジン特有のクエリを受け入れない方針だから。GitHubのイシューを見れば、彼らが「ユニバーサル」SQLにこだわっていて、主にMySQLでのパフォーマンスを気にしているのがわかるよね(https://github.com/rails/solid_queue/issues/567#issuecomment... , https://github.com/rails/solid_queue/issues/508#issuecomment...)。バッチジョブのサポートもまだないしね: https://github.com/rails/solid_queue/pull/142 。
それは最悪の状況だね!$WORKではMySQLを使ってるけど、エンジン特有のクエリがなかったらどうするか全く想像できないよ。特に複雑なJOINでは、MySQLがクエリプランをめちゃくちゃに間違えることがあるし、今は大丈夫でも将来的にそうならない保証はないからね。だから、多くの重要なクエリではテーブルを意図した順番に並べて、STRAIGHT_JOINを追加して将来に備えてるよ。クエリプランナーの複雑さを避けるためにね。
同意だね。good_jobはPGバックのキューに理想的なアプローチだよ。
> 彼らの方針は、RDBMSエンジン特有のクエリを受け入れないことです。 なんで?将来的に切り替えられるようにするため?
MySQLにがっちり依存してるなら、「MySQLショップ」って呼ばれるのも納得だよね。MySQL特有の機能を使うのは理にかなってると思うけど、何か見落としてるのかな。
GoodJobをSolidQueueより推奨する理由について、もう少し具体的に教えてもらえる?俺は(急いではないけど)それぞれResqueからの移行を考えてるんだ。GoodJobの主なブロッカーは、pgbounderのトランザクションモードと互換性がない形で特定のpg固有の機能を使ってることなんだよね。つまり、永続セッションが必要ってこと。これが面倒で、上位のパフォーマンス向上を得るためにやってるけど、俺やほとんどのスケールには関係ないと思う。それ以外は、GoodJobの開発モデルが好きだし、メンテナーの判断を信頼してるし、コードも読みやすいと思うけど…でも、これは大きな問題だな。
DBバックのキューが確実に失敗するユースケースは、大きなペイロードの時だね。例えば、大きなJSONペイロードをワーカーにキューイングして処理させようとすると、DBへの書き込みオーバーヘッドが原因でバックグラウンドワーカーが無駄になっちゃう。Redis(Sidekiq)、Postgres(GoodJob使用)、SQLite(SolidQueue)をベンチマークしたけど、上記のユースケースではRedisが一番だった。SQLiteでバックアップされたSolidQueueは、プライマリキーを渡すだけの時にはいいかもしれないけど、同じデータベースからたくさんのワーカーがポーリングして、ジョブのステータスでキューを更新できるかは疑問だな。過去にSQLiteを使って似たようなことをやったけど、10人くらいのワーカーでも壁にぶつかることが多かった。
私の経験では、ジョブのパラメータは1つ、もしくは2つのIDにするのがいいと思うんだけど、実際にそれが違う例ってある?
大きなキューのペイロードをRedisに保存するのは、普通は悪いプラクティスだよ。Redisのメモリは有限だからね。
面白いね、自己完結型のミニマリスティックなセットアップとしては。ジョブ終了後にエフェメラルな設定やクリーンアップトリガーを使ったS3/ガレージのようなストレージシステムを使うべきじゃない?すべてを一つのシステムでやるのは魅力的だけど、他の部分のためにストレージシステムが必要になるんじゃない?ベンチマークやカットオフ(ペイロードサイズ / スループット / レイテンシ)についてどこかにまとめた?
> Redisは上記のユースケースにおいて他のすべてを凌駕する。Redisが耐久性のために設定されると、PostgreSQLよりも遅くなるってAntirezのブログ記事を思い出す。http://oldblog.antirez.com/post/redis-persistence-demystifie...
参考までに、Sidekiqのドキュメントでは、ジョブのためにプライマリーキーや識別子だけを渡すことを強く推奨してるよ。
> ジョブのレイテンシが1ms未満なのは、ビジネスにとって重要だよ。これはリアルタイム入札や高頻度取引(HFT)、その他同様のアプリケーションにとって実際に切実な問題だね。TFAからの引用だけど、HFTにRailsを使ってる人って本当にいるの?
トレーディングエンジンは絶対にRailsでは動かないけど、取引を監視・制御するためのウェブUIは動くかもしれないね。
もちろん、そうじゃないよ。今読んでるブログの会社も似たようなことはやってないしね。ちょっと面白いと思う。
SolidQueueが登場するずっと前から、ジョブをDBに保存してたよ。大きな利点の一つは、システムの状態(または特定の顧客アカウント)を開発環境にスナップショットできて、実際のプロダクションと同じように見ることができることだね。でも、レートリミッターはRedisにまだ置いてるよ。もしすべての不正リクエストが処理される前にDBに往復しなきゃならないと、スキャナーがDBをオーバーロードするのは簡単だからね。Redisには一時的なデータしか保存しないから、バックアップも必要ないし。
まさに https://www.amazingcto.com/postgres-for-everything/ に書いてある通りだね。シンプルにしてPostgreSQLを使おう。
最近はエンタープライズのNVMEストレージがめちゃくちゃ速いから、Redisってあんまり関係なくなってきてる気がするんだけど。複雑さを増やすことに対して、実際にどれだけのレイテンシを節約できるんだろう?でも、俺はストレージやバックエンドのエンジニアじゃないから、Redisの使い方がよくわからないのかも。
SolidQueueは素晴らしいよ。Rails 8も最高。モノリスもいいね。大体の時は。
経験をシェアします。サイドプロジェクトでSolidQueueを試してみたんだけど、プロダクションで使うには以下の結論に至りました。 - Sidekiqに問題がなければ、SolidQueueやGoodJobに切り替える理由はないかな。Redisのインフラを取り除きたいなら別だけど、それ以外の大きなメリットはないと思う。 - 新しいプロジェクトには、GoodJobに偏りがちかも。成熟してるし、コミュニティも素晴らしいし、機能も多いから。 - SolidQueueの嫌なところは、しっかりしたUIがないこと。GoodJobやSidekiqと比べると、かなり基本的な感じ。前回試したときは、最初のページが最適化されてないインデックスのせいでフリーズしちゃった。データがある一定の閾値に達したときだけ起こる現象だけど、もしかしたら修正されてるかも。 - Redisの代わりにRDBMSを使う場合は、適切な接続プールを確保する必要があるかもしれないね。データベースの設定によるけど。大したことではないけど、Redisを使ってるときには考えなくてよかった「コスト」が一つ増えるってこと。