Tech Racho エンジニアの「?」を「!」に。
  • 開発

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上でマルチテナントのジョブキューシステムを独自構築する(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。