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.queuecronjob:some_scheduled_taskに設定します。 一般的に使用される名前空間は、ワーカークラスに簡単に組み込むことができる独自の懸念モジュールを持ち、それはキューの名前空間以外の他の Sidekiq オプションを設定するかもしれません。例えば、CronjobQueueは名前空間を設定しますが、リトライも無効にします。

bundle exec sidekiq は名前空間を意識しており、--queue(-q) オプション、あるいはconfig/sidekiq_queues.yml:queues: セクションにおいて、単純なキュー名の代わりに名前空間が指定された場合、名前空間内のすべてのキュー (厳密には、名前空間名を先頭に持つすべてのキュー) を自動的に待ち受けます。

既存の名前空間にワーカーを追加する場合、名前空間を処理する Sidekiq プロセスが利用できるリソースが適切に調整されないと、余分なジョブが既にあるワーカーのジョブからリソースを奪ってしまうため、注意して行う必要があることに注意してください。

無限のジョブ

ジョブが複数の理由で失敗することはよく知られています。 例えば、ネットワークの停止やバグなどです。 このアドレスに対応するために、Sidekiqには組み込みのリトライメカニズムがあり、GitLab内のほとんどのワーカーでデフォルトで使用されています。

Sidekiqがジョブをidempotentかつtransactionalにすることを推奨しているのはそのためです。

一般的なルールとして、労働者は次のような場合、idempotentとみなすことができます:

  • 同じ引数で複数回実行しても安全です。
  • アプリケーションの副作用は一度しか起きないと予想されます(または、2回目の実行の副作用は影響しません)。

その良い例がキャッシュ期限切れワーカーです。

注意: Idempotent Worker にスケジュールされたジョブは、同じ引数を持つ未開始のジョブが既にキューにある場合、自動的に重複排除されます。

労働者がべき等であることの保証

以下の共有例を用いて、ワーカテストがパスすることを確認してください:

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! 呼び出しは一番上のワーカークラスでのみ行うことを推奨します。

注意:Worker クラスがidempotent としてマークされていない場合、cop は失敗します。 ジョブを安全に複数回実行できる自信がない場合は、cop をスキップすることを検討してください。

重複排除

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

遅延に敏感なジョブ

多数のバックグラウンドジョブが一度にスケジューリングされた場合、ワーカーノードが利用可能になるのを待つ間、ジョブの待ち行列が発生することがあります。 これは正常なことで、トラフィックの急増に優雅に対処できるようにすることで、システムに回復力を与えます。 しかし、一部のジョブは、他のジョブよりも待ち行列の影響を受けやすくなっています。 このようなジョブの例には、以下のようなものがあります:

  1. ブランチへのプッシュに続いてマージリクエストを更新するジョブ。
  2. ブランチへのプッシュ後に、プロジェクトの既知のブランチのキャッシュを無効にするジョブ。
  3. 権限の変更後に、ユーザーが表示できるグループとプロジェクトを再計算するジョブ。
  4. パイプライン内のジョブの状態変更後にCIパイプラインの状態を更新するジョブ。

これらのジョブが遅延すると、ユーザーはその遅延をバグとして認識するかもしれません。たとえば、ブランチをプッシュし、そのブランチのマージリクエストを作成しようとしたときに、UI でブランチが存在しないと表示されるような場合です。私たちはこれらのジョブをurgency :highとみなします。

これらのジョブは、スケジュールされてから非常に短い時間内に開始されるように、特別な努力が払われています。 しかし、スループットを確保するために、これらのジョブには非常に厳しい実行時間要件もあります:

  1. ジョブ実行時間の中央値は1秒未満でなければなりません。
  2. 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、オブジェクトストレージなどです。 これらはジョブの “内部 “依存関係とみなされます。

ただし、一部のジョブは外部サービスに依存している場合があります。 その例としては、以下のようなものがあります:

  1. ユーザーによって設定されたウェブフックを呼び出すジョブ。
  2. ユーザが設定したk8sクラスターにアプリケーションをデプロイするジョブです。

これらのジョブには「外部依存関係」があります。 これはバックグラウンド処理クラスターのオペレーションにとっていくつかの点で重要です:

  1. ほとんどの外部依存関係 (Web-hook など) は SLO を提供しないため、これらのジョブの実行待ち時間を保証することはできません。 実行待ち時間を保証できないため、スループットを保証することはできません。したがって、トラフィックが多い環境では、外部依存関係を持つジョブを緊急度の高いジョブから分離し、これらのキューのスループットを保証する必要があります。
  2. 外部依存関係のあるジョブのエラーは、エラーの原因が外部にある可能性が高いため、より高い警告しきい値が設定されます。
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 ログで、ワーカーのdurationcpu_s フィールドを集約します。
  • duration ジョブの総実行時間を秒単位で表します。
  • cpu_sProcess::CLOCK_THREAD_CPUTIME_IDカウンタから派生したもので、ジョブがCPU上で費やした時間の指標です。
  • cpu_sduration で割ると、CPU使用時間のパーセンテージが得られます。
  • この比率が 33% を超えた場合、ワーカーは CPU バインドとみなされ、そのようにアノテーションされるべきです。
  • これらの値は、サンプルサイズが小さい場合に使用するのではなく、かなり大規模な集計に使用する必要があることに注意してください。

