Sidekiqのエラーハンドリング
最終更新 2019/07/20 編集者 Scott E. Knight
このことについて言及したくはないのですが、ワーカーはジョブの実行中に例外を起こすことがあります。これは避けることのできない事実です。
Sidekiqはすべてのタイプのエラーを処理するための数多くの機能を持っています。
ベストプラクティス
-
エラー集約サービスを使用する。 Honeybadger、Airbrake、Rollbar、BugSnag、Sentry、Exceptiontrap、Raygunなど。これらはすべて機能セットと価格が似ています。いずれかを選択してください。エラー集約サービスは、ジョブに例外が発生するたびにメールを送信します(Honeybadgerなどのよりスマートなものは、1番目、3番目、および10番目の同一エラーで電子メールを送信するため、1000個のジョブが失敗しても受信ボックスが圧迫されません)。
-
Sidekiqでジョブによって発生した例外をキャッチする。 Sidekiqの組み込み再試行メカニズムは、これらの例外をキャッチし、ジョブを定期的に再試行します。 エラー集約サービスは例外が起きたときにあなたに通知してくれるでしょう。 バグを修正し、修正をデプロイすれば、Sidekiqはジョブを正常に再試行します。
-
25回の再試行(約21日)以内にバグを修正しない場合、Sidekiqは再試行を停止し、ジョブをDeadセットに移動します。 Web UIを使用すれば、バグの修正後、6か月以内であればいつでも手動でジョブを再試行できます。
-
6か月が経過すると、Sidekiqはジョブを破棄します。
エラーハンドラー
Sidekiqのグローバルなエラーハンドラーに登録することで、Sidekiqの内部で起きた例外を通知することができます。
エラー集約サービスのgemをあなたのアプリケーションのGemfileに追加することで、それらのサービスを自動的に統合することができます。
call(exception, context_hash)
に応答するクラスを実装すれば、独自のエラーハンドラーを作成することもできます。
Sidekiq.configure_server do |config|
config.error_handlers << Proc.new {|ex,ctx_hash| MyErrorService.notify(ex, ctx_hash) }
end
注意:上記で紹介したエラーハンドラーは、Sidekiqサーバープロセスのみで有効であることに注意してください。例えば、Railsコンソールからは利用することができません。
バックトレースのロギング
ジョブのバックトレースロギングを有効にすると、ジョブのライフタイム全体にわたってバックトレースが保持されます。
注意:それぞれのジョブのバックトレースは、Redisで1〜4kのメモリを消費する可能性があります。そのため、大量の失敗したジョブはRedisのメモリ使用量を大幅に増やす可能性がある点に注意が必要です。
sidekiq_options backtrace: true
保持するデータ量の上限を超えないために、Redisに保持するバックトレースの行数を指定したり、エラー集約サービスが保存するエラーとバックトレースの量を指定するようにしてください。
sidekiq_options backtrace: 20 # 上から20行だけ残す
自動的なジョブの再試行
Sidekiqは失敗したジョブをエクスポーネンシャル・バックオフを使って再試行します。
その際の式は (retry_count ** 4) + 15 + (rand(30) * (retry_count + 1))
です。(例:15, 16, 31, 96, 271, ... 秒 + ランダムな時間)
この場合、再試行はおよそ21日間に渡って25回行われることになります。その時間内にバグを修正すると、ジョブは再試行され、正常に処理されることになります。
25回の再試行の間にバグが修正されなかった場合、SidekiqはそのジョブをDeadジョブキューに移動します。こうなってしまったジョブを動かすには手動による操作が必要です。
次の行をsidekiq.ymlに追加することで、再試行の最大回数をグローバルに設定できます。
:max_retries: 1
近似的に計算した再試行の待ち時間は下記の表の通りです。
# | 次の再試行までの時間 | トータル待ち時間
-------------------------------------------
1 | 0日 0時間 0分 30秒 | 0日 0時間 0分 30秒
2 | 0日 0時間 0分 46秒 | 0日 0時間 1分 16秒
3 | 0日 0時間 1分 16秒 | 0日 0時間 2分 32秒
4 | 0日 0時間 2分 36秒 | 0日 0時間 5分 8秒
5 | 0日 0時間 5分 46秒 | 0日 0時間 10分 54秒
6 | 0日 0時間 12分 10秒 | 0日 0時間 23分 4秒
7 | 0日 0時間 23分 36秒 | 0日 0時間 46分 40秒
8 | 0日 0時間 42分 16秒 | 0日 1時間 28分 56秒
9 | 0日 1時間 10分 46秒 | 0日 2時間 39分 42秒
10 | 0日 1時間 52分 6秒 | 0日 4時間 31分 48秒
11 | 0日 2時間 49分 40秒 | 0日 7時間 21分 28秒
12 | 0日 4時間 7分 16秒 | 0日 11時間 28分 44秒
13 | 0日 5時間 49分 6秒 | 0日 17時間 17分 50秒
14 | 0日 7時間 59分 46秒 | 1日 1時間 17分 36秒
15 | 0日 10時間 44分 16秒 | 1日 12時間 1分 52秒
16 | 0日 14時間 8分 0秒 | 2日 2時間 9分 52秒
17 | 0日 18時間 16分 46秒 | 2日 20時間 26分 38秒
18 | 0日 23時間 16分 46秒 | 3日 19時間 43分 24秒
19 | 1日 5時間 14分 36秒 | 5日 0時間 58分 0秒
20 | 1日 12時間 17分 16秒 | 6日 13時間 15分 16秒
21 | 1日 20時間 32分 10秒 | 8日 9時間 47分 26秒
22 | 2日 6時間 7分 6秒 | 10日 15時間 54分 32秒
23 | 2日 17時間 10分 16秒 | 13日 9時間 4分 48秒
24 | 3日 5時間 50分 16秒 | 16日 14時間 55分 4秒
25 | 3日 20時間 16分 6秒 | 20日 11時間 11分 10秒
ヒント:上記の表は rand(30)
が常に15を返すという仮定の元に計算されています。
Web UI
SidekiqのWeb UIには、失敗したジョブを見るためのRetriesタブ、Deadタブがあります。
これらのタブを使うことで、それらのジョブを実行、調査、削除することができます。
Dead Set(デッドセット)
Dead Setはすべての再試行に失敗したジョブを保持しています。Sidekiqがそれらのジョブを再試行することはありません、再試行したい場合は、Web UIから手動で操作する必要があります。
Dead Setの大きさはデフォルトでは10,000個または6ヶ月間のどちらかに制限されるため、無限に大きくなることはありません。
再試行が0回以上に設定されているジョブのみがDead Setに移動します。 特定の種類のジョブを何が起こっても1回だけ実行する場合は、`retry: false' を使用してください。
設定
再試行の回数は自分の好きな回数に設定することができます。デフォルトは25回です。
class LessRetryableWorker
include Sidekiq::Worker
sidekiq_options retry: 5 # 5回再試行し、それでも失敗する場合はDeadキューに送られる
def perform(...)
end
end
特定のワーカーのみで再試行機能を無効にできます。
class NonRetryableWorker
include Sidekiq::Worker
sidekiq_options retry: false # 失敗したジョブは破棄されます
def perform(...)
end
end
再試行をせずにDeadキューに送られます。
class NonRetryableWorker
include Sidekiq::Worker
sidekiq_options retry: 0
def perform(...)
end
end
Deadキューに送られることだけを無効化します。
class NoDeathWorker
include Sidekiq::Worker
sidekiq_options retry: 5, dead: false # 再試行を5回行い、それでも失敗する場合は破棄される
def perform(...)
end
end
必要に応じて、再試行の待ち時間を sidekiq_retry_in
でカスタマイズできます。
class WorkerWithCustomRetry
include Sidekiq::Worker
sidekiq_options retry: 5
# 現在の再試行回数と発生した例外が引数として渡されます。ブロックの戻り値はintegerが必須です。
# 戻り値は再試行の待ち時間に使われます。単位は秒です。戻り地がnilの場合はデフォルトの値が使用されます。
sidekiq_retry_in do |count, exception|
case exception
when SpecialException
10 * (count + 1) # 例:10, 20, 30, 40, 50
end
end
def perform(...)
end
end
あらかじめ決められた再試行回数に達したとき、sidekiq_retries_exhausted
が定義されている場合に限り、Sidekiqはこのメソッドを呼び出します。このメソッドは引数としてジョブに渡されたパラメーターを受け取ります。
このメソッドはジョブがDeadキューに移される直前に呼び出されます。
class FailingWorker
include Sidekiq::Worker
sidekiq_retries_exhausted do |msg, ex|
Sidekiq.logger.warn "Failed #{msg['class']} with #{msg['args']}: #{msg['error_message']}"
end
def perform(*args)
raise "or I don't work"
end
end
Deadキューへジョブが送られたことの通知
sidekiq_retries_exhausted
コールバックはワーカークラスで指定することを想定しています。
Sidekiq バージョン5.1から、グローバルなコールバックも用意されています。
# イニシャライザーに入れてください
Sidekiq.configure_server do |config|
config.death_handlers << ->(job, ex) do
puts "Uh oh, #{job['class']} #{job["jid"]} just died with error #{ex.message}."
end
end
上記のコールバックを使えば、何かしらの例外が起きたときにメール送信やSlackへの通知等を行うことができます。
プロセスのクラッシュ
もしSidekiqプロセスでセグメンテーション違反やRuby VMのクラッシュが起きると、そのときに実行されていたすべてのジョブは失われてしまいます。
Sidekiq Proはこのような状況でもジョブを失わず済む方法(reliable queueing)を提供しています。
無駄な投資を避けるために(No More Bike Shedding)
Sidekiqの再試行メカニズムはベストプラクティスの詰め合わせです。
それにも関わらず、多くの人々が、彼ら固有のエッジケースに対応するための様々なオプションを提案し続けています。
この状況は狂気の沙汰です。
既存のSidekiqの再試行メカニズムでうまく動くようにコードを設計するか、JobRetryクラスにあなた自身のロジック用のパッチを当ててください。
再試行メカニズムの機能的な変更を受け入れる予定はありません。もしそうして欲しい場合は、「Sidekiqを既に使っている数千人ものユーザーが、なぜその機能を必要とするのか」についての非常に説得力のあるユースケースを用意してください。