Shred IT!!!!

IT全般について試したこと・勉強したことを綴ったり、趣味について語るブログ

Rails 非同期で処理を実行する方法(Sidekiq, Resque, Delayed Job, Active Job比較)

概要


Rails で WEB 画面からのキックでジョブをバックグラウンドで実行するときどうするか。
例えば、メール送信・画像変換・CSVアップロードによる大量SQL実行など。

そんなときはバックグラウンドで非同期にジョブを実行してくれる便利な gem がある。

ruby-rails.hatenadiary.com
このサイトで丁寧に説明してくれてます。

代表的だと言われている下記3つについて、それぞれ実装して使い心地を比較する。

それと上記サイトで書かれている Active Job これの使い勝手も試してみる。

それぞれの gem について、もっと詳しく知りたい方は下記参照。
メリット・デメリットがそれぞれまとめられているし、ジョブを実行する際の違いも書かれてます。

qa.atmarkit.co.jp

stackoverflow.com

準備


Rails を 4.2.1 へ

勉強用の Rails 環境は 4.1.8 なので、 Active Job が導入されたという 4.2.1 (現時点で最新)へアップデートする。

Gemfile を必要に応じて書き換え、下記を実行。

# bundle update
...
Installing rails 4.2.1 (was 4.1.8)
...

redis をインストール・起動

Sidekiq, Resque はキューの管理に redis を使うので、インストールして起動しておく。

# brew install redis
# redis-server

gem をインストール

Gemfile に下記を追加。

gem 'resque'
gem 'sidekiq'
gem 'delayed_job_active_record'

ActiveRecord でなくて、Mongoid を利用している場合は gem 'delayed_job_mongoid'をインストール。

# bundle install

実装

Sidekiq 編


準備

Sidekiq 向けに Redis 接続先の設定をする。

# vim config/initializers/sidekiq.rb でファイル作成。

Sidekiq.configure_server do |config|
    config.redis = { url: 'redis://localhost:6379', namespace: 'sidekiq' }
end
Sidekiq.configure_client do |config|
    config.redis = { url: 'redis://localhost:6379', namespace: 'sidekiq' }
end

# 環境が同居している場合は下記のように設定する
# 設定しないと環境の区別がつかないので何の環境で実行されるかわからなくなる
# namespace: "sidekiq_#{Rails.env}" 
# アプリ & 環境名を付けるといいっぽい

次は Sidekiq 起動時の設定。

# vim config/sidekiq.yml でファイル作成。

---
:pidfile: ./tmp/pids/sidekiq.pid
:logfile: ./log/sidekiq.log
Sidekiq 向けの Worker を実装

Worker クラスを定義し、perform メソッドに非同期で処理させたい実装を行う。

# vim app/workers/worker_for_sidekiq.rb

class WorkerForSidekiq
  include Sidekiq::Worker
  sidekiq_options queue: :sidekiq
  def perform(text)
    sleep 5
    p "sidekiq: #{text}"
  end

# ActiveRecord を扱う場合など、引数で全情報を与える必要がある
# def perform(id, text)
#   hoge = Hoge.find(id)
#   hoge.update_attributes({text: text})
# end
end
Sidekiq 起動

Sidekiq を起動する。
画面はそのままにしておく。

# bundle exec sidekiq -q sidekiq

-q オプションでsidekiq_options queue: :sidekiqで定義したキュー名を指定。

キューに追加

コントローラーの実装が面倒なのでコンソールを立ち上げる。

# bundle exec rails c

コンソール上からキューに追加する。

[1] pry(main)> WorkerForSidekiq.perform_async("aiueo")
=> "358c09a3547d3d52dc1901ab"
[2] pry(main)> WorkerForSidekiq.perform_async("kakikukeko")
=> "804d15dadef12e34a5beb1cd"
[3] pry(main)> WorkerForSidekiq.perform_async("sasisuseso")
=> "ab4fda7f312442fe0fddb892"
確認

Sidekiq を立ち上げたコンソール側では、下記が非同期で処理されて表示されることが確認できる。

"sidekiq: aiueo"
"sidekiq: kakikukeko"
"sidekiq: sasisuseso"

# tail log/sidekiq.log にも実行のログが残っていることが確認できる。

これで Sidekiq はおしまい。

参考

Resque 編


Resque 向け Redis 設定

# vim config/initializers/resque.rb 設定ファイルを作る。

Resque.redis = 'localhost:6379'
Resque.redis.namespace = "resque"

# 補足:アプリ & 環境 でネームスペースを決めた方がいいらしい
# Resque.redis.namespace = "resque:app_name:#{Rails.env}"
Resque 向け Worker 起動用タスクを追加