特集カテゴリー

すべてのSidekiqワーカーは、既知のフィーチャーカテゴリーを定義する必要があります。

ジョブ・ウェイト

これは、Sidekiqをデフォルトの実行モードで実行する場合にのみ使用されます。sidekiq-clusterを使用してもウェイトは考慮されません。

Coreではsidekiq-cluster を使用する方向に進んでいるため、、新しく追加されたワーカーにはウェイトを指定する必要はありません。 単純にデフォルトのウェイト(1)を使用することができます。

労働者のコンテキスト

ログにワーカーに関するより多くの情報を持つために、メタデータをApplicationContextの形でジョブに追加します。ほとんどの場合、リクエストからジョブをスケジューリングするとき、このコンテキストはすでにリクエストから差し引かれ、スケジューリングされたジョブに追加されます。

ジョブが実行されると、そのジョブがスケジュールされたときにアクティビティだったコンテキストがリストアされます。 これにより、コンテキストは実行中のジョブ内からスケジュールされたすべてのジョブに伝搬されます。

つまり、ほとんどの場合、ジョブにコンテキストを追加するために、私たちは何もする必要がないのです。

しかし、ジョブのスケジューリング時にコンテキストが存在しないインスタンスや、存在するコンテキストが正しくない可能性が高いインスタンスもあります。 このようなインスタンスのために、Rubocopルールを追加し、ログに誤ったメタデータが残らないように注意を喚起しています。

ほとんどの警官と同様に、警官を無効にする正当な理由があります。 この場合、リクエストからのコンテキストが正しい可能性があります。 あるいは、警官に拾われない方法ですでにコンテキストを指定しているかもしれません。 いずれにせよ、警官を無効にするときにどのコンテキストが使用されるかを示すコードコメントを残してください。

オブジェクトをコンテキストに提供するときは、名前空間とプロジェクトのルートが事前にロードされていることを確認します。 これは、すべてのRoutableで定義されている.with_route スコープを使用することで可能です。

クロン作業員

リクエストからスケジューリングする場合でも、Cronjob キュー (include CronjobQueue) にあるワーカーのコンテキストは自動的にクリアされます。 cron ワーカーから他のジョブがスケジューリングされたときに、メタデータが正しくないことがないようにするためです。

Cronワーカー自体はインスタンスンスワイドで実行されるため、ユーザー、名前空間、プロジェクト、またはコンテキストに追加されるべき他のリソースにスコープされません。

しかし、彼らは文脈を必要と_する_他のジョブを予定することがよくあります。

そのため、Worker 内のどこかにコンテキストを示す必要があります。 これは、Worker 内のどこかで以下のメソッドのいずれかを使用することで実現できます:

  1. ジョブをスケジュールするコードを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
    
  2. コンテキストを提供するバッチスケジューリングメソッドを使用します:

      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_asyncbulk_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ジョブの引数は、それが実行のためにスケジュールされている間、キューに格納されることに留意してください。 オンラインアップデートの間、これはいくつかの可能な状況につながる可能性があります:

  1. 古いバージョンのアプリケーションがジョブを発行し、アップグレードされたSidekiqノードで実行されます。
  2. ジョブはアップグレード前にキューに入れられ、アップグレード後に実行されます。
  3. ジョブは新しいバージョンのアプリケーションを実行しているノードでキューに入れられ、古いバージョンのアプリケーションを実行しているノードで実行されます。

ワーカーの引数の変更

ジョブはアプリケーションの連続したバージョン間で後方互換性と前方互換性を保つ必要があります。 引数を追加または削除すると、すべてのRailsノードとSidekiqノードが更新されたコードを持つ前にデプロイ中に問題が発生する可能性があります。

引数の削除

perform 関数から引数を削除しないでください。 . 代わりに、以下の方法を使用してください:

  1. デフォルト値(通常はnil)を指定し、コメントで非推奨とマークします。
  2. perform_asyncの議論を使うのはやめてください。
  3. Worker クラスの値は無視しますが、次のメジャーリリースまで削除しないでください。

次の例では、arg2を削除したい場合、まずnil のデフォルト値を設定し、ExampleWorker.perform_async が呼び出される場所を更新します。

class ExampleWorker
  def perform(object_id, arg1, arg2 = nil)
    # ...
  end
end

引数の追加

Sidekiqワーカーに新しい引数を安全に追加するには、2つのオプションがあります:

  1. 新しい引数が最初に Worker に追加される、マルチステップデプロイを設定します。
  2. 追加の引数にはパラメータ・ハッシュを使用します。 これはおそらく最も柔軟なオプションです。
マルチステップのデプロイ

このアプローチでは複数のマージリクエストが必要で、最初のマージリクエストがマージされ、デプロイされてから追加の変更がマージされます。

  1. 最初のマージリクエストでは、デフォルト値でワーカーに引数を追加します:

    class ExampleWorker
      def perform(object_id, new_arg = nil)
        # ...
      end
    end
    
  2. 新しい引数でワーカーをマージし、デプロイします。
  3. さらなるマージリクエストでは、新しい引数を使用するようにExampleWorker.perform_async 呼び出しを更新します。
パラメータハッシュ

既存の Worker が既にパラメータハッシュを利用している場合、この方法では複数のデプロイは必要ありません。

  1. 将来の柔軟性を考慮し、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