- アプリケーションワーカー
- 専用キュー
- キュー名前空間
- 無限のジョブ
- ジョブの緊急性
- 外部依存のジョブ
- CPUバウンドとメモリバウンドワーカー
- CPUバウンドジョブの宣言
- ワーカーがCPUバウンドしているかどうかの判断
- 特集カテゴリー
- ジョブ・ウェイト
- 労働者のコンテキスト
- ロギングの引数
- テスト
- Sidekiqの互換性とアップデート
Sidekiqスタイルガイド
このドキュメントでは、Sidekiqワーカーを追加または変更する際に従うべきさまざまなガイドラインの概要を説明します。
アプリケーションワーカー
すべてのワーカーはSidekiq::Worker
の代わりにApplicationWorker
を含めるべきです。これはいくつかの便利なメソッドを追加し、ワーカーの名前に基づいてキューを自動的に設定します。
専用キュー
すべてのワーカーは独自のキューを使うべきです。キューはワーカークラス名に基づいて 自動的に設定されます。ProcessSomethingWorker
という名前のワーカーの場合、キュー名はprocess_something
となります。 ワーカーがどのキューを使っているのかわからない場合は、SomeWorker.queue
を使って見つけることができます。sidekiq_options queue: :some_queue
を使って手動でキュー名を上書きする理由は、ほとんどありません。
新しいキューを追加した後、bin/rake
gitlab:sidekiq:all_queues_yml:generate
を実行して、app/workers/all_queues.yml
またはee/app/workers/all_queues.yml
を再生成し、sidekiq-cluster
でピックアップできるようにします。
キュー名前空間
異なるワーカーがキューを共有することはできませんが、キューの名前空間を共有することはできます。
Workerにキュー名前空間を定義することで、すべてのキュー名を明示的に列挙しなくても、その名前空間内のすべてのWorkerのジョブを自動的に処理するSidekiqプロセスを起動することが可能になります。 例えば、sidekiq-cron
によって管理されるすべてのWorkerがcronjob
キュー名前空間を cronjob
使用している場合cronjob
、これらの種類のスケジュールされたジョブ専用のSidekiqプロセスをスピンアップすることができます。 cronjob
名前空間をcronjob
使用する新しいWorkerが cronjob
後で追加された場合、Sidekiqプロセスは、設定を変更することなく、そのWorkerのジョブも(再起動後に)自動的にピックアップします。
キューの名前空間は、queue_namespace
DSL クラスメソッドを使用して設定することができます:
class SomeScheduledTaskWorker
include ApplicationWorker
queue_namespace :cronjob
# ...
end
裏側では、これはSomeScheduledTaskWorker.queue
をcronjob:some_scheduled_task
に設定します。 一般的に使用される名前空間は、ワーカークラスに簡単に組み込むことができる独自の懸念モジュールを持ち、それはキューの名前空間以外の他の Sidekiq オプションを設定するかもしれません。例えば、CronjobQueue
は名前空間を設定しますが、リトライも無効にします。
bundle exec sidekiq
は名前空間を意識しており、--queue
(-q
) オプション、あるいはconfig/sidekiq_queues.yml
の:queues:
セクションにおいて、単純なキュー名の代わりに名前空間が指定された場合、名前空間内のすべてのキュー (厳密には、名前空間名を先頭に持つすべてのキュー) を自動的に待ち受けます。
既存の名前空間にワーカーを追加する場合、名前空間を処理する Sidekiq プロセスが利用できるリソースが適切に調整されないと、余分なジョブが既にあるワーカーのジョブからリソースを奪ってしまうため、注意して行う必要があることに注意してください。
無限のジョブ
ジョブが複数の理由で失敗することはよく知られています。 例えば、ネットワークの停止やバグなどです。 このアドレスに対応するために、Sidekiqには組み込みのリトライメカニズムがあり、GitLab内のほとんどのワーカーでデフォルトで使用されています。
Sidekiqがジョブをidempotentかつtransactionalにすることを推奨しているのはそのためです。
一般的なルールとして、労働者は次のような場合、idempotentとみなすことができます:
- 同じ引数で複数回実行しても安全です。
- アプリケーションの副作用は一度しか起きないと予想されます(または、2回目の実行の副作用は影響しません)。
その良い例がキャッシュ期限切れワーカーです。
労働者がべき等であることの保証
以下の共有例を用いて、ワーカテストがパスすることを確認してください:
include_examples 'an idempotent worker' do
it 'marks the MR as merged' do
# Using subject inside this block will process the job multiple times
subject
expect(merge_request.state).to eq('merged')
end
end
job.perform
の代わりにperform_multiple
メソッドを直接使用してください (このヘルパーメソッドはワーカー用に自動的に組み込まれます)。
ワーカーが冪等であることの宣言
class IdempotentWorker
include ApplicationWorker
# Declares a worker is idempotent and can
# safely run multiple times.
idempotent!
# ...
end
perform
メソッドが他のクラスやモジュールで定義されていたとしても、idempotent!
呼び出しは一番上のワーカークラスでのみ行うことを推奨します。
重複排除
Idempotent Worker のジョブがエンキューされている間に別の未着手ジョブがキューに入っていた場合、GitLab は 2 番目のジョブを取り下げます。 同じ作業が最初にスケジュールされたジョブによって行われるため、その作業はスキップされます。
たとえば、AuthorizedProjectsWorker
はユーザー ID を受け取ります。ワーカーが実行されると、ユーザーの権限を再計算します。GitLab は、ユーザーの権限を変更する可能性のあるアクションが発生するたびにこのジョブをスケジュールします。同じユーザーが同時に二つのプロジェクトに追加された場合、最初のジョブが開始されていなければ二番目のジョブはスキップできます。最初のジョブが実行されると、両方のプロジェクトの権限が作成されるからです。
GitLabは未来にスケジュールされたジョブをスキップしません。ジョブの実行がスケジュールされる頃には状態が変わっていることを想定しているからです。
より多くの重複排除戦略が提案されています。 もしあなたが別の戦略から利益を得ることができるワーカーを実装しているのであれば、イシューにコメントしてください。
自動重複排除が特定のキューで問題を引き起こす場合、disable_<queue name>_deduplication
という機能フラグを有効にすることで、これを一時的に無効にすることができます。例えば、AuthorizedProjectsWorker
の重複排除を無効にするには、disable_authorized_projects_deduplication
という機能フラグを有効にします。
ChatOpsより:
/chatops run feature set disable_authorized_projects_deduplication true
Railsコンソールから:
Feature.enable!(:disable_authorized_projects_deduplication)
ジョブの緊急性
ジョブには、urgency
属性を設定することができ、:high
、:low
、:throttled
のいずれかを指定することができます。これらの属性には以下のターゲットがあります:
緊急性 | キュースケジューリング対象 | 実行レイテンシ要件 |
---|---|---|
:high
| 10秒 | p50で1秒、p99で10秒 |
:low
| 1分 | 最大運転時間5分 |
:throttled
| なし | 最大運転時間5分 |
ジョブの緊急度を設定するには、urgency
クラスメソッドを使用します:
class HighUrgencyWorker
include ApplicationWorker
urgency :high
# ...
end
遅延に敏感なジョブ
多数のバックグラウンドジョブが一度にスケジューリングされた場合、ワーカーノードが利用可能になるのを待つ間、ジョブの待ち行列が発生することがあります。 これは正常なことで、トラフィックの急増に優雅に対処できるようにすることで、システムに回復力を与えます。 しかし、一部のジョブは、他のジョブよりも待ち行列の影響を受けやすくなっています。 このようなジョブの例には、以下のようなものがあります:
- ブランチへのプッシュに続いてマージリクエストを更新するジョブ。
- ブランチへのプッシュ後に、プロジェクトの既知のブランチのキャッシュを無効にするジョブ。
- 権限の変更後に、ユーザーが表示できるグループとプロジェクトを再計算するジョブ。
- パイプライン内のジョブの状態変更後にCIパイプラインの状態を更新するジョブ。
これらのジョブが遅延すると、ユーザーはその遅延をバグとして認識するかもしれません。たとえば、ブランチをプッシュし、そのブランチのマージリクエストを作成しようとしたときに、UI でブランチが存在しないと表示されるような場合です。私たちはこれらのジョブをurgency :high
とみなします。
これらのジョブは、スケジュールされてから非常に短い時間内に開始されるように、特別な努力が払われています。 しかし、スループットを確保するために、これらのジョブには非常に厳しい実行時間要件もあります:
- ジョブ実行時間の中央値は1秒未満でなければなりません。
- 99%のジョブは10秒以内に完了するはずです。
ワーカーがこれらの期待に応えられない場合、urgency :high
ワーカーとして扱うことはできません。ワーカーを再設計するか、2つの異なるワーカーに仕事を分けることを検討してください。1つは、urgency :high
コードで素早く実行し、もう1つは、urgency :low
、実行遅延の要求がありません (しかし、スケジューリング目標も低くなります)。
キューの緊急度の変更
GitLab.comでは、Sidekiqをいくつかのシャードで実行し、それぞれが特定のタイプのワークロードを表しています。
キューの緊急度を変更する場合、あるいは新しいキューを追加する場合、新しいシャードで予想される仕事量を考慮する必要があります。 既存のキューを変更する場合、古いシャードにも影響がありますが、それは常に仕事の減少であることに注意してください。
そのために、新しいシャードの総実行時間とRPS(スループット)の予想増加量を計算したいと思います。 これらの値は、以下から取得できます:
- キュー詳細ダッシュボードには、キューそのものの値が表示されます。 新しいキューについては、似たようなパターンを持つキューや、似たような状況でスケジュールされているキューを探すことができます。
- [シャードの詳細] ダッシュボードには](https://dashboards.gitlab.net/d/sidekiq-shard-detail/sidekiq-shard-detail)、[合計実行時間] と スループットが表示されます。 [シャードの使用率] パネルには、このシャードに現在余剰容量があるかどうかが表示されます。
次に、新しいシャードで期待されるRPSと実行時間の相対的な増加を確認するために、変更するキューのRPS * 平均実行時間(新しいジョブの推定値)を計算することができます:
new_queue_consumption = queue_rps * queue_duration_avg
shard_consumption = shard_rps * shard_duration_avg
(new_queue_consumption / shard_consumption) * 100
5%未満の増加が見込まれる場合は、それ以上のアクションは必要ありません。
そうでない場合は、@gitlab-org/scalability
にマージリクエストを送り、レビューを依頼してください。
外部依存のジョブ
GitLabアプリケーションのバックグラウンドジョブのほとんどは、他のGitLabサービスと通信します。 例えば、PostgreSQL、Redis、Gitaly、オブジェクトストレージなどです。 これらはジョブの “内部 “依存関係とみなされます。
ただし、一部のジョブは外部サービスに依存している場合があります。 その例としては、以下のようなものがあります:
- ユーザーによって設定されたウェブフックを呼び出すジョブ。
- ユーザが設定したk8sクラスターにアプリケーションをデプロイするジョブです。
これらのジョブには「外部依存関係」があります。 これはバックグラウンド処理クラスターのオペレーションにとっていくつかの点で重要です:
- ほとんどの外部依存関係 (Web-hook など) は SLO を提供しないため、これらのジョブの実行待ち時間を保証することはできません。 実行待ち時間を保証できないため、スループットを保証することはできません。したがって、トラフィックが多い環境では、外部依存関係を持つジョブを緊急度の高いジョブから分離し、これらのキューのスループットを保証する必要があります。
- 外部依存関係のあるジョブのエラーは、エラーの原因が外部にある可能性が高いため、より高い警告しきい値が設定されます。
class ExternalDependencyWorker
include ApplicationWorker
# Declares that this worker depends on
# third-party, external services in order
# to complete successfully
worker_has_external_dependencies!
# ...
end
CPUバウンドとメモリバウンドワーカー
CPUやメモリリソースの制約があるワーカーは、worker_resource_boundary
メソッドでアノテーションしてください。
ほとんどのワーカーは、Redis、PostgreSQL、Gitalyなどの他のサービスからのネットワーク応答を待つために、ほとんどの時間をブロックして過ごす傾向があります。 Sidekiqはマルチスレッド環境なので、これらのジョブは高い同時実行性でスケジュールできます。
Ruby MRI は真のマルチスレッディングをサポートしていません。プロセスをホストするマシンのコア数に関係なく、プロセス内の Ruby コードの 1 セクションだけを一度に実行できるようにすることで、アプリケーション開発を大幅に簡素化するためにGILに依存しています。 IO バインドされたワーカーの場合、スレッドのほとんどは(GIL の外にある)基礎ライブラリでブロックされるため、これは問題ではありません。
多くのスレッドが同時にRubyコードを実行しようとすると、GIL上で競合が発生し、すべてのプロセスが遅くなります。
トラフィックの多い環境では、ワーカーが CPU に負荷がかかっていることを知ることで、同時実行数の少ない別のフリートで実行することができます。 これにより、最適なパフォーマンスが保証されます。
同様に、ワーカーが大量のメモリを使用する場合は、特注の低コンカレンシー、高メモリのフリートで実行することができます。
メモリに束縛されたワーカーは、10-50ms の休止を伴う重い GC 負荷を生成することに注意してください。 これはワーカーのレイテンシ要件に影響を及ぼします。 このため、memory
に束縛されたurgency :high
ジョブは許可されず、CI に失敗します。一般的に、memory
に束縛されたワーカーは推奨されず、仕事を処理する別のアプローチを検討すべきです。
ワーカーが大量のメモリと CPU 時間の両方を必要とする場合、緊急度の高いメモリバインドワーカーに対する上記の制限により、メモリバインドとしてマークされるべきです。
CPUバウンドジョブの宣言
この例では、ジョブをCPUバウンドであると宣言する方法を示します。
class CPUIntensiveWorker
include ApplicationWorker
# Declares that this worker will perform a lot of
# calculations on-CPU.
worker_resource_boundary :cpu
# ...
end
ワーカーがCPUバウンドしているかどうかの判断
ワーカーがCPUバウンドしているかどうかを判断するには、次のような方法を使います:
- Sidekiq 構造化 JSON ログで、ワーカーの
duration
とcpu_s
フィールドを集約します。 -
duration
ジョブの総実行時間を秒単位で表します。 -
cpu_s
はProcess::CLOCK_THREAD_CPUTIME_ID
カウンタから派生したもので、ジョブがCPU上で費やした時間の指標です。 -
cpu_s
をduration
で割ると、CPU使用時間のパーセンテージが得られます。 - この比率が 33% を超えた場合、ワーカーは CPU バインドとみなされ、そのようにアノテーションされるべきです。
- これらの値は、サンプルサイズが小さい場合に使用するのではなく、かなり大規模な集計に使用する必要があることに注意してください。
特集カテゴリー
すべてのSidekiqワーカーは、既知のフィーチャーカテゴリーを定義する必要があります。
ジョブ・ウェイト
これは、Sidekiqをデフォルトの実行モードで実行する場合にのみ使用されます。sidekiq-cluster
を使用してもウェイトは考慮されません。
Coreではsidekiq-cluster
を使用する方向に進んでいるため、、新しく追加されたワーカーにはウェイトを指定する必要はありません。 単純にデフォルトのウェイト(1)を使用することができます。
労働者のコンテキスト
- GitLab 12.8で導入されました。
ログにワーカーに関するより多くの情報を持つために、メタデータをApplicationContext
の形でジョブに追加します。ほとんどの場合、リクエストからジョブをスケジューリングするとき、このコンテキストはすでにリクエストから差し引かれ、スケジューリングされたジョブに追加されます。
ジョブが実行されると、そのジョブがスケジュールされたときにアクティビティだったコンテキストがリストアされます。 これにより、コンテキストは実行中のジョブ内からスケジュールされたすべてのジョブに伝搬されます。
つまり、ほとんどの場合、ジョブにコンテキストを追加するために、私たちは何もする必要がないのです。
しかし、ジョブのスケジューリング時にコンテキストが存在しないインスタンスや、存在するコンテキストが正しくない可能性が高いインスタンスもあります。 このようなインスタンスのために、Rubocopルールを追加し、ログに誤ったメタデータが残らないように注意を喚起しています。
ほとんどの警官と同様に、警官を無効にする正当な理由があります。 この場合、リクエストからのコンテキストが正しい可能性があります。 あるいは、警官に拾われない方法ですでにコンテキストを指定しているかもしれません。 いずれにせよ、警官を無効にするときにどのコンテキストが使用されるかを示すコードコメントを残してください。
オブジェクトをコンテキストに提供するときは、名前空間とプロジェクトのルートが事前にロードされていることを確認します。 これは、すべてのRoutable
で定義されている.with_route
スコープを使用することで可能です。
クロン作業員
リクエストからスケジューリングする場合でも、Cronjob キュー (include CronjobQueue
) にあるワーカーのコンテキストは自動的にクリアされます。 cron ワーカーから他のジョブがスケジューリングされたときに、メタデータが正しくないことがないようにするためです。
Cronワーカー自体はインスタンスンスワイドで実行されるため、ユーザー、名前空間、プロジェクト、またはコンテキストに追加されるべき他のリソースにスコープされません。
しかし、彼らは文脈を必要と_する_他のジョブを予定することがよくあります。
そのため、Worker 内のどこかにコンテキストを示す必要があります。 これは、Worker 内のどこかで以下のメソッドのいずれかを使用することで実現できます:
-
ジョブをスケジュールするコードを
with_context
ヘルパーでラップします:def perform deletion_cutoff = Gitlab::CurrentSettings .deletion_adjourned_period.days.ago.to_date projects = Project.with_route.with_namespace .aimed_for_deletion(deletion_cutoff) projects.find_each(batch_size: 100).with_index do |project, index| delay = index * INTERVAL with_context(project: project) do AdjournedProjectDeletionWorker.perform_in(delay, project.id) end end end
-
コンテキストを提供するバッチスケジューリングメソッドを使用します:
def schedule_projects_in_batch(projects) ProjectImportScheduleWorker.bulk_perform_async_with_contexts( projects, arguments_proc: -> (project) { project.id }, context_proc: -> (project) { { project: project } } ) end
あるいは、遅延を伴うスケジュールの場合:
diffs.each_batch(of: BATCH_SIZE) do |diffs, index| DeleteDiffFilesWorker .bulk_perform_in_with_contexts(index * 5.minutes, diffs, arguments_proc: -> (diff) { diff.id }, context_proc: -> (diff) { { project: diff.merge_request.target_project } }) end
ジョブの一括スケジュール
多くの場合、ジョブを一括でスケジューリングする場合、これらのジョブは包括的なコンテキストではなく、別のコンテキストを持つ必要があります。
その場合は、bulk_perform_async
をbulk_perform_async_with_context
ヘルパーに置き換え、bulk_perform_in
の代わりにbulk_perform_in_with_context
を使用します。
使用例:
ProjectImportScheduleWorker.bulk_perform_async_with_contexts(
projects,
arguments_proc: -> (project) { project.id },
context_proc: -> (project) { { project: project } }
)
第1引数の列挙可能なオブジェクトは、それぞれ2つのブロックに分けられます:
-
arguments_proc
ジョブがスケジュールされる必要がある引数のリストを返す必要があります。 -
context_proc
ジョブのコンテキスト情報を持つハッシュを返す必要があります。
ロギングの引数
SIDEKIQ_LOG_ARGUMENTS
を有効にすると、Sidekiqジョブの引数がログに記録されます。
デフォルトでは、ログに記録される引数は数値引数のみです。 他の型の引数には機密情報が含まれる可能性があるからです。 これを上書きするには、loggable_arguments
をワーカーの内部で、ログに記録する引数のインデックスと一緒に使ってください (数値引数はここで指定する必要はありません)。
使用例:
class MyWorker
include ApplicationWorker
loggable_arguments 1, 3
# object_id will be logged as it's numeric
# string_a will be logged due to the loggable_arguments call
# string_b will be filtered from logs
# string_c will be logged due to the loggable_arguments call
def perform(object_id, string_a, string_b, string_c)
end
end
テスト
各 Sidekiq ワーカーは、他のクラスと同様に RSpec を使用してテストする必要があります。 これらのテストはspec/workers
に配置する必要があります。
Sidekiqの互換性とアップデート
Sidekiqジョブの引数は、それが実行のためにスケジュールされている間、キューに格納されることに留意してください。 オンラインアップデートの間、これはいくつかの可能な状況につながる可能性があります:
- 古いバージョンのアプリケーションがジョブを発行し、アップグレードされたSidekiqノードで実行されます。
- ジョブはアップグレード前にキューに入れられ、アップグレード後に実行されます。
- ジョブは新しいバージョンのアプリケーションを実行しているノードでキューに入れられ、古いバージョンのアプリケーションを実行しているノードで実行されます。
ワーカーの引数の変更
ジョブはアプリケーションの連続したバージョン間で後方互換性と前方互換性を保つ必要があります。 引数を追加または削除すると、すべてのRailsノードとSidekiqノードが更新されたコードを持つ前にデプロイ中に問題が発生する可能性があります。
引数の削除
perform
関数から引数を削除しないでください。 . 代わりに、以下の方法を使用してください:
- デフォルト値(通常は
nil
)を指定し、コメントで非推奨とマークします。 -
perform_async
の議論を使うのはやめてください。 - Worker クラスの値は無視しますが、次のメジャーリリースまで削除しないでください。
次の例では、arg2
を削除したい場合、まずnil
のデフォルト値を設定し、ExampleWorker.perform_async
が呼び出される場所を更新します。
class ExampleWorker
def perform(object_id, arg1, arg2 = nil)
# ...
end
end
引数の追加
Sidekiqワーカーに新しい引数を安全に追加するには、2つのオプションがあります:
- 新しい引数が最初に Worker に追加される、マルチステップデプロイを設定します。
- 追加の引数にはパラメータ・ハッシュを使用します。 これはおそらく最も柔軟なオプションです。
マルチステップのデプロイ
このアプローチでは複数のマージリクエストが必要で、最初のマージリクエストがマージされ、デプロイされてから追加の変更がマージされます。
-
最初のマージリクエストでは、デフォルト値でワーカーに引数を追加します:
class ExampleWorker def perform(object_id, new_arg = nil) # ... end end
- 新しい引数でワーカーをマージし、デプロイします。
- さらなるマージリクエストでは、新しい引数を使用するように
ExampleWorker.perform_async
呼び出しを更新します。
パラメータハッシュ
既存の Worker が既にパラメータハッシュを利用している場合、この方法では複数のデプロイは必要ありません。
-
将来の柔軟性を考慮し、Worker のパラメータハッシュを使用します:
class ExampleWorker def perform(object_id, params = {}) # ... end end
作業員の撤去
マイナーリリースやパッチリリースでは、ワーカーとそのキューを削除しないようにしてください。
オンラインアップデートの間、インスタンスには保留中のジョブがあり、キューを削除するとそれらのジョブが永久に動かなくなる可能性があります。 それらの Sidekiq ジョブのマイグレーションを書けない場合は、メジャーリリースでのみワーカーの削除を検討してください。
キューの名前の変更
ワーカーの削除が危険であるのと同じ理由で、キューの名前を変更する際には注意が必要です。
キューの名前を変更する際には、この例で示されているように、sidekiq_queue_migrate
ヘルパーの移行メソッドを使用してください:
class MigrateTheRenamedSidekiqQueue < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
sidekiq_queue_migrate 'old_queue_name', to: 'new_queue_name'
end
def down
sidekiq_queue_migrate 'new_queue_name', to: 'old_queue_name'
end
end