RabbitMQはSidekiqと同等以上だと思う: 前編(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

原文が長いため、3本に分割します。中編と後編は今後公開いたします。

RabbitMQはSidekiqと同等以上だと思う: 前編(翻訳)

私はRabbitMQに乗り換えたこともあって、Sidekiqについてああだこうだと書いてきました。本記事では、production運用を1年間行って得た知見と所感を述べたいと思います。

本記事を書いたきっかけは、ある地元のRuby meetupグループで私が行ったスピーチが大きな反響を呼んだことでした。

そもそもSidekiqやRabittMQがなぜ必要なのか

Sidekiqなどのいわゆる「バックグラウンドジョブ」ライブラリは、処理の結果に対して他の処理(副作用)も引き起こす必要がある状況で使われます

例としてユーザー登録フォームを考えてみましょう。ユーザーがサインアップするときは、メールアドレス確認のために確認メールを送信するのが普通ですが、メールそのものはユーザー登録で必須ではなく、確認メールの送信に失敗した場合でもユーザーアカウントを作成するとします。

ここで明確になっていないのは、メール送信が失敗した場合の処理です。直感的にはエラーを表示すればよさそうな気がしますが、メールアドレスが登録済みになってしまうのでユーザーがアカウントを作成できなくなってしまいます。これでは、ユーザー側で何かできるわけでもなく、失敗しても本来影響のないはずの現象によって、何もおかしなことをしていないユーザーにペナルティを食らわせていることになります。この状態で登録作業をやり直しても、同じ現象が発生して失敗してしまいます。

問題解決のため、この作業を「バックグラウンドジョブ」ライブラリに任せます。ユーザー登録の例で言うと、ユーザーのアカウントが作成されてユーザーがログインしたら「メール送信ジョブ」をキューに置き、このジョブが最終的に実行されます。ジョブが失敗した場合はリトライを指定の回数繰り返すか、raiseしたエラーでカスタムロジックを実行します。これにより、サービス側の失敗でユーザーが面倒を押し付けられることがなくなります

しかしそのためにはSidekiqがなくてはならないのでしょうか?いいえ、Ruby標準ライブラリのQueueクラスThreadでも同じことができます(Gist)。

# ThreadとQueueを使ったシンプルなジョブキューの例
class App < Roda
  JOBS = Queue.new

  Thread.new do
    loop do
      begin
        job = JOBS.pop
        job.call
      rescue => e
        puts "ERROR: #{e}"
        JOBS << job
      end
    end
  end

  route do |r|
    r.on 'sign_up' do
      r.post do
        email = r.params['email']
        JOBS << proc do
          Mailer::ConfirmationMailer.deliver(email)
        end
      end
    end
  end
end

ではなぜバックグラウンドワーカーを使うのか?

上の方法にはさまざまな欠点があります。うさぎの穴の奥深くに入り込みすぎないようにしたいので、デバッグのしやすさ(debuggabilityは私の造語です)、永続性スケーリング、そしてフォールトトレーランスを中心にご説明します。

上の方法ではデバッグが難しくなります。bindings to pryでコードを掘り進める以外にキューの内容を調べるうまい方法がありません。(binding to pryを追加するために)サーバーを止めると、キューにたまっていたジョブはすべて失われてしまうので、問題の原因となったジョブを取り出して再現することすらできません。この問題を解決するために、SidekiqではジョブをRedisに保存します。Redisはメモリ上に配置されるキーバリューストアであり、さまざまなデータタイプを保存できます。ジョブはリストにJSONオブジェクトとして保存されます。redis-cliでRedisに接続してキューの内容を調査することも、キューのリストを調査することもできます。

Redisのデフォルトキーの内容

Redisをジョブキューの中心に据えることで、ワーカー数をスケールアップして高負荷に対応することもできます。

Redisとワーカープロセスのフォールトトレーランスについては、懸念点が2つあります。Redisのストアはデフォルトでは揮発性(ストアを再起動するとデータが失われる可能性がある)です。しかしこれは、データ喪失防止に関連するRDB機能とAOF機能を使えば変更可能です(AOF: Append Only Files)。いくつか注意点があります。RDB(デフォルトでオン)をAOFなしで利用している場合、データ喪失の可能性が残されます。また、AOFと併用すると、パフォーマンスの変動を招く可能性があります。

ワーカーが失敗した場合、SidekiqはRubyエラーハンドリングやリトライロジックに関連するほとんどの問題を取り扱えます。しかしジョブ実行中のワーカーがクラッシュした場合やkillされた場合、ジョブは失われます

Sidekiqからのジョブが再起動/クラッシュ時に失われる様子

ジョブの喪失原因になりやすいのは、デプロイ(アプリがその時点でkillされるため: ただしSidekiqはPro版でローリングリスタート機能を提供)と、gemの拡張の失敗によるVMのクラッシュです(この失敗はキャッチできないため)。

エラーハンドリングに関するSidekiqのREADMEより

1000ドル余分に持っているか企業ユーザーであれば、Sidekiq Proライセンスを購入することでジョブの確実な実行といったさまざまなメリットを得られます。個人的には、小規模プロジェクトや個人利用としては価格の上昇曲線が急な気がします(しかも価格上昇は1度ではないので)。そういうわけで別の製品を探してみました。

関連記事

Rails: RedisキャッシュとRackミドルウェアでパフォーマンスを改善(翻訳)

RailsのPostgreSQL上でマルチテナントのジョブキューシステムを独自構築する(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

BPSアドベントカレンダー

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