Techouse Developers Blog

テックハウス開発者ブログ|マルチプロダクト型スタートアップ|エンジニアによる技術情報を発信|SaaS、求人プラットフォーム、DX推進

Sidekiq と Solid Queue の機能比較 - Kaigi on Rails day2 Sidekiq vs Solid Queue によせて

ogp

Kaigi on Rails 2024 に参加しました

こんにちは、クラウドハウス採用でエンジニアインターンをしている Higashiji です。

10 月の 25・26 日、Ruby on Rails についてのカンファレンス、 Kaigi on Rails 2024 が開催されました。

弊社からは新卒エンジニアの @izumitomo が「デプロイを任されたので、教わった通りにデプロイしたら障害になった件 ~俺のやらかしを越えてゆけ~ 」というセッションで登壇しました。

スライドがアップロードされているので、興味を持っていただけた方はぜひご覧になってください。

私はこれまでカンファレンスに参加したことがなかったのですが、カンファレンス参加費補助制度を使って初めて参加させていただきました。

本記事では、Shinichi Maeshima (@willnet) さんによるセッション Sidekiq vs Solid Queue の簡単なまとめと、
聴講後 Sidekiq と Solid Queue について機能面に着目して調べた内容 を共有させていただきます。

セッションの内容

本セッションの内容は、Rails 向けバックグラウンドワーカーの歴史をおさらいしたのち、
デファクトスタンダートである Sidekiq と、Rails 8 にてデフォルトのバックグラウンドワーカーとして採用される Solid Queue を比較し、選定の基準について考察するというものです。

すでにスライドが SpeakerDeck にアップロードされているため、セッションの詳しい内容についてここでは触れませんが、
Rails の黎明期から現在に至るまでのバックグラウンドワーカー周りの状況が簡潔にまとめられており、Rails を触り始めて日の浅い自分にとってとても興味深い内容でした。

セッション内での結論

「今から rails new するのであればどちらを採用したほうがいいですか?」という問いに対して、セッション内では以下の結論が下されていました。

- ジョブをあまり使わないサービスは SolidQueue
- ジョブをたくさん使うサービスは Sidekiq
- 中間のサービスは Sidekiq 特有の機能の中で便利なものがあるかを探して決める
  - あれば Sidekiq
  - なければ SolidQueue
    - サービスが育った結果 Solid Queue から Sidekiq への移行するのはやればできなくはないはず...

スライド該当箇所

多くのプロダクトはここにおける「中間」に位置するため、最終的には 機能面の比較 が必要になることがわかります。

機能面の違いについて調べる

私の開発しているプロダクトでは Sidekiq の Enterprise 版を利用しているのですが、これまでその機能について能動的に調べたことはありませんでした。

せっかくの機会なので調べてみるとともに、セッションの内容に乗じて Solid Queue と比較する形で Sidekiq の機能について紹介させていただきます。

ソース

本記事の内容は、以下のページの内容に基づきます。(2024 年 10 月 27 日時点)

Sidekiq にあって、Solid Queue にない機能

Batches

batch = Sidekiq::Batch.new # Batch の初期化
batch.on(:success, MyCallback, :to => user.email) # 完了時に発火する処理を登録
batch.jobs do
  rows.each { |row| RowJob.perform_async(row) } # Batch 内のジョブを登録
end

こちらはセッションでも触れられていた機能です。

複数のジョブを 1 つのまとまりとして扱い、全てのジョブが完了したタイミングで所定の処理を発火させることができます。

Wiki では、具体的なユースケースとして、巨大な Excel の処理 を挙げています。
行ごとにジョブを分割して並行処理することで全体の処理時間を抑え、全てのジョブが完了したタイミングでユーザーへの通知などを行います。

Ref: https://github.com/sidekiq/sidekiq/wiki/Batches

Rate Limiting

こちらもセッション中に触れられていた機能です。

Sidekiq では、以下の 4 種類のレート制限を利用できます。

方式 説明
Concurrent 同時に実行できる同種のジョブの実行数を制限
Bucket 時間枠ごとに実行可能なジョブの個数を制限
Window 指定した間隔のうちに、指定した個数以上ジョブが実行されないように制限
Leaky Bucket 基本的には Bucket と同じだが、利用可能枠は一度にリセットされるのではなく徐々に回復していく

Leaky Bucket については今回初めて知ったのですが、Shopify API のレート制限 をはじめとして大規模なアプリケーションで採用されています。

Solid Queue はこのうち Concurrent のみサポート しています。

外部に公開されている API では 1 リクエスト/秒 など、時間枠当たりのリクエスト数を制限しているケースが多い(例: Slack API)ため、Solid Queue を使って外部 API をコールする場合はこの点を考慮する必要がありそうです。

Ref: https://github.com/sidekiq/sidekiq/wiki/Ent-Rate-Limiting

補足: Sidekiq の Rate Limiting と Solid Queue の Concurrency controls の違い

「Solid Queue はこのうち Concurrent のみサポートしています」と書きましたが、両者の制限方式には微妙な違いがあります。

