✍️

【Active Job】Sidekiq vs Resque vs Delayed Job

2020/11/23に公開

1. Active Jobとは

バックグラウンドで実行するジョブをRailsアプリケーションで動かすための共通インターフェイスです。
例えばメール送信やCSVアップロード等の時間がかかる重い処理はバックグラウンドで実行することが多いです。

Rails 4.2で導入された機能で、非同期処理を実装する際にまず検討する事が多いかと思います。

2. 共通インターフェイスって?

Active Jobが導入される以前のRailsバージョンの場合、非同期処理を実装するGemを使用していました。
代表的なものではDelayed JobSidekiqResqueがあります。

当然それぞれのGemで記述や機能が異なっていましたが、Active Jobはそうした違いを気にせずジョブを扱うことができるインフラ機能です。

Railsガイド Active Jobの目的によると、Active Jobには下記のようなメリットがあります。

  • Delayed JobとResqueなどのように、さまざまなジョブ実行機能のAPIの違いを気にせずにジョブフレームワーク機能やその他のgemを搭載することができる
  • バックエンドでのキューイング作業では、操作方法以外のことを気にせずに済む
  • ジョブ管理フレームワークを切り替える際にジョブを書き直さずに済む

かんたんなJobを実装

  • ruby 2.7.2
  • rails 6.0.3

この環境でとりあえずJobを実装してみます。

$ rails g job sample
      create  app/jobs/sample_job.rb
      create  app/jobs/application_job.rb

2つのファイルがapp/job/配下に生成されます。
application_job.rbはActiveJob::Baseを継承する親クラスで、処理を実装するジョブファイルでは必ずApplicationJobを継承する必要があります。

app/jobs/application_job.rb
class ApplicationJob < ActiveJob::Base
  # Automatically retry jobs that encountered a deadlock
  # retry_on ActiveRecord::Deadlocked

  # Most jobs are safe to ignore if the underlying records are no longer available
  # discard_on ActiveJob::DeserializationError
end

そしてApplicationJobを継承したSampleJobを実装していきます。
Jobが実行される際には、performメソッドが呼ばれるので、このメソッドに処理を記述します。

app/jobs/sample_job.rb
class SampleJob < ApplicationJob
  queue_as :default

  def perform(*args)
    puts '--------------------------------'
    puts '------------  Test  ------------'
    puts '--------------------------------'
  end
end

railsコンソール上で下記のようにJobを実行できます。

# 「キューイングシステムが空いたらジョブを実行する」とキューに登録する
> SampleJob.perform_later

# 明日正午に実行したいジョブをキューに登録する
> SampleJob.set(wait_until: Date.tomorrow.noon).perform_later

# 5秒後に実行するジョブをキューに登録する
> SampleJob.set(wait: 5.second).perform_later

performメソッドには実際には非同期で実行する重い処理が実装されることになります。
かんたんに実装できましたが、一つ問題があります。

Active JobはキューをRailsのメモリ内に保持するため、Railsを再起動するとジョブは失われてしまいます。

Railsガイドには以下のような記述があります。

デフォルトのRailsは非同期キューを実装します。これは、インプロセスのスレッドプールでジョブを実行します。ジョブは非同期に実行されますが、再起動するとすべてのジョブは失われます。
ーー
Rails自身が提供するのは、ジョブをメモリに保持するインプロセスのキューイングシステムだけです。 プロセスがクラッシュしたりコンピュータをリセットしたりすると、デフォルトの非同期バックエンドの振る舞いによって主要なジョブが失われてしまいます。

アプリケーションが停止、または再起動した際に、ジョブが失われないように、Railsで使うべきサードパーティのキューイングライブラリ、 Active Job のバックエンドを決める必要があります。

3. バックエンドはどれがいいのか?

代表的なものは、Delayed JobSidekiqResqueがあります。
プロダクトの規模や要件に応じて技術選定することになると思いますが、かんたんに比較してみます。

Worker Queuing プロセス/スレッド 特徴
Sidekiq Redis マルチスレッド ・大量のJob処理に向いている。
・メモリあたりのパフォーマンスが良い。
Resque Redis シングルスレッド ・メモリを消費するが、肥大化の心配はない。
Delayed Job RDB シングルスレッド ・Redisが不要で既存Railsアプリに導入が楽。
・信頼性が高い処理の実行
shoryuken AWS SQS マルチスレッド ・Redis障害でキューが吹っ飛ぶリスクがない。
http/App Engine 等 Cloud Tasks (GCP) worker次第 ??

3-1. Sidekiq

利点

  • ストレージとしてRedisを使用し、マルチスレッドプロセスで動くので処理速度が速い
  • マルチスレッドであるがゆえに、他の2つと比べて、使用メモリに対するパフォーマンスが良い
  • ダッシュボードがいい感じ

