Solid Queueにasyncモードを追加し、メモリ使用量を60%削減

rails/solid_queue

Solid Queueに asyncモード が追加され、workerとdispatcherを別プロセスではなく同一プロセス内の異なるスレッドで実行できるようになりました。これにより、1GBの小規模サーバーでも安定した運用が可能になります。

背景

Solid Queueはこれまで forkモード のみをサポートしており、supervisor配下の各worker、dispatcher、schedulerは独立したプロセスとして起動していました。この設計はプロセス間の隔離性とパフォーマンスを提供しますが、メモリ制約のある環境では課題がありました。

PR本文では、1GBのVPS環境でKamalを使用したデプロイ時の問題が報告されています。Solid Queueが1プロセスあたり約100MBを消費し、複数プロセスの起動により総メモリ使用量が700MBに達していました。この状態では、2回目のデプロイ時にメモリ不足が発生し、デプロイが失敗する事態が生じていました。

#343 で同様の問題が議論されており、メモリ制約のある環境での運用が困難である点が指摘されていました。本PRはこの課題に対する解決策として、以前に提案された asyncモードの実装案 を再実装したものです。

技術的な変更

Supervisorの分離

Supervisor クラスが ForkSupervisorAsyncSupervisor に分割されました。モード選択は Configuration クラスで行われ、起動時に適切なクラスが生成されます。

def start(**options)
  configuration = Configuration.new(**options)

  if configuration.valid?
    klass = configuration.mode.fork? ? ForkSupervisor : AsyncSupervisor
    klass.new(configuration).tap(&:start)
  else
    abort configuration.errors.full_messages.join("\n") + "\nExiting..."
  end
end

ForkSupervisor は従来のfork処理を担当し、AsyncSupervisor はスレッドベースの管理を担当します。この分離により、既存のforkモードの動作を変更せずに新しいasyncモードを追加できています。

AsyncSupervisorの実装

AsyncSupervisor は、supervisorと同一プロセス内でworkerとdispatcherをスレッドとして起動します。

private
  def supervise
    if standalone? then super
    else
      @thread = create_thread { super }
    end
  end

  def check_and_replace_terminated_processes
    terminated_threads = process_instances.select { |thread_id, instance| !instance.alive? }
    terminated_threads.each { |thread_id, instance| replace_thread(thread_id, instance) }
  end

standalone? メソッドにより、Solid Queue単体での起動とPumaプラグインとしての起動を区別しています。単体起動時は従来通りの動作を維持し、プラグインとして動作する場合はスレッドを生成してsupervise処理を行います。

プロセス監視もスレッドに対応しており、check_and_replace_terminated_processes では alive? メソッドでスレッドの生存確認を行い、終了したスレッドを replace_thread で再起動します。

Runnableの拡張

Processes::Runnable モジュールに run_in_mode メソッドが追加され、実行モードに応じた起動方法の切り替えが実装されました。

def start
  run_in_mode do
    boot
    run
  end
end

private
  def run_in_mode(&block)
    case
    when running_as_fork?
      fork(&block)
    when running_async?
      @thread = create_thread(&block)
      @thread.object_id
    else
      block.call
    end
  end

この実装により、worker、dispatcher、schedulerの各クラスは起動モードを意識せず、start メソッドを呼び出すだけで適切な方法で起動されます。asyncモードでは create_thread がスレッド作成と例外処理を担当し、スレッドオブジェクトIDが返されます。

CLIとPumaプラグインの対応

CLIに --mode オプションが追加されました。

class_option :mode, type: :string, default: "fork", enum: %w[ fork async ],
  desc: "Whether to fork processes for workers and dispatchers (fork) or to run these in the same process as the supervisor (async) (default: fork).",
  banner: "SOLID_QUEUE_SUPERVISOR_MODE"

Pumaプラグインでは、puma.rbsolid_queue_mode メソッドを使用してモードを指定できます。

module Puma
  class DSL
    def solid_queue_mode(mode = :fork)
      @options[:solid_queue_mode] = mode.to_sym
    end
  end
end
plugin :solid_queue
solid_queue_mode :async

プラグイン内部では、指定されたモードに応じて start_forked または start_async が呼び出されます。asyncモードでは、Pumaプロセスと同一プロセス内でSolid Queueが動作するため、メモリ使用量が大幅に削減されます。