Sidekiq
LIMITER_NAME = Sidekiq::Limiter.concurrent('example', 5, wait_timeout: 5, lock_timeout: 30)

def perform(...)
  LIMITER_NAME.within_limit do
    # 処理
  end
end

Sidekiq では、Limiter.concurrent を使用して、特定の ブロック の同時実行数を制限します。
この例では、 within_limit に渡されたブロックは同時に 5 件までしか実行されません。

Solid Queue
class MyJob < ApplicationJob
  limits_concurrency to: 5, key: ->(arg1, arg2, **) { ... }, duration: 30

  # ...

一方、Solid Queue では、ジョブクラスに limits_concurrency を宣言して、同一 ジョブ の同時実行件数を制限します。

ユースケースの違い

Sidekiq の Rate Limiting はバックグラウンドワーカー以外の箇所でも同時実行を制限できます。これは Concurrency 方式に限らず、すべての方式に共通です。

そのため、外部 API のレスポンスを同期的にユーザーに返却したい場合 や、 API のエンドポイントを顧客に公開していて、顧客ごとにジョブの同時実行数を制限したいとき など、より広いユースケースに対応可能だと言えます。

@willnet 様のアドバイスに基づき、公開当時から情報を追記しました。)

Job Argument Encryption

class PrivateJob
  include Sidekiq::Job
  sidekiq_options encrypt: true # encrypt オプションを設定

  def perform(x, y, secret_bag)
  end
end

SecretJob.perform_async(1, 2, {"ssn" => "123-45-6789"}) # ジョブを実行

# Redis では以下のように見える
# {"class"=>"SecretJob", "args"=>[1, 2, "BAhTOhFTaWRla2lxOjpFbmMIOgdpdiIV...

これもセッション中で触れられていた機能です。
encrypt オプションを有効化すると ジョブの引数を Redis 格納時に自動で暗号化し、アプリケーションで実行する際に復号します。

ジョブの引数は Sidekiq の WebUI に表示されることもあり、個人情報を扱うサービスでは適切に設定する必要がありそうです。

Ref: https://github.com/sidekiq/sidekiq/wiki/Ent-Encryption

Expiring Jobs

class SomeJob
  include Sidekiq::Job
  sidekiq_options expires_in: 1.hour # expires_in オプションを設定
  ...
end

ジョブごとに無効化されるまでの期間を設定する機能です。
Sidekiq プロセスが有効期間を過ぎたジョブを取り出した場合、Expired job と判定され、処理が行われず即座に完了します。

具体的なユースケースとしては以下が挙げられています。

  • 30 分に一度キャッシュを無効化する処理
  • 1 日に一度、その日の出来事をユーザーに通知する処理

これらのジョブは、所定の期間をすぎたら実行する意味がありません。
後者については、実行が遅延するとむしろユーザーを混乱させてしまうため、このオプションを有効化しておくことが望ましいでしょう。

Ref: https://github.com/sidekiq/sidekiq/wiki/Pro-Expiring-Jobs

Iteration

Iteration は執筆時点で最新のマイナーバージョンである v7.3 で ベータ版として追加された 新機能です。

Kaigi on Rails 2024 では、@hypermkt さんのセッション Sidekiq で実現する長時間非同期処理の中断と再開 でも触れられていました。

Iteration では、巨大な繰り返し処理を分割し、実行位置(Cursor)を Redis に保存しておくことで、デプロイを跨いだ中断と再開を実現しています。

Wiki には、50,000 件のレコードを作成する処理を、 1000 レコード × 50 ジョブ に分割して並列実行する例が記載されています。(コメント部分は改変)

class PostCreator
  include Sidekiq::IterableJob

  # **kwargs に含まれる :cursor に実行中の位置を保持しているため、
  # 処理途中での中断と再開が可能
  def build_enumerator(start_at, count, **kwargs)
    @start_at = start_at
    @count = count
    logger.info { "Creating posts for #{start_at}" }
    # ヘルパーメソッドを用いて Array から Enumerator を作成する
    array_enumerator((start_at...(start_at + count)).to_a, **kwargs)
  end

  # 各要素についての処理
  def each_iteration(pid, *_unused_args)
    Post.create!(id: pid, title: "Post #{pid}", body: "Body of post #{pid}")
  end

  # 1000 件の生成が完了したタイミングで発火するコールバック
  # 50,000 件全てが完了したタイミングで何らかの処理を行いたい場合は Batch 機能を利用する
  def on_complete
    logger.info { "#{@start_at} complete, updating..." }
    PostUpdater.perform_async(@start_at, @count)
  end
end

# 実行
50.times { |idx| PostCreator.perform_async(idx*1000, 1000) }

Ref: https://github.com/sidekiq/sidekiq/wiki/Iteration

補足: Shopify / job-iteration について

Sidekiq の Iteration は Shopify / job-iteration およびそれに影響を受けた fatkodima / sidekiq-iteration を元に実装されています。

job-iteration は Solid Queue のための interruption adapter を実装しており、
これを用いることで キューイングバックエンドに Solid Queue を利用している場合でも Sidekiq の Iteration と同様の機能を実現することができます。

