気づけばRailsがデーターベースでジョブをキューできるようになっていた

に公開

はじめに

こんにちは。株式会社カウンターワークスで主にバックエンド領域の開発をしておりますエンジニアのはたけと申します。

Railsでジョブをキューイングしたい時、みなさんどうしていますか?
多くの方がRedis等のインメモリツールを使ってキューを実現していると思いますが、Rails 8で、ついにRedisなしでもジョブキューを設定できるようになりました!
今回はそのRails 8で追加されたデフォルトジョブキュー Solid Queue について解説します。

Sidekiqはどうやってキューイングシステムを実現しているか

まず比較対象として、Railsで広く使われているSidekiqの仕組みを見ていきます。SidekiqはRedisのList型データ構造を使ってFIFO(先入れ先出し)のキューを実現しています。

基本的な流れは以下の通りです。

  1. LPUSH: Redisのリストの左端(left)にジョブを追加(enqueue)
  2. BRPOP: リストの右端(right)からジョブを取得し、同時に削除(dequeue)

LPUSHで左端に追加し、BRPOPで右端から取得することで、先入れ先出しのキューを実現しています。

BRPOPはブロッキング操作で、ジョブが利用可能になるまで待機します。これにより、ポーリングによるCPU負荷を避けつつ、取得と削除を一度の操作で実行するため、複数のワーカーがあっても同一のジョブを複数回取得することはありません。また、インメモリであるRedisの特性により、高速にジョブを処理できます。

Solid QueueはRDBMSでどうキューイングシステムを実現したか

Solid Queueの実装はシンプルで、ready_executionsテーブルからのジョブ取得は以下のようなSQLクエリで実現されています。

SELECT * FROM solid_queue_ready_executions
ORDER BY priority, job_id
LIMIT 1
FOR UPDATE SKIP LOCKED;

それぞれの役割

  • ORDER BYでソート順を保証してキューの順序を実現
  • LIMIT 1で先頭要素を取得(dequeue操作)
  • FOR UPDATEでロックをかけて排他制御
  • SKIP LOCKEDでロック競合を回避

これら全てを一つのトランザクションでまとめることで、ジョブキューに必要な一貫性を保証しています。