プロセス数設定の無効化

asyncモードでは、設定ファイルの processes オプションが無視されます。

def workers
  workers_options.flat_map do |worker_options|
    processes = if mode.fork?
      worker_options.fetch(:processes, WORKER_DEFAULTS[:processes])
    else
      1
    end

    processes.times.map { Process.new(:worker, worker_options.with_defaults(WORKER_DEFAULTS)) }
  end
end

この制約により、asyncモードではworkerは常に1つのスレッドとして起動されます。複数プロセスによる並列処理はforkモードの特権であり、asyncモードではスレッドプール内での並列実行のみが可能です。

設計判断

ForkとAsyncの明確な分離

Supervisor を継承した ForkSupervisorAsyncSupervisor という2つの独立したクラスが作成されました。これにより、既存のforkモードの動作を保証しつつ、asyncモードを追加できています。

READMEには「推奨され、デフォルトのモードはforkです。asyncは理由があり、何をしているか理解している場合のみ使用してください」という警告が追加されています。この文言は、forkモードが標準であり、asyncモードは特殊な用途向けであることを明確にしています。

シグナルハンドリングの条件付き登録

シグナルハンドラーの登録が standalone? 条件付きになりました。

included do
  before_boot :register_signal_handlers, if: :standalone?
  after_shutdown :restore_default_signal_handlers, if: :standalone?
end

Pumaプラグインとして動作する場合、Solid Queueは独立したプロセスではないため、独自のシグナルハンドラーを登録する必要がありません。この判断により、Pumaのシグナル処理との競合を回避しています。

終了処理の相違点

asyncモードでは、KILLシグナルによるsupervisor終了時に実行中のジョブを完了できません。テストコードにはこの挙動に関するコメントが含まれています。

# In async mode, killing the supervisor kills all threads too,
# so we can't complete in-flight jobs

この制約は、スレッドがプロセスと生存期間を共有するためです。forkモードでは、各workerが独立したプロセスとして動作するため、supervisor終了後も実行中のジョブを完了できますが、asyncモードではこの保証がありません。

まとめ

本PRは、メモリ制約のある環境でのSolid Queue運用を可能にするasyncモードを追加しました。実際の検証では、1GBサーバーでのメモリ使用量が700MBから270MBへと60%以上削減されています。ForkSupervisorとAsyncSupervisorの分離により、既存のforkモードの動作を変更せず、新しい実行モデルを追加できています。ただし、asyncモードは実行中ジョブの保証が弱いため、用途を理解した上での使用が推奨されています。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「総論→各論→結論」の構成が記事全体で明確に適用されています。リード文、背景、技術詳細、設計判断、まとめの各要素が適切に配置されており、非常に分かりやすい構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きのシンタックスハイライトやGitHubのIssue/PRへのリンク記法が、ガイドラインに沿って正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

専門用語を適切に用い、前提知識を要求するスタイルは、対象読者であるエンジニアに適合しています。冗長な説明がなく、簡潔です。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各段落がトピックセンテンスで始まり、1段落1トピックの原則が守られています。段落の長さも適切で、非常に高い可読性を実現しています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているすべてのコードブロックは、提供されたDiff情報と完全に一致しており、ファイル名も正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「asyncモード」「forkモード」「supervisor」などの技術用語が、PRの文脈および一般的な技術知識に照らして正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

コード変更の意図や影響に関する説明は、Diffの内容と論理的に整合しており、技術的に正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

メモリ削減率(60%以上)や具体的なメモリ使用量(700MB→270MB)など、記事内のすべての主張がPRのDescriptionやDiffで裏付けられており、ハルシネーションは検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#644)、Issue番号(#343)、メモリ使用量などの数値や固有名詞はすべて正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルは、PRの主題である「asyncモードの追加」とその最も重要な成果である「メモリ使用量の大幅削減」を的確に要約しており、PR内容と完全に一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事の内容はすべて提供されたPR情報に基づいており、バージョンサポートやリリース予定といったPR外の知識の捏造は見られませんでした。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「これまで」「以前に提案された」といった時間表現が、PRの文脈に沿って正確に使用されており、事実関係の歪曲はありません。