# vim lib/tasks/resque.rake でファイル追加。

require 'resque/tasks'
task 'resque:setup' => :environment
Resque 向けの Worker を実装

Worker クラスを定義し、self.perform メソッドに非同期で処理させたい実装を行う。

class WorkerForResque
  @queue = :resque
  def self.perform(text)
    sleep 5
    p "resque: #{text}"
  end
end

# Sidekiq と同じで必要情報は全て引数で perform メソッドに渡す必要あり
Resque 起動

Resque を起動する。
画面はそのままにしておく。

# QUEUE=resque bundle exec rake resque:work

QUEUE で指定する値は Worker の @queue へセットした値。
QUEUE=* と指定して実行すると全てのキューが実行対象となる。

キューに追加

コンソールを立ち上げる。

# bundle exec rails c

コンソール上からキューに追加。

[1] pry(main)> Resque.enqueue(WorkerForResque, "aiueo")
=> true
[2] pry(main)> Resque.enqueue(WorkerForResque, "kakikukeko")
=> true
[3] pry(main)> Resque.enqueue(WorkerForResque, "sasisuseso")
=> true
確認

Resque 起動のコンソールが下記のように表示されることを確認。

"resque: aiueo"
"resque: kakikukeko"
"resque: sasisuseso"

これで Resque の確認はおしまい。

参考

Delayed Job 編


特徴

Delayed Job では Sidekiq と Resque とは異なる方法でキューにジョブを追加する方法がある。

Delayed Job をインストールすると、Object と Module に delay メソッドが追加される。
Hoge.delay.send_mailhoge.delay.send_mail などのようにすることで、 どんな処理でも非同期で実行できる。

またキューの管理は Redis を使わず、Rails と同じ DB (ORM)を利用する。

準備

キュー管理用のテーブルを追加して、反映する。

# bundle exec rails g delayed_job:active_record
# bundle exec rake db:migrate
Delayed Job 向けの Worker を実装
class WorkerForDelayedJob
  def initialize(text)
    @text = text
  end

  def perform
    sleep 5
    p "delayed_job: #{@text}"
  end
end
# perform メソッドに引数取れないっぽいのでこんな実装になった
Delayed Job 起動

コンソールを立ち上げて、コマンドを打ったらそのままの状態にしておく。

# QUEUE=delayed_job bundle exec rake jobs:work
キューに追加

コンソール起動。

# bundle exec rails c

コンソールからキューを追加。

[1] pry(main)> Delayed::Job.enqueue(WorkerForDelayedJob.new("aiueo"), queue: "delayed_job")
[2] pry(main)> Delayed::Job.enqueue(WorkerForDelayedJob.new("kakikukeko"), queue: "delayed_job")
[3] pry(main)> Delayed::Job.enqueue(WorkerForDelayedJob.new("sasisuseso"), queue: "delayed_job")
[4] pry(main)> job = WorkerForDelayedJob.new("tatututeto")
[5] pry(main)> job.delay(queue: "delayed_job").perform

Delayed Job 特有の delayメソッドでのキュー追加も試してみる。

確認

Delayed Job を起動したコンソールを確認する。
実行ログに混じって、実行結果が下記のように表示されることを確認。

"delayed_job: aiueo"
"delayed_job: kakikukeko"
"delayed_job: sasisuseso"
"delayed_job: tatututeto"

Delayed Job 編おしまい。

参考

RailsでAcitiveJobとDelayedJobを使ってバックグランド処理を行う - Rails Webook

Active Job を使ってみる


概要

Active Job で実装すれば、上記のような非同期処理の gem を意識する必要がなくなる。

対応している gem を確認するには下記ディレクトリを覗く。

https://github.com/rails/rails/tree/master/activejob/lib/active_job/queue_adapters

(美しい Adapter パターン)

Active Job で実装して、queue_adapter の指定をそれぞれ Sidekiq, Resque, Delayed Job にしてみて、 同じように非同期で動作するところまでを確認する。

ジョブを作成する

下記コマンドを実行。

# bundle exec rails g job hoge
      invoke  test_unit
      create    test/jobs/hoge_job_test.rb
      create  app/jobs/hoge_job.rb
ジョブを実装する

# vim app/jobs/hoge_job.rb でファイル開くと、下記の状態。

class HogeJob < ActiveJob::Base
  queue_as :default

  def perform(*args)
    # Do something later
  end
end

それを下記のように実装。

class HogeJob < ActiveJob::Base
  queue_as :default

  def perform(text)
    sleep(5)
    p "ActiveJob: #{text}"
  end
end

Sidekiq アダプターで実行


アダプターの設定をする

# vim config/application.rb でファイルを編集。

