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 풀이 사용되도록 보장하십시오. Gitlab::SidekiqSharding::Router.get_shard_instance 메서드를 호출하여 적합한 Redis 풀을 가져옵니다.

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 tasks에서 allow_unrouted_sidekiq_calls를 사용했습니다. 이는 GitLab.com에 영향을 미치지 않기 때문입니다. 그러나 개발자는 샤딩이 자체 관리 사용자에게 기능으로서 배포되기 위한 선행 조건이기 때문에 가능한 경우에는 샤드 인식 코드를 작성해야 합니다.

재시도

Sidekiq는 25번의 재시도를 기본값으로 사용하며 각 재시도 사이에 백오프를 수행합니다. 25번의 재시도는 마지막 재시도가 처음 시도 후 약 세 주 뒤에 발생하게 됨을 의미합니다 (모든 24번의 재시도가 실패한 경우를 가정합니다).

이는 작업이 예약되고 실행 사이에 많은 일이 발생할 수 있다는 것을 의미합니다. 따라서, 예약된 후 상태가 변경되어야 하는 경우에 작업이 25번 실패하지 않도록 워커를 보호해야 합니다. 예를 들어, 예약된 프로젝트가 삭제되었을 때 작업이 실패해서는 안 됩니다.

다음과 같이 작성하십시오:

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

대부분의 워커에 대해 - 특히 멱등 워커의 경우 - 25번의 재시도는 충분합니다. 많은 예전 워커들은 3번의 재시도를 선언하는데, 이것은 이전에 GitLab 응용프로그램에서의 기본값이었습니다. 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_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 # 예외를 throw합니다
end

Sidekiq 워커 연기

Sidekiq 워커는 두 가지 방법으로 연기됩니다.

  1. 수동: 특정 워커를 명시적으로 연기하기 위해 기능 플래그를 사용할 수 있으며, 자세한 내용은 여기에서 찾을 수 있습니다.
  2. 자동: 배치 마이그레이션의 끄는 메커니즘과 유사하게, 데이터베이스 상태 지표가 Sidekiq 워커를 연기하기 위해 사용됩니다.

    자동 연기 메커니즘을 사용하려면, worker는 defer_on_database_health_signalgitlab_schema, delay_by (지연 시간) 및 (autovacuum db indicator에서 사용되는) 테이블과 함께 호출해야 합니다.

    예시:

     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에서 적용할 수 있습니다. 잠재적인 변경 사항에 대한 자세한 내용은 epic 596을 확인하십시오.

또한 config/sidekiq_queues.yml를 다시 생성하려면 bin/rake gitlab:sidekiq:sidekiq_queues_yml:generate를 실행하십시오.

대기열 네임스페이스

서로 다른 워커는 같은 대기열을 공유할 수 없지만 대기열 네임스페이스를 공유할 수 있습니다.

워커에 대한 대기열 네임스페이스를 정의하면 해당 네임스페이스의 모든 워커 작업을 명시적으로 나열할 필요 없이 Sidekiq 프로세스를 시작할 수 있습니다. 예를 들어 sidekiq-cron이 관리하는 모든 워커가 cronjob 대기열 네임스페이스를 사용하는 경우, 이러한 유형의 예약된 작업을 위해 특정하된 Sidekiq 프로세스를 실행할 수 있습니다. 나중에 cronjob 네임스페이스를 사용하는 새 워커가 추가되면 설정을 변경할 필요 없이 (재시작된 후) Sidekiq 프로세스도 해당 워커의 작업을 가져옵니다.

대기열 네임스페이스는 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 워커 클래스에 대해 버전을 지정할 수 있습니다. 이것은 작업이 생성될 때 함께 전송됩니다.

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에 저장합니다. 원본 크기가 100 KB보다 큰 경우 Sidekiq 작업의 인수를 압축하여 과도한 메모리 사용을 피합니다.

압축 후에도 크기가 5 MB를 초과하면 작업을 예약할 때 ExceedLimitError 오류가 발생합니다.

이런 일이 발생하면 Sidekiq에서 데이터를 사용할 수 있는 다른 방법을 의존해야 합니다.

  • 데이터를 다른 곳(예: 데이터베이스 또는 기타 곳)에서로드하여 Sidekiq에서 데이터를 사용하여 재작성합니다.
  • 작업을 예약하기 전에 객체 저장소에 데이터를 저장하고 작업 내에서 가져올 수 있습니다.

작업 가중치

일부 작업에는 선언된 가중치가 있습니다. 기본 실행 모드인 시키큐를 실행하는 경우에만 사용됩니다. sidekiq-cluster를 사용하지 않습니다.

우리는 무료로 sidekiq-cluster를 사용하도록 이동하고 있습니다, 따라서 새로 추가된 워커는 명시적으로 가중치를 지정할 필요가 없습니다. 기본 가중치인 1을 사용할 수 있습니다.

테스트

각 Sidekiq 워커는 기타 클래스와 마찬가지로 RSpec을 사용하여 테스트해야 합니다. 이러한 테스트는 spec/workers에 배치되어야 합니다.

Sidekiq Redis 및 API 상호 작용

애플리케이션에서는 Sidekiq.redis 및 Sidekiq APIs와의 상호 작용을 최소화해야 합니다. 일반 응용 프로그램 논리에서의 이러한 상호 작용은 재사용을 위해 Sidekiq middleware로 추상화되어야 합니다. Sidekiq 데이터 저장소와 애플리케이션 논리를 분리함으로써 GitLab 백그라운드 처리 설정을 수평으로 확장할 때 더 큰 자유를 제공합니다.

이 규칙의 일부 예외는 마이그레이션 관련 논리 또는 관리 작업입니다.