Sidekiq 개발 지침

우리는 Sidekiq를 백그라운드 작업 프로세서로 사용합니다. 이 가이드는 GitLab.com에서 잘 작동하고 기존의 워커 클래스와 일치하는 작업을 작성하기 위한 것입니다. GitLab을 관리하는 방법에 대한 정보는 Sidekiq 구성을 참조하세요.

다음 주제에 대한 추가 세부정보가 포함된 페이지가 있습니다:

  1. 업데이트 간 호환성

  2. 작업의 아이도포텐스와 작업 중복 제거

  3. 제한된 용량 워커: 지정된 동시성으로 지속적으로 작업 수행하기

  4. 로깅

  5. 워커 속성

    1. 작업 긴급성은 큐잉 및 실행 SLO를 지정합니다.

    2. 리소스 경계외부 종속성은 작업량을 설명하기 위해 사용됩니다.

    3. 기능 범주화

    4. 데이터베이스 로드 밸런싱

ApplicationWorker

모든 워커는 Sidekiq::Worker 대신 ApplicationWorker를 포함해야 하며, 이는 몇 가지 편리한 메서드를 추가하고 라우팅 규칙에 따라 큐를 자동으로 설정합니다.

샤딩

모든 Sidekiq API 호출은 샤딩을 고려해야 합니다. 이를 위해서는, Sidekiq::Client.via 블록 내에서 Sidekiq API를 사용하여 정확한 Sidekiq.redis 풀을 보장해야 합니다.

적절한 Redis 풀은 Gitlab::SidekiqSharding::Router.get_shard_instance 메서드를 호출하여 얻습니다.

pool_name, pool = Gitlab::SidekiqSharding::Router.get_shard_instance(worker_class.sidekiq_options['store'])

Sidekiq::Client.via(pool) do
  ...
end

라우팅되지 않은 Sidekiq 호출은 모든 API 요청, 서버 측의 Sidekiq 작업 및 테스트에서 검증자가 잡습니다. 우리는 Gitlab::SidekiqSharding::Router를 이용하여 애플리케이션 로직을 작성하는 것을 추천합니다. 그러나 샤딩이 출시되지 않은 기능이므로, 컴포넌트가 GitLab.com에 영향을 미치지 않는 경우, 아래와 같이 .allow_unrouted_sidekiq_calls 범위 내에서 실행하는 것이 허용됩니다.

# 이 경우 라우팅되지 않은 Sidekiq 호출을 허용하는 이유를 설명하는 주석 추가
Gitlab::SidekiqSharding::Validator.allow_unrouted_sidekiq_calls do
  # 여러분의 라우팅되지 않은 논리
end

과거의 예로는 Geo Rake 작업에서 allow_unrouted_sidekiq_calls를 사용한 것이 있습니다. 이는 GitLab.com에 영향을 미치지 않기 때문입니다. 그러나 개발자는 가능한 경우 샤드 인식 코드를 작성해야 하며, 이는 샤딩이 자체 관리 사용자에게 기능으로 출시되기 위한 전제 조건입니다.

재시도

Sidekiq는 25회 재시도를 기본값으로 사용하며, 각 재시도 간 대기 시간이 있습니다. 25회의 재시도는 마지막 재시도가 첫 번째 시도 약 3주 후에 발생함을 의미합니다(모든 24회의 재시도가 실패한 경우).

이는 작업이 예약되고 실행되는 사이에 많은 일이 발생할 수 있다는 것을 의미합니다. 그러므로 우리는 작업자가 예약된 후 상태가 변경될 때 25번 실패하지 않도록 작업자를 보호해야 합니다. 예를 들어, 작업이 예약된 프로젝트가 삭제되면 작업은 실패해야 하지 않습니다.

대신에:

def perform(project_id)
  project = Project.find(project_id)
  # ...
end

다음과 같이 작성합니다:

def perform(project_id)
  project = Project.find_by_id(project_id)
  return unless project
  # ...
end

대부분의 워커—특히 아이도포텐트 워커—의 경우 기본값인 25회의 재시도가 충분합니다. 많은 구식 워커는 GitLab 애플리케이션 내에서 기본값이었던 3회의 재시도를 선언합니다. 3회의 재시도는 몇 분 동안 발생하므로, 작업이 완전히 실패할 가능성이 높습니다.

아래 사항 중 어느 하나에 해당하는 경우 낮은 재시도 횟수가 적용될 수 있습니다:

  1. 워커가 외부 서비스에 연결되며 전달에 대한 보장을 제공하지 않는 경우. 예를 들어, 웹후크.

  2. 워커가 아이도포텐트가 아니며 여러 번 실행하면 시스템이 일관되지 않은 상태가 될 수 있는 경우. 예를 들어, 시스템 노트를 게시한 다음 작업을 수행하는 워커: 두 번째 단계가 실패하고 워커가 재시도를 수행하면 시스템 노트가 다시 게시됩니다.

  3. 워커가 빈번하게 실행되는 크론잡인 경우. 예를 들어, 크론잡이 매시간 실행된다면, 같은 작업이 동시에 두 번 실행될 필요가 없으므로 한 시간을 넘어서 재시도할 필요는 없습니다.

각 워커의 재시도는 당사의 지표에서 실패로 계산됩니다. 항상 9번 실패하고 10번째에서 성공하는 워커는 90% 오류율을 갖게 됩니다.

예외를 Sentry에서 추적하지 않고 수동으로 워커를 재시도하려면, 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_by_id(msg['args'].first)
  return unless project

  project.perform_a_rollback # 영구적인 실패 처리
end

def perform(project_id)
  project = Project.find_by_id(project_id)
  return unless project

  project.some_action # 예외 발생
end