ただし、RedisのBRPOPが一度の操作で取得と削除を完了するのに対し、Solid Queueでは取得後に状態遷移(ready_executionsからclaimed_executionsへの移動)が必要です。具体的には、以下の複数ステップで構成されています。

  1. ready_executionsからジョブをSELECT(FOR UPDATE SKIP LOCKED
  2. claimed_executionsにレコードをINSERT
  3. ready_executionsから該当レコードをDELETE
  4. COMMIT

この複数ステップの処理が、パフォーマンスとデータベース接続への依存度に影響します。

SKIP LOCKEDが並行処理を可能にする

PostgreSQL 9.5、MySQL 8から追加されたこの SKIP LOCKED ですが、
簡単に説明すると ロック中の行をスキップして次の行を取得します。

このオプションがない場合、他のワーカーやプロセスがロック中の行がある場合、待機状態になってしまうロック競合が発生してしまいます。しかし SKIP LOCKED を使うことで、並行して複数のワーカーがそれぞれジョブを取得することができます。

さらに深掘りしたい方はこちらもぜひ読んでみてください!

データベースベースの信頼性とトレードオフ

Solid Queueでは、ジョブの状態をsolid_queue_jobsテーブルでトランザクション管理しています。処理完了までデータがテーブルに保持されるため、ワーカーがクラッシュしてもジョブがロストすることはありません。claimed_executionsから状態を復元して再実行できます。

ただ、データベースだから完璧というわけではなく、データベースとの接続切断時の振る舞いには注意が必要です。前述の通り、ジョブのdequeueは複数ステップで構成されており、途中でデータベース接続が失われると状態遷移が完了できません。この場合、タイムアウト後にジョブが再度ready_executionsに戻り、同じジョブが重複実行される可能性があります。

そのため、どのジョブキューでも推奨される設計ですが、Solid Queueでもジョブの冪等性を保証する設計が重要です。

ジョブの状態をSolid Queueはどう持っているのか

Solid Queueのテーブル設計にはもう一つ興味深い特徴があり、
1つのテーブルにstatusカラムを持たせるのではなく、ready_executionsscheduled_executionsclaimed_executionsfailed_executionsと、状態ごとに専用テーブルを用意しています。

https://github.com/rails/solid_queue/blob/main/lib/generators/solid_queue/install/templates/db/queue_schema.rb

公式ドキュメントによると、この設計はワーカーがポーリングするsolid_queue_ready_executionsテーブルを可能な限り小さく保つことを意図しているそうです!

Solid Queue tries to keep the solid_queue_ready_executions as small as possible; this is by design

引用: How Solid Queue works under the hood - Honeybadger

これによって

  • ワーカーのポーリングクエリが高速化
  • 各状態に最適なインデックスを設定できる
  • statusの指定が不要でクエリがシンプルに
  • 失敗ジョブが溜まっても実行待ちジョブに影響しない

といった恩恵が得られます。

Rails 8.1で追加されたContinuationsでSolid Queueはさらに便利に

Rails 8.1では、Solid QueueにActive Job Continuationsという機能が追加されました。
これは長時間実行されるジョブを途中で中断しても、後で再開できる仕組みで、データベースでこれが実現できる理由は、ジョブの状態を永続化しているからです。
メモリベースのRedisと違い、Solid Queueでは処理途中の状態もテーブルに保存されるため、どこまで処理が進んだかを正確に記録し、失敗箇所から再開できます。

大量のデータ処理や、複数ステップに分かれた複雑なジョブが必要な場合には有効な選択肢となります。

Solid Queueはどんな場合に適しているか?

Solid Queueが適しているのは、以下のような条件を満たすケースです。

  • ジョブの処理量が中規模以下(目安として秒間数十件程度)
  • インフラをシンプルに保ちたい
  • データベースに十分な余裕がある
  • ジョブの冪等性を設計できる

具体的な導入事例として、37signals社が本番環境で1日約560万ジョブを処理しています(平均で秒間約65件程度)(参考)。ただし、これはあくまで一つの事例であり、自社の要件とデータベース性能に基づいて判断する必要があります。

パフォーマンスとインフラのトレードオフ

Redisベースのインメモリツールと比較すると、データベースのスループットは遅く、パフォーマンスの差は明確に発生します。
また、Solid Queueはジョブのポーリングや状態遷移でデータベースへの読み書きが頻繁に発生するため、アプリケーションの通常のデータベース負荷に加えてジョブキューの負荷も考慮する必要があります。ワーカーが常にポーリングを行い、ジョブごとに状態テーブル間の移動が発生するため、データベースのコネクション数やI/O待ちが増加し、アプリケーションの通常のクエリ性能に影響が出る場合があります。

一方で、Solid Queueの最大のメリットはインフラ構成のシンプルさです。別途Redisサーバーを立てる必要がないため、ホスティングコストが削減でき、運用の複雑さも減ります。ローカル開発環境でもRedisのセットアップが不要になるため、開発体験も向上します。

また、ジョブの状態がデータベースに永続化されるため、標準でジョブロストの心配がありません。

なお、Solid QueueとSidekiq OSSの詳細なベンチマーク結果については、弊社エンジニアが検証記事を公開していますので、併せてご参照ください。
https://zenn.dev/counterworks/articles/a899a4f6a621e9

おわりに

今回、Solid Queueについて深掘りしてみて、その設計思想のシンプルさと合理性の高さが面白かったです。
状態ごとにテーブルを分けることでワーカーのポーリング性能を最適化し、データベースの永続性を活かしてジョブの途中再開機能を実現するなど、データベースの特性を上手に活用した設計になっています。

一方で、複数ステップの状態遷移が必要なこと、データベースI/O負荷、冪等性の設計が必要だったりなど、トレードオフもあるため、高負荷な環境や高度な機能が必要な場合はSidekiq Pro/Enterpriseやインメモリツールベースのシステムの方が適しているケースもあります。

Solid Queueは銀の弾丸ではありませんが、多くのアプリケーションでは十分なパフォーマンスが得られる上、インフラをシンプルに保てるメリットは大きいです。自社の要件をしっかり見極めつつ、開発時の選択肢として検討する価値があります。

この記事が、みなさんの技術選定の助けになれば幸いです。


Solid Queue実行に必要な環境:

  • Ruby 3.1以上
  • Rails 7.1以上
  • データベース: MySQL 8+、PostgreSQL 9.5+、またはSQLite
    • MySQL 8+またはPostgreSQL 9.5+の使用を推奨(FOR UPDATE SKIP LOCKEDサポート)

参考:

最後に株式会社カウンターワークスでは要件を満たしつつ開発者体験の向上を重視しているエンジニアを募集しています!

COUNTERWORKS テックブログ

Discussion