Sidekiq 개발 가이드라인
GitLab v19.1우리는 Sidekiq을 백그라운드 job 처리기로 사용합니다. 다음 주제에 대한 상세 내용이 담긴 페이지가 있습니다: 제한된 용량 워커: 지정된 동시성으로 작업 지속 수행 **Job 긴급도(urgency)**는 큐잉 및 실행 SLO를 지정합니다.
우리는 Sidekiq을 백그라운드 job 처리기로 사용합니다. 이 가이드는 GitLab.com에서 잘 동작하고 기존 워커 클래스와 일관성을 유지하는 job을 작성하기 위한 것입니다. GitLab 관리에 대한 정보는 Sidekiq 설정을 참고하세요.
다음 주제에 대한 상세 내용이 담긴 페이지가 있습니다:
**Job 긴급도(urgency)**는 큐잉 및 실행 SLO를 지정합니다.
-
리소스 경계 및 외부 의존성은 워크로드를 설명합니다.
-
기능 분류
-
데이터베이스 로드 밸런싱
ApplicationWorker#
모든 워커는 Sidekiq::Worker 대신 ApplicationWorker를 포함해야 합니다.
ApplicationWorker는 편의 메서드를 추가하고 라우팅 규칙에 따라 큐를 자동으로 설정합니다.
Cells 호환성#
GitLab.com은 서로 다른 조직이 GitLab의 별개 물리 인스턴스에 의해 서비스되는 Cells 아키텍처로 이동 중입니다.
모든 Sidekiq job은 단일 조직 범위로 지정되어야 합니다.
단일 조직으로 컴퓨트 범위를 지정하는 방법에 대한 가이드는 Current.organization 사용을 참고하세요.
모든 경우에서 마이그레이션 중 데이터 손실을 방지하기 위해, 조직 간 job은 다음 두 가지 조건이 모두 충족될 때만 허용됩니다:
-
job이 반복 실행되는 cron job인 경우.
-
job이 멱등(idempotent)인 경우.
샤딩(Sharding)#
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 job, 그리고 테스트에서 유효성 검사기에 의해 감지됩니다.
Gitlab::SidekiqSharding::Router를 사용하여 애플리케이션 로직을 작성하는 것을 권장합니다.
그러나 샤딩은 아직 릴리즈되지 않은 기능이므로, 해당 컴포넌트가 GitLab.com에 영향을 주지 않는다면 아래와 같이 .allow_unrouted_sidekiq_calls 범위 내에서 실행하는 것도 허용됩니다:
# Add a comment explaining why it is safe to allow unrouted Sidekiq calls in this case
Gitlab::SidekiqSharding::Validator.allow_unrouted_sidekiq_calls do
# your unrouted logic
end
과거 예시로, Geo Rake tasks에서 GitLab.com에 영향을 미치지 않아 allow_unrouted_sidekiq_calls를 사용한 경우가 있습니다.
그러나 개발자는 가능한 경우 샤드 인식 코드를 작성해야 합니다.
이는 샤딩이 GitLab Self-Managed 사용자에게 기능으로 릴리즈되기 위한 사전 요구 사항이기 때문입니다.
재시도(Retries)#
Sidekiq은 기본적으로 각 재시도 사이에 백오프를 적용하여 25회 재시도를 사용합니다. 25회 재시도는 (이전 24번의 재시도가 모두 실패했다고 가정할 때) 마지막 재시도가 첫 번째 시도 후 약 3주 후에 발생한다는 것을 의미합니다.
이는 job이 예약된 시점과 실행 시점 사이에 많은 일이 발생할 수 있음을 의미합니다. 따라서 예약된 후 상태가 변경되어도 워커가 25번 실패하지 않도록 보호해야 합니다. 예를 들어, job이 예약된 프로젝트가 삭제된 경우에도 job은 실패하지 않아야 합니다.
다음 코드 대신:
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회 재시도로 충분합니다. 기존의 많은 워커들은 3회 재시도를 선언하고 있는데, 이것은 GitLab 애플리케이션 내에서 이전 기본값이었습니다. 3회 재시도는 몇 분 내에 발생하므로, job이 완전히 실패할 가능성이 높습니다.
다음 경우 중 하나에 해당하면 낮은 재시도 횟수가 적합할 수 있습니다:
-
워커가 외부 서비스에 연락하고 전달을 보장하지 않는 경우. 예: 웹훅.
-
워커가 멱등하지 않아 여러 번 실행하면 시스템이 일관성 없는 상태가 될 수 있는 경우. 예: 시스템 노트를 게시한 후 작업을 수행하는 워커 — 두 번째 단계가 실패하고 워커가 재시도하면 시스템 노트가 다시 게시됩니다.
-
워커가 자주 실행되는 cron job인 경우. 예: cron job이 매 시간 실행된다면, 같은 job 두 개가 동시에 실행될 필요가 없으므로 한 시간을 초과하는 재시도는 필요하지 않습니다.
워커의 각 재시도는 메트릭에서 실패로 집계됩니다. 항상 9번 실패하고 10번째에 성공하는 워커는 90%의 오류율을 가지게 됩니다.
Sentry에서 예외를 추적하지 않고 수동으로 워커를 재시도하려면 Gitlab::SidekiqMiddleware::RetryError에서 상속된 예외 클래스를 사용하세요.
ServiceUnavailable = Class.new(::Gitlab::SidekiqMiddleware::RetryError)
def perform
...
raise ServiceUnavailable if external_service_unavailable?
end
실패 처리#
실패는 일반적으로 위에서 언급한 내장 재시도 메커니즘을 활용하는 Sidekiq 자체에 의해 처리됩니다. Sidekiq이 job을 다시 예약할 수 있도록 예외가 발생하도록 허용해야 합니다.
모든 재시도 시도 후 job이 실패할 때 작업을 수행해야 하는 경우, sidekiq_retries_exhausted 메서드에 추가하세요.
sidekiq_retries_exhausted do |msg, ex|
project = Project.find_by_id(msg['args'].first)
return unless project
project.perform_a_rollback # handle the permanent failure
end
def perform(project_id)
project = Project.find_by_id(project_id)
return unless project
project.some_action # throws an exception
end
동시성 제한(Concurrency Limit)#
시스템 과부하를 방지하고 안정적인 운영을 보장하기 위해 모든 워커에 동시성 제한을 설정하는 것을 강력히 권장합니다. 각 워커가 예약할 수 있는 job 수를 제한하면 심각한 장애로 이어질 수 있는 시스템 과부하 위험을 줄이는 데 도움이 됩니다.
이 지침은 .com과 self-managed 고객 모두에게 적용됩니다. 단일 워커가 수천 개의 job을 예약하면 SM 인스턴스의 정상적인 기능을 쉽게 방해할 수 있습니다.
Sidekiq에 스레드가 20개만 있고 특정 job의 제한이 200이라면, 200의 동시성에 도달할 수 없으므로 제한이 적용되지 않습니다.
정적 동시성 제한#
정적 제한의 경우 다음 예시를 참고하세요:
class LimitedWorker
include ApplicationWorker
concurrency_limit -> { 100 if Feature.enabled?(:concurrency_limit_some_worker, Feature.current_request) }
# ...
end
동시성 제한을 롤아웃할 때는 boolean 피처 플래그(완전히 켜짐/꺼짐)만 사용하세요.
Feature.current_request를 사용한 퍼센트 기반 롤아웃은 일관성 없는 동작을 유발할 수 있습니다.
또는 고정 제한을 직접 설정할 수도 있습니다:
concurrency_limit -> { 250 }
정적 제한을 사용한다는 것은 업데이트나 변경 사항이 있을 때마다 MR을 머지하고 다음 배포를 기다려야 효과가 발생한다는 점을 유의하세요.
인스턴스 구성 가능 동시성 제한#
인스턴스 관리자가 동시성 제한을 제어하도록 허용하려면:
concurrency_limit -> { ApplicationSetting.current.some_feature_concurrent_sidekiq_jobs }
이 방법은 .com과 GitLab Self-Managed 인스턴스에 대해 별도의 제한을 두는 것도 가능하게 합니다. 이를 달성하기 위해 다음을 수행할 수 있습니다:
-
self-managed 제한을 기본값으로 설정하는 구성 옵션을 추가하는 마이그레이션을 생성합니다.
-
동일한 MR에서 .com에 대해서만 제한을 업데이트하는 마이그레이션을 작성합니다.
제한값 선택 방법#
적절한 제한값을 결정하려면 Grafana에서 sidekiq: Worker Concurrency Detail 대시보드를 가이드로 활용할 수 있습니다.
동시성 제한은 일시적으로 초과될 수 있으며 엄격한 제한으로 의존해서는 안 됩니다.
Sidekiq 워커 지연(Deferring)#
Sidekiq 워커는 두 가지 방법으로 지연됩니다:
수동: 피처 플래그를 사용하여 특정 워커를 명시적으로 지연시킬 수 있습니다. 자세한 내용은 Sidekiq job 지연을 참고하세요.
자동: 배치 백그라운드 마이그레이션의 스로틀링 메커니즘과 유사하게, 데이터베이스 상태 지표를 사용하여 Sidekiq 워커를 지연시킵니다.
자동 지연 메커니즘을 사용하려면 워커가 defer_on_database_health_signal을 gitlab_schema, delay_by(지연 시간), tables(autovacuum DB 지표에서 사용됨)를 파라미터로 호출하여 옵트인해야 합니다.
예시:
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의 경우, 로그에는 소스를 나타내는 다음 내용이 포함됩니다:
-
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을 재생성하세요.
큐 네임스페이스#
서로 다른 워커는 큐를 공유할 수 없지만, 큐 네임스페이스를 공유할 수 있습니다.
워커에 대한 큐 네임스페이스를 정의하면, 모든 큐 이름을 명시적으로 나열하지 않고도 해당 네임스페이스의 모든 워커의 job을 자동으로 처리하는 Sidekiq 프로세스를 시작할 수 있습니다.
예를 들어, sidekiq-cron이 관리하는 모든 워커가 cronjob 큐 네임스페이스를 사용한다면, 이러한 종류의 예약된 job을 위한 Sidekiq 프로세스를 별도로 시작할 수 있습니다.
나중에 cronjob 네임스페이스를 사용하는 새 워커가 추가되면, Sidekiq 프로세스는 (재시작 후) 설정을 변경할 필요 없이 해당 워커의 job도 처리합니다.
큐 네임스페이스는 queue_namespace DSL 클래스 메서드를 사용하여 설정할 수 있습니다:
class SomeScheduledTaskWorker
include ApplicationWorker
queue_namespace :cronjob
# ...
end
내부적으로 이는 SomeScheduledTaskWorker.queue를 cronjob:some_scheduled_task로 설정합니다.
일반적으로 사용되는 네임스페이스는 워커 클래스에 포함할 수 있는 자체 concern 모듈을 가지고 있으며, 큐 네임스페이스 외에 다른 Sidekiq 옵션을 설정할 수도 있습니다.
예를 들어 CronjobQueue는 네임스페이스를 설정하지만, 재시도도 비활성화합니다.
bundle exec sidekiq은 네임스페이스를 인식하며, --queue(-q) 옵션 또는 config/sidekiq_queues.yml의 :queues: 섹션에서 단순 큐 이름 대신 네임스페이스가 제공되면 해당 네임스페이스의 모든 큐(기술적으로는 네임스페이스 이름으로 시작하는 모든 큐)를 수신합니다.
기존 네임스페이스에 워커를 추가할 때는 주의해야 합니다. 네임스페이스를 처리하는 Sidekiq 프로세스에 사용 가능한 리소스가 적절히 조정되지 않으면, 추가 job이 이미 있던 워커들의 job에서 리소스를 빼앗아 갈 수 있습니다.
버전 관리(Versioning)#
버전은 각 Sidekiq 워커 클래스에 지정할 수 있습니다. 지정된 버전은 job 생성 시 함께 전송됩니다.
class FooWorker
include ApplicationWorker
version 2
def perform(*args)
if job_version == 2
foo = args.first['foo']
else
foo = args.first
end
end
end
이 스키마에서 모든 워커는 이전 버전의 워커가 큐에 넣은 job을 처리할 수 있어야 합니다.
즉, 워커가 받는 인수를 변경할 때는 version을 증가시켜야 하며(또는 워커의 인수가 처음 변경되는 경우 version 1로 설정), 워커가 이전 버전의 인수로 큐에 넣은 job도 처리할 수 있어야 합니다.
워커의 perform 메서드에서 job 버전에 따라 분기하려면 self.job_version을 읽거나, 제공된 인수의 수 또는 유형을 읽을 수 있습니다.
Job 크기#
GitLab은 Sidekiq job과 해당 인수를 Redis에 저장합니다. 과도한 메모리 사용을 피하기 위해, 원래 크기가 100 KB보다 큰 경우 Sidekiq job의 인수를 압축합니다.
압축 후에도 크기가 5 MB를 초과하면, job 예약 시 ExceedLimitError 오류가 발생합니다.
이 경우, Sidekiq에서 데이터를 사용할 수 있도록 다른 방법을 사용하세요. 가능한 해결 방법은 다음과 같습니다:
-
데이터베이스나 다른 곳에서 데이터를 로드하여 Sidekiq에서 데이터를 재구성합니다.
-
job을 예약하기 전에 객체 스토리지에 데이터를 저장하고, job 내부에서 검색합니다.
Job 가중치(weights)#
일부 job에는 가중치가 선언되어 있습니다.
이는 기본 실행 모드에서 Sidekiq을 실행할 때만 사용됩니다. sidekiq-cluster를 사용하면 가중치가 고려되지 않습니다.
Free에서 sidekiq-cluster를 사용하는 방향으로 이동하고 있으므로, 새로 추가되는 워커는 가중치를 지정할 필요가 없습니다.
기본 가중치인 1을 사용하면 됩니다.
Job 파라미터#
Sidekiq의 권장 모범 사례에 따라, 파라미터는 작고 단순해야 합니다.
워커 파라미터로 전달되는 해시의 경우, 키는 문자열이어야 하고 값은 네이티브 JSON 타입이어야 합니다. Sidekiq 버전 7.0 이후에서 이러한 기대값이 충족되지 않으면 예외가 발생합니다. 이 버전으로 업그레이드하기 위해 개발 및 테스트 모드에서는 예외 대신 경고만 표시하도록 비활성화했습니다.
앞으로 개발자는 워커 파라미터의 키와 값이 네이티브 JSON 타입인지 확인해야 합니다.
워커 파라미터를 생성하는 코드에 대한 테스트를 추가하는 것을 권장합니다.
예를 들어, 다음 커스텀 RSpec 매처 param_containing_valid_native_json_types(SidekiqJSONMatcher에 정의됨)는 해시 배열로 예상되는 파라미터를 테스트합니다:
it 'passes a valid JSON parameter to MyWorker#perform_async' do
expect(MyWorker).to receive(:perform_async).with(param_containing_valid_native_json_types)
method_calling_worker_perform_sync
end
파라미터 순서#
워커가 여러 파라미터를 받는 경우, 코드베이스 전체에서 가독성과 유지보수성을 향상시키기 위해 일관된 순서 규칙을 따르세요.
파라미터를 다음 순서로 배치하세요:
-
최상위 리소스 식별자 (예:
project_id,namespace_id,group_id) -
사용자 식별자 (예:
user_id,current_user_id) -
하위 리소스 식별자 (예:
merge_request_id,issue_id,pipeline_id) -
선택적 구성 해시 (예:
params = {},options = {})
파라미터가 많거나 자주 변경될 수 있는 워커의 경우, 핵심이 아닌 파라미터는 별도의 위치 인수 대신 구성 해시 안에 배치하는 것을 고려하세요. 이렇게 하면 핵심 식별자에 대한 순서 규칙을 유지하면서 향후 변경에 대한 유연성을 제공합니다.
좋은 예: 핵심 식별자는 위치 파라미터로, 추가 파라미터는 해시에
class CreatePipelineWorker
def perform(project_id, user_id, merge_request_id, params = {})
# ...
ref = params['ref']
source = params['source']
# ...
end
end
class ProcessCommitWorker
def perform(project_id, user_id, params = {})
commit_hash = params['commit_hash']
# ...
end
end
나쁜 예: 일관성 없는 순서
# Don't put non-core parameters as positional args before core identifiers
class ProcessCommitWorker
def perform(commit_hash, ref, project_id, user_id)
# Harder to scan and less flexible
end
end
# Don't alternate between user and resource IDs
class CreatePipelineWorker
def perform(user_id, project_id, ref, source, merge_request_id)
# Inconsistent ordering
end
end
이 순서를 사용하는 이유는?
-
일관성: 기존 워커의 대다수가 이 패턴을 따르므로 코드베이스의 예측 가능성이 높아집니다.
-
가독성: 개발자가 첫 번째 파라미터를 보고 job이 어떤 리소스에서 동작하는지 빠르게 파악할 수 있습니다.
-
컨텍스트 흐름: 리소스 → 사용자 → 액션 순서는 "무엇이 수정되고, 누가, 어떻게?"라는 자연스러운 질문 흐름을 따릅니다.
-
유연성: 핵심이 아닌 파라미터에 해시 파라미터를 사용하면 호환성을 깨지 않고 새 파라미터를 추가할 수 있습니다.
이 가이드라인은 새 워커에만 적용됩니다. 파라미터 순서나 구조를 변경하면 이미 큐에 있는 job과의 호환성이 깨질 수 있으므로, 기존 워커를 이 형식에 맞게 리팩토링하지 마세요. 자세한 내용은 업데이트 간 Sidekiq 호환성을 참고하세요.
테스트#
각 Sidekiq 워커는 다른 클래스와 마찬가지로 RSpec을 사용하여 테스트해야 합니다.
이러한 테스트는 spec/workers에 배치해야 합니다.
Sidekiq Redis 및 API와의 상호작용#
애플리케이션은 Sidekiq.redis 및 Sidekiq API와의 상호작용을 최소화해야 합니다.
일반 애플리케이션 로직에서의 이러한 상호작용은 팀 간 재사용을 위해 Sidekiq 미들웨어로 추상화해야 합니다.
애플리케이션 로직을 Sidekiq 데이터 저장소에서 분리함으로써 GitLab 백그라운드 처리 설정을 수평 확장할 때 더 많은 자유를 얻을 수 있습니다.
이 규칙의 예외로는 마이그레이션 관련 로직이나 관리 작업이 있습니다.
Job 지속 시간 제한#
일반적으로 Sidekiq job은 짧은 시간 동안 실행되는 것이 모범 사례입니다.
job 지속 시간에 대한 특정 하드 제한은 없지만, 장시간 실행 job에는 두 가지 특별한 고려 사항이 있습니다:
-
urgency속성 임계값을 초과하는 job 지속 시간은 Sidekiq Apdex에 부정적인 영향을 미치고 오류 예산에 영향을 줄 수 있습니다. -
배포가 장시간 실행 job을 중단시킵니다. GitLab.com에서는 하루에 여러 번 배포가 발생할 수 있어, job이 실행될 수 있는 최대 시간을 효과적으로 제한할 수 있습니다.
배포가 job 지속 시간에 미치는 영향#
배포 중에 Sidekiq은 TERM 신호를 받습니다.
job에는 완료하기 위한 25초의 유예 시간이 주어지며, 이후 강제로 중단됩니다.
25초 유예 기간은 Sidekiq 기본값이지만 차트를 통해 구성할 수 있습니다.
job이 일정 횟수(기본값 3회, max_retries_after_interruption으로 구성 가능) 강제 중단되면 영구적으로 종료됩니다.
이는 sidekiq-reliable-fetch 젬을 통해 처리됩니다.
이는 job이 실행될 수 있는 시간을 효과적으로 max_retries_after_interruption 횟수의 배포 기간, 즉 기본값으로 3번의 배포로 제한합니다.
장시간 실행 job 처리 팁#
하나의 큰 job 대신 많은 작은 job으로 나누는 것이 좋습니다.
워커를 분할하고 병렬화해야 하는지 결정하려면 로그에서 job의 런타임을 확인할 수 있습니다. job 지속 시간의 99번째 백분위수가 구성된 긴급도를 기준으로 해당 샤드의 타깃보다 낮다면, job을 분할할 필요가 없습니다.
장시간 실행 job을 많은 작은 job으로 분할할 때는 다운스트림 의존성을 고려해야 합니다. 예를 들어, 수천 개의 job이 모두 기본 데이터베이스에 써야 한다면, 기본 데이터베이스 연결에 경합이 생겨 샤드의 다른 Sidekiq job들이 연결을 얻기 위해 기다려야 할 수 있습니다. 이를 방지하기 위해 동시성 제한 지정을 고려할 수 있습니다.