Sidekiq 작업자 지연

Sidekiq 작업자는 두 가지 방법으로 지연됩니다,

  1. 수동: 기능 플래그를 사용하여 특정 작업자를 명시적으로 지연시킬 수 있으며, 더 많은 세부정보는 여기를 참조하시기 바랍니다.

  2. 자동: 배치 마이그레이션에서의 스로틀링 메커니즘과 유사하게, 데이터베이스 건강 지표를 사용하여 Sidekiq 작업자를 지연시킵니다.

    자동 지연 메커니즘을 사용하려면, 작업자가 defer_on_database_health_signal을 호출하여 gitlab_schema, delay_by (지연 시간) 및 테이블 (자동 진공 데이터베이스 지표에 의해 사용됨)을 매개변수로 지정해야 합니다.

    예시:

    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를 사용하여 큐 이름을 수동으로 재정의할 필요는 거의 없습니다.

새 작업자를 추가한 후, bin/rake gitlab:sidekiq:all_queues_yml:generate를 실행하여 app/workers/all_queues.yml 또는 ee/app/workers/all_queues.yml을 다시 생성해야 하며, 이를 통해 sidekiq-cluster에서 라우팅 규칙을 사용하지 않는 설치에서 인식할 수 있습니다. 잠재적인 변경 사항에 대한 자세한 내용은 에픽 596을 참조하세요.

또한, bin/rake gitlab:sidekiq:sidekiq_queues_yml:generate를 실행하여 config/sidekiq_queues.yml을 다시 생성합니다.

큐 네임스페이스

서로 다른 작업자는 큐를 공유할 수는 없지만, 큐 네임스페이스는 공유할 수 있습니다.

작업자를 위한 큐 네임스페이스를 정의하면, 모든 작업자를 위해 자동으로 작업을 처리하는 Sidekiq 프로세스를 시작할 수 있으며, 모든 큐 이름을 명시적으로 나열할 필요가 없습니다. 예를 들어, sidekiq-cron에 의해 관리되는 모든 작업자가 cronjob 큐 네임스페이스를 사용하는 경우, 이러한 종류의 예약 작업을 위해 특정한 Sidekiq 프로세스를 생성할 수 있습니다. 나중에 cronjob 네임스페이스를 사용하는 새로운 작업자가 추가되면, 해당 작업에는 영향을 주지 않으면서도(재시작된 후) Sidekiq 프로세스가 자동으로 작업을 처리합니다.

큐 네임스페이스는 queue_namespace DSL 클래스 메서드를 사용하여 설정할 수 있습니다:

class SomeScheduledTaskWorker
  include ApplicationWorker

  queue_namespace :cronjob

  # ...
end

이 과정에서 SomeScheduledTaskWorker.queuecronjob:some_scheduled_task로 설정됩니다. 일반적으로 사용되는 네임스페이스는 작업자 클래스에 쉽게 포함될 수 있는 자체 concern 모듈이 있으며, 이는 큐 네임스페이스 외에도 다른 Sidekiq 옵션을 설정할 수 있습니다. 예를 들어, CronjobQueue는 네임스페이스를 설정하지만, 재시도를 비활성화하는 것도 포함됩니다.

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로 설정). 하지만, 여전히 이전 버전의 인수로 큐에 넣은 작업을 처리할 수 있어야 합니다. 작업자의 perform 메서드에서 특정 작업 버전에 따라 분기하려면 self.job_version을 읽거나 제공된 인수의 수나 유형을 읽을 수 있습니다.

작업 크기

GitLab은 Sidekiq 작업과 그 인수를 Redis에 저장합니다. 과도한 메모리 사용을 피하기 위해, Sidekiq 작업의 원본 크기가 100 KB를 초과하면 인수를 압축합니다.

압축 후에도 크기가 여전히 5 MB를 초과할 경우, 작업 예약 시 ExceedLimitError 오류가 발생합니다.

이런 경우, Sidekiq에서 데이터를 사용할 수 있게 하는 다른 방법에 의존해야 합니다. 가능한 해결 방법은 다음과 같습니다:

  • 데이터베이스 또는 다른 곳에서 로드된 데이터를 사용하여 Sidekiq에서 데이터를 재구성합니다.
  • 작업 예약 전에 데이터를 오브젝트 스토리지에 저장하고, 작업 내부에서 이를 가져옵니다.

작업 가중치

일부 작업에는 선언된 가중치가 있습니다. 이는 기본 실행 모드에서 Sidekiq를 실행할 때만 사용됩니다 - sidekiq-cluster를 사용하면 가중치가 고려되지 않습니다.

우리가 Free에서 sidekiq-cluster 사용으로 이동하고 있기 때문에, 새로 추가된 작업자는 가중치를 지정할 필요가 없습니다. 그들은 기본 가중치인 1을 사용할 수 있습니다.

테스트

각 Sidekiq 작업자는 다른 클래스와 마찬가지로 RSpec를 사용하여 테스트해야 합니다. 이러한 테스트는 spec/workers에 배치해야 합니다.

Sidekiq Redis 및 API와 상호작용하기

어플리케이션은 Sidekiq.redis 및 Sidekiq API와의 상호작용을 최소화해야 합니다. 일반적인 어플리케이션 논리에서의 이러한 상호작용은 팀 간 재사용을 위해 Sidekiq 미들웨어로 추상화되어야 합니다. 어플리케이션 논리를 Sidekiq 데이터 저장소에서 분리함으로써 GitLab 백그라운드 처리 설정을 수평적으로 확장하는 데 더 큰 자유를 허용합니다.

이 규칙에 대한 몇 가지 예외는 마이그레이션 관련 논리 또는 관리 작업이 될 수 있습니다.