懸念点

  • マルチスレッドなので、スレッドセーフであるように実装しなければならない(下記参照)
  • これもマルチスレッドがゆえに、 メモリが肥大化することがある
  • sidekiqは起動時にソースコードを読み込むため、(workker or jobの)コードを修正した場合に再起動が必要
  • 実際に運用していて、重いジョブが予想外に重なるとプロセスが落ちました。サーバーのスペックを上げるなり、スレッド数を制限する等の対応が必要そうです。

本番環境デプロイ時にcapistranoを使用していれば、同時にsidekiqも再起動してあげたら良さそうです。(ジョブ実行中の考慮は必要かも)


(↓以下は、コアな部分に興味のない人は読み飛ばしてください。)

マルチスレッドとは、ひとつのプロセス内で複数のスレッドが動作していることを指します。

まずプロセスとは実行中のプログラムのことで、ひとつのプロセスには1つのメモリ領域(正確にはOSのメモリ空間)が割り当てられます。

そしてスレッドとはプロセス内で作られる並列動作が可能な処理の単位です。1つのスレッドにつき、1つのCPUコアに命令を出し処理を行います。プロセス内のスレッドは、プロセスに割り当てられたメモリ空間を共有できます。

そのため、並列で実行しても互いに影響を与えない実装、つまりスレッドセーフな実装を行う必要があります。

※Rubyは言語仕様上、Giant VM lock(GVL)という仕組みにより、OSレベルではメモリに対するアクセスが複数並行することはなく、スレッドセーフであることが担保されているようです。(通常Rubyはスレッドセーフを意識しなくていい!)
ただし、I/O待ちでブロックされているときは、GVLが解放されマルチスレッドとなるので、DBのレコード更新等の処理にはsidekiqは向いています。
この辺は詳しくないです。。参考記事


Active Jobのアダプタよりも、純粋にSidekiqを導入したほうが良い場合

Rails 6.0.1以前のActive Jobは、Sidekiqのリトライ機能を完全にサポートしていませんでした。
具体的には純粋なSidekiqで使用できるsidekiq_optionsメソッドがあります。sidekiq_optionsを使うとジョブを登録するキューや、ジョブが失敗したときのリトライ処理の有無が設定できます。

sidekiq_optionsでリトライ等の詳細に設定を行い場合、Active Jobではなく、純粋なSidekiqを使うほうが良いです。

Sidekiq 6.0.1 + Rails 6.0.2の組み合わせ以上のバージョンで、オプションが完全に使用できるようになります。

3-2. Resque

利点

  • ストレージとしてRedisを使用し、シングルスレッド・マルチプロセスで動く
  • delayed jobよりも高速
  • ジョブごとにフォークされてメモリ初期化されるからスッキリ(メモリリーク/肥大化の心配が基本的にない)
  • ダッシュボードがある

懸念点

  • 大量にジョブを処理するとフォークオーバーヘッドが大きく、速度面やメモリ面でやや不利

3-3. Delayed Job

利点

  • ストレージにDBを使用するので、既存のRailsアプリに導入が容易
  • 登録されたキューもActiveRecordと同じように扱えるので、色々とやりやすい
  • キューへの登録が簡単 ※Active Jobを使う場合はインターフェイスは統一されている
  • 処理が並列で実行されないので、Jobの信頼性を担保したい場合は良い

懸念点

  • シングルスレッドプロセスがゆえの速度面
  • 大量のJobには向いていない(どんどん溜まる)
  • DBに依存する

DB以外のバックエンドもサポートしています。

3-4. shoryuken

利点

  • Sidekiqを意識して作られており、マルチスレッドで動く。
  • Redisが吹っ飛んで、ジョブが失われるリスクはない。

懸念点

  • sidekiqと同じ。
  • ローカル環境での検証が手間。

4. Active Jobを使ったほうが良いの?

利点

  • インターフェイスが統一され可読性が上がった (アダプタ間の移行はそんなに発生しなさそう)
  • Gem依存を減らせる (Railsのレールに沿ったほうがバージョンアップ時などに楽?)

懸念点

  • リトライ制御が弱い
  • 複雑な要件にどこまで耐えられるか

5. 結論

小規模なアプリや、スケーラビリティを考慮しない or Jobの信頼性が重要な場合 ----> Delayed Job
パフォーマンス重視や大量のジョブが発生する場合 ----> Sidekiq

Active Jobか純粋なGemを使用するかは、大きなアプリケーションで複雑な仕様の場合、純粋なGemを使うほうが選択肢が広がりそう。
ただし、最新版のRails + Sidekiq の環境であれば、Active Jobも検討できる。

小規模 or 個人開発レベルであれば、Active Jobに沿って実装すれば良いのでは?といった感じ。

参考サイト

やはり原典をあたるのが一番良く、この辺りは情報が充実していました(英語ですが)。
Qiita記事も大変参考になり、ありがとうございました。

Discussion