module Hoge
  class Application < Rails::Application
  ...
  config.active_job.queue_adapter = :sidekiq
  ...
end
Sidekiq 起動して、Rails コンソールからキュー登録
# bundle exec sidekiq
↑立ち上げっぱなし
↓別コンソールで実行
# bundle exec rails c
[1] pry(main)> HogeJob.perform_later("aiueo")
Enqueued HogeJob (Job ID: b5bfa62b-11f6-4d16-848c-0a1aee98abb8) to Sidekiq(default) with arguments: "aiueo"
=> #<HogeJob:0x007ff5e35427f8 @arguments=["aiueo"], @job_id="b5bfa62b-11f6-4d16-848c-0a1aee98abb8", @queue_name="default">
確認

Sidekiq を起動したコンソールで下記が表示される。

"ActiveJob: aiueo"

Resque アダプターで実行


アダプターの設定をする

# vim config/application.rb でファイルを編集。

  ...
  config.active_job.queue_adapter = :resque
  ...
Resque 起動して、Rails コンソールからキュー登録
# QUEUE=* bundle exec rake resque:work
↑立ち上げっぱなし
↓別コンソールで実行
# bundle exec rails c
[1] pry(main)> HogeJob.perform_later("kakikukeko")
Enqueued HogeJob (Job ID: f205128d-5941-4abd-a579-35b992bd06a2) to Resque(default) with arguments: "kakikukeko"
=> #<HogeJob:0x007fac1a9237b8 @arguments=["kakikukeko"], @job_id="f205128d-5941-4abd-a579-35b992bd06a2", @queue_name="default">
確認

Resque を起動したコンソールで下記が表示される。

"ActiveJob: kakikukeko"

Delayed Job アダプターで実行


アダプターの設定をする

# vim config/application.rb でファイルを編集。

  ...
  config.active_job.queue_adapter = :delayed_job
  ...
Delayed Job 起動して、Rails コンソールからキュー登録
# bundle exec rake jobs:work
↑立ち上げっぱなし
↓別コンソールで実行
# bundle exec rails c
[1] pry(main)> HogeJob.perform_later("sasisuseso")
Enqueued HogeJob (Job ID: 052dc5e2-5f37-4947-a193-3b91783a84e1) to DelayedJob(default) with arguments: "sasisuseso"
   (0.1ms)  begin transaction
  SQL (0.7ms)  INSERT INTO "delayed_jobs" ("queue", "handler", "run_at", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["queue", "default"], ["handler", "--- !ruby/object:ActiveJob::QueueAdapters::DelayedJobAdapter::JobWrapper\njob_data:\n  job_class: HogeJob\n  job_id: 052dc5e2-5f37-4947-a193-3b91783a84e1\n  queue_name: default\n  arguments:\n  - sasisuseso\n"], ["run_at", "2015-06-05 02:04:21.435653"], ["created_at", "2015-06-05 02:04:21.435913"], ["updated_at", "2015-06-05 02:04:21.435913"]]
   (0.6ms)  commit transaction
=> #<HogeJob:0x007f934b23b370 @arguments=["sasisuseso"], @job_id="052dc5e2-5f37-4947-a193-3b91783a84e1", @queue_name="default">
確認

Delayed Job を起動したコンソールで下記が表示される。

"ActiveJob: sasisuseso"

まとめ


Sidekiq, Resque, Delayed Job どれもコーディングするにあたってはほとんど大差ない。
それぞれ Web UI でキューの確認もできるし、ジョブの優先度付けなどもできる。

Delayed Job は既存のDBを利用するし、導入も使い方も一番簡単な印象。
delay メソッド拡張によって、どの Object に対しても、Hoge.delay.fugaで非同期実行にできるのは楽。
小規模サイトで手っ取り早く非同期処理を実現したいなら、Delayed Job がいいと思う。

Redis が使える環境ならば、Sidekiq と Resque も選択肢に入る。
それぞれ下記の特徴があるのでインフラとの兼ね合いや好みか。

  • Sidekiq
  • Resque
    • fork して 1プロセス毎実行
    • メモリリークの心配はないが、fork のコストがかかる

Rails 4.2 以上の環境であれば、とりあえず Active Job で実装しておくのがいいと思う。

アダプターの交換も今回実装したように簡単にできるので、 開発環境は Delayed Job で実装してしまって、 本番環境ではジョブキューを何で処理するか相談して決めれば良い。

正直、どれが優れているとか決められないので、 どんな処理をどれくらいの頻度・量で実行するか洗い出し、 下記サイトを参考にインフラ担当者と話し合うのがいいかと。
ジョブキュー処理のResqueとDelayed Jobの使い分けの方針などはありますか? - QA@IT