Sidekiq開発ガイドライン
私たちはSidekiqをバックグラウンドジョブプロセッサとして使っています。これらのガイドは、GitLab.comでうまく動作し、既存のワーカークラスと一貫性のあるジョブを書くためのものです。GitLabの管理については、Sidekiqの設定をご覧ください。
以下のトピックについては、さらに詳細なページがあります:
- アップデート間の互換性
- ジョブの冪等性とジョブの重複排除
- リミテッドキャパシティワーカー: 指定された同時実行回数で継続的に作業を実行します。
- ロギング
-
ワーカーの属性
- ジョブの緊急度:キューイングと実行SLOを指定
- ワークロードを記述するためのリソース境界と 外部依存関係
- フィーチャー分類
- データベースの負荷分散
アプリケーションワーカー
すべての Worker はSidekiq::Worker
の代わりにApplicationWorker
を含むべきです。これはいくつかの便利なメソッドを追加し、ルーティングルールに基づいてキューを自動的に設定します。
再試行
Sidekiqはデフォルトで25回のリトライを行います。25回の再試行とは、最初の試行から約3週間後に最後の再試行が行われることを意味します(前の24回の再試行がすべて失敗したと仮定した場合)。
これは、ジョブがスケジューリングされてから実行されるまでの間にいろいろなことが起こりうることを意味します。従って、ワーカーがスケジュールされた後に状態が変化しても、25回失敗しないようにワーカーを守らなければなりません。例えば、ジョブはスケジュールされたプロジェクトが削除されたときに失敗してはいけません。
の代わりに
def perform(project_id)
project = Project.find(project_id)
# ...
end
To-Do this:
def perform(project_id)
project = Project.find_by_id(project_id)
return unless project
# ...
end
ほとんどのワーカー(特に非定常ワーカー)にとって、デフォルトの 25 リトライで十分です。私たちの古いワーカーの多くは、GitLabアプリケーションのデフォルトであった3リトライを宣言しています。3回のリトライは数分の間に行われるので、ジョブは完全に失敗しやすくなります。
以下のいずれかに当てはまる場合は、リトライ回数を少なくすることができます:
- ワーカーが外部のサービスに連絡しており、配送の保証をしていない場合。例えば Webhook などです。
- Worker は冪等ではないので、複数回実行するとシステムが矛盾した状態になる可能性があります。例えば、システムノートを投稿し、その後アクションを実行するワーカーです。2番目のステップが失敗し、ワーカーが再試行すると、システムノートは再度投稿されます。
- ワーカーは頻繁に実行される cronjob です。例えば、cronジョブが1時間ごとに実行される場合、同じジョブを一度に2つ実行する必要はないので、1時間を超えてリトライする必要はありません。
各 Worker のリトライは、メトリクスでは失敗としてカウントされます。常に9回失敗して10回目に成功するワーカーは、90%のエラー率になります。
Sentry で例外を追跡せずに手動で Worker を再試行したい場合は、Gitlab::SidekiqMiddleware::RetryError
から継承した例外クラスを使用してください。
ServiceUnavailable = Class.new(::Gitlab::SidekiqMiddleware::RetryError)
def perform
...
raise ServiceUnavailable if external_service_unavailable?
end
失敗処理
失敗は通常Sidekiq自身によって処理され、前述の内蔵リトライメカニズムを利用します。Sidekiqがジョブを再スケジュールできるように、例外の発生を許可する必要があります。
ジョブがすべてのリトライを試みて失敗したときにアクションを実行する必要がある場合は、sidekiq_retries_exhausted
メソッドに追加してください。
sidekiq_retries_exhausted do |msg, ex|
project = Project.find(msg['args'].first)
project.perform_a_rollback # handle the permanent failure
end
def perform(project_id)
project = Project.find(project_id)
project.some_action # throws an exception
end
Sidekiqワーカーの延期
Sidekiqワーカーは2つの方法で延期できます、
- 手動:機能フラグを使って特定のワーカーを明示的に遅延させることができます。
-
自動:バッチマイグレーションにおけるスロットリングメカニズムと同様に、データベースのヘルスインジケータを使用して Sidekiq ワーカーを延期します。
自動遅延メカニズムを使用するには、ワーカーは
gitlab_schema
、delay_by (遅延する時間)、tables (autovacuum db インジケータで使用される) をパラメータとしてdefer_on_database_health_signal
を呼び出すことで、オプトインする必要があります。例
module Chaos class SleepWorker # rubocop:disable Scalability/IdempotentWorker include ApplicationWorker data_consistency :always sidekiq_options retry: 3 include ChaosQueue defer_on_database_health_signal :gitlab_main, [:users], 1.minute def perform(duration_s) Gitlab::Chaos.sleep(duration_s) end end end
例: 遅延されたジョブのログには、ソースを示すために以下のコンテナが含まれます:
-
job_status
:deferred
-
job_deferred_by
: ‘feature_flag’ または ‘database_health_check’ を指定します。
Sidekiqキュー
以前は、それぞれのワーカーには独自のキューがあり、それはワーカーのクラス名に基づいて自動的に設定されていました。ProcessSomethingWorker
という名前のワーカーの場合、キュー名はprocess_something
となります。キュールーティングルールを使って、ワーカーを特定のキューにルーティングできるようになりました。GDK では、新しいワーカーはdefault
という名前のキューにルーティングされます。
ワーカーがどのキューを使用しているかわからない場合は、SomeWorker.queue
を使って見つけることができます。sidekiq_options queue: :some_queue
を使って手動でキュー名を上書きする理由は、ほとんどありません。
新しいワーカーを追加した後、app/workers/all_queues.yml
またはee/app/workers/all_queues.yml
を再生成するためにbin/rake
gitlab:sidekiq:all_queues_yml:generate
を実行してください。ルーティングルールを使用しないインストールでは、sidekiq-cluster
でピックアップできるようにします。潜在的な変更の詳細については、エピック 596を参照してください。
さらに、config/sidekiq_queues.yml
を再生成するためにbin/rake gitlab:sidekiq:sidekiq_queues_yml:generate
を実行してください。
キューの名前空間
異なるワーカーがキューを共有することはできませんが、キュー名前空間を共有することはできます。
ワーカーにキュー名前空間を定義することで、すべてのキュー名を明示的に列挙しなくても、その名前空間内のすべてのワーカーのジョブを自動的に処理するSidekiqプロセスを起動できるようになります。例えば、sidekiq-cron
によって管理されるすべてのワーカーがcronjob
キュー cronjob
名前空間を使用する場合、cronjob
これらの種類のスケジュールされたジョブ専用の Sidekiq プロセスを立ち上げることが cronjob
できます。cronjob
その cronjob
名前空間をcronjob
使用する新しいワーカーが cronjob
後から追加された場合、Sidekiqプロセスは、設定を変更することなく、そのワーカーのジョブも(再起動後に)ピックアップします。
キューの名前空間は、queue_namespace
DSLクラスのメソッドを使用して設定できます:
class SomeScheduledTaskWorker
include ApplicationWorker
queue_namespace :cronjob
# ...
end
内部では、これはSomeScheduledTaskWorker.queue
をcronjob:some_scheduled_task
に設定します。一般的に使用される名前空間には、worker クラスに簡単に組み込むことができる独自の懸念モジュールがあり、キューの名前空間以外の Sidekiq オプションを設定することができます。CronjobQueue
また、キューの名前空間以外の他の Sidekiq オプションを設定することもできます。
bundle exec sidekiq
は名前空間を意識しており、--queue
(-q
) オプションや、config/sidekiq_queues.yml
の:queues:
セクションで、単純なキュー名の代わりに名前空間が指定された場合、名前空間内のすべてのキュー (厳密には、名前空間名を先頭に持つすべてのキュー) を待ち受けます。
既存の名前空間にワーカーを追加する場合、名前空間を扱う Sidekiq プロセスが利用できるリソースが適切に調整されないと、余分なジョブが既にそこにあったワーカーからのジョブからリソースを奪ってしまうため、注意して行う必要があります。
バージョニング
バージョンは各Sidekiqワーカークラスで指定できます。これはジョブが作成されるときに一緒に送信されます。
class FooWorker
include ApplicationWorker
version 2
def perform(*args)
if job_version == 2
foo = args.first['foo']
else
foo = args.first
end
end
end
このスキーマの下では、どのワーカーも、そのワーカーの古いバージョンによってエンキューされたジョブを処理できることが期待されます。つまり、ワーカーが受け取る引数を変更するときは、version
をインクリメントしなければなりません(ワーカーの引数を初めて変更する場合はversion 1
をセットしなければなりません)。Worker のperform
メソッドから、特にジョブのバージョンでブランチしたい場合はself.job_version
を、提供された引数の数や種類を読むことができます。
ジョブサイズ
GitLabはSidekiqジョブとその引数をRedisに保存します。過剰なメモリ使用を避けるため、Sidekiqジョブの引数の元のサイズが100 KBを超える場合は圧縮します。
圧縮後もサイズが5MBを超える場合、ジョブのスケジューリング時にExceedLimitError
エラーが発生します。
このような場合は、Sidekiqでデータを利用可能にする他の手段に頼ってください。次のような回避策が考えられます:
- Sidekiqのデータをデータベースや他の場所から読み込んだデータで再構築します。
- ジョブをスケジューリングする前にオブジェクトストレージにデータを格納し、ジョブ内でデータを取り出します。
ジョブの重み
いくつかのジョブには重みが宣言されています。これはSidekiqをデフォルトの実行モードで実行する場合にのみ使用されます。sidekiq-cluster
を使用してもウェイトは考慮されません。
Free でsidekiq-cluster
を使用する方向に移行しているため、新しく追加されたワーカーにウェイトを指定する必要はありません。デフォルトの重みは 1 です。
テスト
各 Sidekiq ワーカーは、他のクラスと同様に RSpec を使用してテストする必要があります。これらのテストはspec/workers
に記述します。
Sidekiq RedisおよびAPIとの対話
アプリケーションは、Sidekiq.redis
およびSidekiqAPIとの相互作用を最小限に抑える必要があります。一般的なアプリケーションロジックにおけるこのような相互作用は、チーム間で再利用できるようにSidekiqミドルウェアに抽象化する必要があります。アプリケーションロジックをSidekiqデータストアから切り離すことで、GitLabバックグラウンド処理のセットアップを水平にスケールする際の自由度が高まります。
このルールの例外は、マイグレーション関連のロジックや管理オペレーションです。