class TestJob < ApplicationJob
  include JobIteration::Iteration

  self.queue_adapter = :solid_queue

  def build_enumerator(cursor:)
    enumerator_builder.array(['test1', 'test2', 'test3', 'test4', 'test5'], cursor: cursor)
  end

  def each_iteration(array_element)
    puts "Started: #{array_element}"
    sleep 5
    puts "Finished: #{array_element}"
  end
end

こちらが Solid Queue で job-iteration を用いる例です。
インターフェースは Sidekiq 版とほとんど変わりません。

なお、job-iteration の機能を使うためには、Solid Queue のバージョンが 0.7.1 以上である必要があります。

(こちらも @willnet 様のアドバイスに基づき、公開当時から情報を追記しました。)

Sidekiq の無償版になくて、Solid Queue にある機能

ここからは少し趣向を変えて、Sidekiq では有償版を購入しないと使えないものの、Solid Queue では使える機能について紹介します。

Periodic Jobs

# initializer に以下を記載
Sidekiq.configure_server do |config|
  config.periodic do |mgr|
    # crontab 記法
    mgr.register('0 * * * *', "SomeHourlyWorkerClass")
  end
end

Cron の要領で、特定のタイミングでジョブが自動実行されるようにする機能です。
Sidekiq では Enterprise 版限定の機能になっていますが、Solid Queue では Recurring tasks として実装済みです。

# config/schedule.yml
production:
  a_hourly_job:
    class: SomeHourlyWorkerClass
    schedule: every hour

Solid Queue で同様の機能を実現する場合、定期実行のための設定ファイル (default: config/schedule.yml ) にこのように記載します。

記載箇所が専用の YAML ファイルである点に違いがあるものの、同じく特定のジョブを定期的に実行できます。

Ref (Sidekiq): https://github.com/sidekiq/sidekiq/wiki/Ent-Periodic-Jobs
Ref (Solid Queue): https://github.com/rails/solid_queue?tab=readme-ov-file#recurring-tasks

Reliability (super_fetch)

デプロイなどに際する Sidekiq のプロセスの終了は、基本的には以下の流れで実行されます。

  • 新規処理の開始を止めつつ、実行中の処理が完了するまで待つ状態 (quiet) に移行する。
  • 所定の時間が経っても処理が終わらない場合は処理を中断してジョブをキューに戻す。

この方法の問題点として、ネットワークの問題などでジョブを Redis に戻すことができない場合、そのままジョブが消失してしまう点が挙げられます。

その対策として Pro, Enterprise 版で用意されているのが super_fetch です。

# config/sidekiq.yml
Sidekiq.configure_server do |config|
  config.super_fetch!
end

super_fetch を有効化すると、処理開始時に Redis から単に pop するのではなく、プライベートキューに移行させる挙動となります。そのため、プロセスが異常終了してもジョブが失われることはありません。

一方、Solid Queue では RDB を使ってジョブを管理しており、デフォルトでは処理完了後もレコードを削除しない設定になっています。

なお、宙に浮いたジョブを再びキューに戻すプロセスは両者に共通しており、プロセスの heartbeat が一定期間確認されなければプロセスが死んだと判断し、処理中のジョブをキューに戻す処理を行います。

Ref (Sidekiq): https://github.com/sidekiq/sidekiq/wiki/Reliability#using-super_fetch
Ref (Solid Queue): https://github.com/rails/solid_queue?tab=readme-ov-file#threads-processes-and-signals

まとめ

調べてみると、Sidekiq 特有の機能は細かいところでかなり多い印象でした。 特に、BatchesIteration のような大規模な処理に適する機能は Sidekiq の方が充実しており、さすが Enterprise の名を冠すだけあるという印象です。

また、記事内では触れられませんでしたが、 メトリクスの可視化StatsD 形式でのデータの送信 など、監視についてのオプションも Sidekiq の方が充実しています。

一方、Solid Queue は機能の充実度こそ Sidekiq に劣るものの、ジョブの定期実行ジョブの消失対策といった、基本的なアプリケーションの要件を満たすための機能は十分に網羅している印象を受けました。

特に、Sidekiq では Enterprise 版でしか使うことができない Periodic Jobs に相当する機能を提供している点が印象的でした。
多くのアプリケーションの機能要件を満たす上で必要になる機能をはじめから提供している点は、Rails のデフォルトとして採用されるだけあると感じます。

Solid Queue は本記事の公開 3 週間前に v1.0.0 が公開されたばかりで、今後より機能が充実していくと思われます。
来る Rails 8 とともに、 Solid Queue の動向にも注目していきたいものです。

最後になりますが、本記事の掲載を承諾し、内容についてのアドバイスをいただいた Shinichi Maeshima (@willnet) 様、
Kaigi on Rails 2024 運営の皆様にこの場を借りて感謝を申し上げます。

Techouse では、社会課題の解決に一緒に取り組むエンジニアを募集しております。 ご応募お待ちしております。

jp.techouse.com