Sidekiq worker 속성
GitLab v19.1Worker 클래스는 특정 속성을 정의하여 동작을 제어하고 메타데이터를 추가할 수 있습니다. 다른 worker를 상속하는 자식 클래스도 이러한 속성을 상속하므로, 값을 재정의하려는 경우에만 다시 정의하면 됩니다. Job에는 urgency 속성을 설정할 수 있으며, 값은 :high, :low, :throttled 중 하나입니다.
Worker 클래스는 특정 속성을 정의하여 동작을 제어하고 메타데이터를 추가할 수 있습니다.
다른 worker를 상속하는 자식 클래스도 이러한 속성을 상속하므로, 값을 재정의하려는 경우에만 다시 정의하면 됩니다.
Job 긴급도#
Job에는 urgency 속성을 설정할 수 있으며, 값은 :high, :low, :throttled 중 하나입니다. 각 값의 목표는 아래와 같습니다:
| 긴급도 | 큐 스케줄링 목표 | 실행 지연 요건 |
|---|---|---|
| :high | 10초 | 10초 |
| :low (기본값) | 1분 | 5분 |
| :throttled | 없음 | 5분 |
job의 긴급도를 설정하려면 urgency 클래스 메서드를 사용하세요:
class HighUrgencyWorker
include ApplicationWorker
urgency :high
# ...
end
지연에 민감한 job#
대량의 백그라운드 job이 한꺼번에 스케줄링되면, worker 노드가 사용 가능해질 때까지 job이 큐에 대기하는 상황이 발생할 수 있습니다. 이는 정상적인 현상으로, 트래픽 급증을 우아하게 처리함으로써 시스템에 복원력을 부여합니다. 그러나 일부 job은 다른 job보다 지연에 더 민감합니다.
일반적으로 지연에 민감한 job은 사용자가 백그라운드 worker에서 비동기적으로 처리되는 것이 아닌, 동기적으로 발생할 것이라고 합리적으로 기대할 수 있는 작업을 수행합니다. 일반적인 예로는 어떤 작업 이후의 쓰기 작업이 있습니다. 이러한 job의 예는 다음과 같습니다:
-
브랜치에 푸시한 후 머지 리퀘스트를 업데이트하는 job.
-
브랜치에 푸시한 후 프로젝트의 알려진 브랜치 캐시를 무효화하는 job.
-
권한 변경 후 사용자가 볼 수 있는 그룹 및 프로젝트를 재계산하는 job.
-
파이프라인의 job 상태가 변경된 후 CI 파이프라인의 상태를 업데이트하는 job.
이러한 job이 지연되면 사용자는 그 지연을 버그로 인식할 수 있습니다. 예를 들어, 브랜치를 푸시한 후 해당 브랜치에 대한 머지 리퀘스트를 생성하려 할 때 UI에서 해당 브랜치가 존재하지 않는다고 표시될 수 있습니다. 이러한 job을 urgency :high로 분류합니다.
이러한 job이 스케줄링된 후 매우 짧은 시간 내에 시작될 수 있도록 추가적인 노력이 기울여집니다. 그러나 처리량을 보장하기 위해 이러한 job에는 매우 엄격한 실행 시간 요건이 있습니다:
-
중간값 job 실행 시간은 1초 미만이어야 합니다.
-
99%의 job은 10초 내에 완료되어야 합니다.
worker가 이러한 기대치를 충족할 수 없다면, urgency :high worker로 처리할 수 없습니다. worker를 재설계하거나, 빠르게 실행되는 urgency :high 코드와 실행 지연 요건이 없지만 스케줄링 목표가 낮은 urgency :low 코드를 가진 두 개의 서로 다른 worker로 작업을 분리하는 것을 고려하세요.
큐의 긴급도 변경#
GitLab.com에서는 Sidekiq을 여러 개의 샤드로 운영하며, 각 샤드는 특정 유형의 워크로드를 나타냅니다.
큐의 긴급도를 변경하거나 새 큐를 추가할 때는 새 샤드에서 예상되는 워크로드를 고려해야 합니다. 기존 큐를 변경하는 경우 기존 샤드에도 영향이 있지만, 그 영향은 항상 작업량을 감소시킵니다.
이를 위해 새 샤드의 총 실행 시간과 RPS(처리량)의 예상 증가량을 계산해야 합니다. 이 값은 다음에서 확인할 수 있습니다:
-
Queue Detail 대시보드에는 큐 자체에 대한 값이 있습니다. 새 큐의 경우 유사한 패턴을 가지거나 유사한 상황에서 스케줄링되는 큐를 찾을 수 있습니다.
-
Shard Detail 대시보드에는 총 실행 시간과 처리량(RPS)이 있습니다. Shard Utilization 패널에는 이 샤드에 현재 여유 용량이 있는지 표시됩니다.
그런 다음 변경하려는 큐에 대해 RPS * 평균 런타임(새 job에 대한 추정치)을 계산하여 새 샤드에서 예상되는 RPS 및 실행 시간의 상대적 증가량을 확인할 수 있습니다:
new_queue_consumption = queue_rps * queue_duration_avg
shard_consumption = shard_rps * shard_duration_avg
(new_queue_consumption / shard_consumption) * 100
5% 미만의 증가가 예상된다면 추가 조치는 필요하지 않습니다.
그렇지 않은 경우, 머지 리퀘스트에 @gitlab-com/gl-infra/data-access/durability를 멘션하고 리뷰를 요청하세요.
외부 의존성이 있는 Job#
GitLab 애플리케이션의 대부분의 백그라운드 job은 다른 GitLab 서비스와 통신합니다. 예를 들어 PostgreSQL, Redis, Gitaly, Object Storage가 있습니다. 이것들은 job의 "내부" 의존성으로 간주됩니다.
그러나 일부 job은 성공적으로 완료하기 위해 외부 서비스에 의존합니다. 예는 다음과 같습니다:
-
사용자가 구성한 웹훅을 호출하는 job.
-
사용자가 구성한 쿠버네티스 클러스터에 애플리케이션을 배포하는 job.
이러한 job에는 "외부 의존성"이 있습니다. 이는 백그라운드 처리 클러스터 운영에 여러 면에서 중요합니다:
-
대부분의 외부 의존성(예: 웹훅)은 SLO를 제공하지 않으므로 이러한 job의 실행 지연을 보장할 수 없습니다. 실행 지연을 보장할 수 없으므로 처리량을 보장할 수 없으며, 따라서 트래픽이 높은 환경에서는 외부 의존성이 있는 job을 고긴급 job과 분리하여 해당 큐의 처리량을 보장해야 합니다.
-
외부 의존성이 있는 job의 오류는 오류의 원인이 외부에 있을 가능성이 있으므로 알림 임계값이 더 높습니다.
class ExternalDependencyWorker
include ApplicationWorker
# Declares that this worker depends on
# third-party, external services in order
# to complete successfully
worker_has_external_dependencies!
# ...
end
job은 고긴급도이면서 동시에 외부 의존성을 가질 수 없습니다.
CPU 바운드 및 메모리 바운드 Worker#
CPU 또는 메모리 리소스 제한에 의해 제약을 받는 worker는 worker_resource_boundary 메서드로 표시해야 합니다.
대부분의 worker는 Redis, PostgreSQL, Gitaly와 같은 다른 서비스로부터 네트워크 응답을 기다리며 대부분의 시간을 블로킹 상태로 보냅니다. Sidekiq은 멀티스레드 환경이므로, 이러한 job은 높은 동시성으로 스케줄링될 수 있습니다.
그러나 일부 worker는 Ruby에서 로직을 실행하는 CPU 상에서 많은 시간을 소비합니다. Ruby MRI는 진정한 멀티스레딩을 지원하지 않으며, GIL에 의존하여 프로세스를 호스팅하는 머신에 코어가 몇 개 있든 상관없이 한 번에 하나의 Ruby 코드 섹션만 실행되도록 함으로써 애플리케이션 개발을 크게 단순화합니다. IO 바운드 worker의 경우 대부분의 스레드가 GIL 외부에 있는 하위 라이브러리에서 블로킹되므로 이는 문제가 되지 않습니다.
많은 스레드가 동시에 Ruby 코드를 실행하려 하면 GIL에 대한 경합이 발생하여 모든 프로세스가 느려지는 효과가 나타납니다.
트래픽이 높은 환경에서는 worker가 CPU 바운드라는 것을 알면 더 낮은 동시성을 가진 다른 플릿에서 실행할 수 있습니다. 이를 통해 최적의 성능을 보장합니다.
마찬가지로, worker가 많은 양의 메모리를 사용하는 경우 맞춤형 저동시성, 고메모리 플릿에서 실행할 수 있습니다.
메모리 바운드 worker는 10-50ms의 일시 정지와 함께 GC 워크로드가 무거워집니다. 이는 worker의 지연 요건에 영향을 미칩니다. 이러한 이유로, memory 바운드 urgency :high job은 허용되지 않으며 CI에서 실패합니다. 일반적으로 memory 바운드 worker는 권장되지 않으며, 작업을 처리하는 대안적인 접근 방식을 고려해야 합니다.
worker가 많은 양의 메모리와 CPU 시간 모두 필요한 경우, 고긴급 메모리 바운드 worker에 대한 위의 제한으로 인해 메모리 바운드로 표시해야 합니다.
CPU 바운드로 Job 선언하기#
이 예시는 job을 CPU 바운드로 선언하는 방법을 보여줍니다.
class CPUIntensiveWorker
include ApplicationWorker
# Declares that this worker will perform a lot of
# calculations on-CPU.
worker_resource_boundary :cpu
# ...
end
worker가 CPU 바운드인지 판별하기#
다음 접근 방식을 사용하여 worker가 CPU 바운드인지 판별합니다:
-
Sidekiq 구조화된 JSON 로그에서 worker의
duration및cpu_s필드를 집계합니다. -
duration은 총 job 실행 시간(초)을 나타냅니다. -
cpu_s는Process::CLOCK_THREAD_CPUTIME_ID카운터에서 파생되며, job이 CPU 상에서 소비한 시간을 나타냅니다. -
cpu_s를duration으로 나누어 CPU에서 소비된 시간의 비율을 계산합니다. -
이 비율이 33%를 초과하면 해당 worker는 CPU 바운드로 간주되어 그에 맞게 표시해야 합니다.
-
이 값은 소수의 샘플이 아닌 상당히 큰 집계에서 사용해야 합니다.
기능 카테고리#
모든 Sidekiq worker는 알려진 기능 카테고리를 정의해야 합니다.
Job 데이터 일관성 전략#
GitLab 13.11 이전에는 Sidekiq worker가 항상 읽기와 쓰기 모두 기본 데이터베이스 노드로 데이터베이스 쿼리를 전송했습니다. 이를 통해 데이터 무결성이 보장되고 즉각적으로 유지되었는데, 단일 노드 시나리오에서는 자신의 쓰기를 읽는 worker에서도 오래된 읽기를 만나는 것이 불가능하기 때문입니다. 그러나 worker가 기본 노드에 쓰고 레플리카에서 읽는 경우, 레플리카가 기본 노드보다 뒤처질 수 있으므로 오래된 레코드를 읽을 가능성이 0이 아닙니다.
데이터베이스에 의존하는 job의 수가 증가하면, 즉각적인 데이터 일관성을 보장하는 것이 기본 데이터베이스 서버에 지속 불가능한 부하를 줄 수 있습니다. 따라서 Sidekiq worker를 위한 데이터베이스 로드 밸런싱을 사용할 수 있는 기능을 추가했습니다.
worker의 data_consistency 필드를 구성하면 아래에 설명된 여러 전략에 따라 스케줄러가 읽기 레플리카를 타깃으로 삼을 수 있습니다.
즉시성을 줄이고 기본 노드 부하를 낮추기#
Sidekiq worker가 모든 읽기 및 쓰기에 기본 데이터베이스 노드를 사용해야 하는지, 아니면 읽기를 레플리카에서 제공할 수 있는지에 대해 명시적인 결정을 내리도록 요구합니다. 이는 data_consistency 필드가 설정되도록 보장하는 RuboCop 규칙으로 강제됩니다.
data_consistency가 도입되기 전에 기본 동작은 :always를 모방했습니다. 이제 job이 현재 데이터베이스 LSN과 함께 큐에 추가되므로, (:sticky 또는 :delayed의 경우) 레플리카가 해당 시점까지 따라잡는 것이 보장되거나, job이 재시도되거나, 기본 노드를 사용합니다. 이는 데이터가 최소한 job이 큐에 추가된 시점까지 일관성이 유지됨을 의미합니다.
아래 표는 data_consistency 속성과 그 값을 읽기 레플리카 선호도와 레플리카 따라잡기 대기 정도 순으로 보여줍니다:
| 데이터 일관성 | 설명 | 가이드라인 |
|---|---|---|
| :always | job이 모든 쿼리에 기본 데이터베이스를 사용해야 합니다. | 강력히 권장되지 않습니다. 기본 스티키니스와 관련된 엣지 케이스가 발생하는 job에만 필요합니다. |
| :sticky | job이 레플리카를 선호하지만, 쓰기 또는 복제 지연이 발생하면 기본 노드로 전환합니다. | 이것이 선호되는 옵션입니다. 가능한 한 빠르게 실행해야 하는 job에 사용해야 합니다. 레플리카는 Sidekiq에 job이 큐에 추가된 시점까지 따라잡는 것이 보장됩니다. |
| :delayed | job이 레플리카를 선호하지만, 쓰기 시 기본 노드로 전환합니다. job 시작 전에 복제 지연이 발생하면 job을 한 번 재시도합니다. 다음 재시도에서도 레플리카가 최신 상태가 아닌 경우 기본 노드로 전환합니다. | 캐시 만료 또는 웹훅 실행과 같이 실행을 추가로 지연시키는 것이 일반적으로 문제가 되지 않는 job에 사용해야 합니다. 크론 job과 같이 재시도가 비활성화된 job에는 사용하지 않아야 합니다. |
모든 경우에 worker는 완전히 따라잡은 레플리카 또는 기본 노드에서 읽으므로, 데이터 일관성은 항상 보장됩니다.
worker의 데이터 일관성을 설정하려면 data_consistency 클래스 메서드를 사용하세요:
class DelayedWorker
include ApplicationWorker
data_consistency :delayed
# ...
end
분해된 데이터베이스에 대한 데이터 일관성 재정의#
GitLab은 여러 분해된 데이터베이스를 사용합니다. Sidekiq worker의 각 데이터베이스 사용이 특정 데이터베이스 쪽으로 치우칠 수 있습니다. 예를 들어, PipelineProcessWorker는 main 데이터베이스보다 ci 데이터베이스에 대한 쓰기 트래픽이 더 높습니다. 기본 스티키니스와 관련된 엣지 케이스가 발생할 때, 각 데이터베이스에 대해 별도의 데이터 일관성을 정의하면 worker가 읽기 레플리카를 더 효율적으로 사용할 수 있습니다.
overrides 키워드 인수가 설정되면, Gitlab::Database::LoadBalancing::SidekiqServerMiddleware는 읽기 레플리카를 가장 선호하는 데이터 일관성을 사용하여 로드 밸런싱 전략을 로드합니다.
선호도가 증가하는 순서는 :always, :sticky, :delayed입니다.
재정의는 GitLab 인스턴스가 여러 데이터베이스를 사용하거나 Gitlab::Database.database_mode == Gitlab::Database::MODE_MULTIPLE_DATABASES인 경우에만 적용됩니다.
worker의 데이터 일관성을 설정하려면 overrides 키워드 인수와 함께 data_consistency 클래스 메서드를 사용하세요:
class MultipleDataConsistencyWorker
include ApplicationWorker
data_consistency :always, overrides: { ci: :sticky }
# ...
end
feature_flag 속성#
feature_flag 속성을 사용하면 job의 data_consistency를 토글할 수 있으며, 특정 job에 대한 로드 밸런싱 기능을 안전하게 토글할 수 있습니다.
feature_flag가 비활성화되면 job은 기본적으로 :always로 설정되며, 이는 job이 항상 기본 데이터베이스를 사용함을 의미합니다.
feature_flag 속성은 액터 기반 기능 게이트 사용을 허용하지 않습니다.
이는 기능 플래그를 특정 프로젝트, 그룹 또는 사용자에 대해서만 토글할 수 없음을 의미하며, 대신 시간 비율 롤아웃을 안전하게 사용할 수 있습니다.
Sidekiq 클라이언트와 서버 모두에서 기능 플래그를 확인하므로, 10%의 시간 롤아웃은 실제로 1%(0.1 [클라이언트에서]*0.1 [서버에서])의 job이 레플리카를 사용하게 됩니다.
예시:
class DelayedWorker
include ApplicationWorker
data_consistency :delayed, feature_flag: :load_balancing_for_delayed_worker
# ...
end
overrides와 함께 feature_flag 속성을 사용할 경우, job은 모든 데이터베이스 연결에 대해 기본적으로 always로 설정됩니다.
기능 플래그가 활성화되면 구성된 데이터 일관성이 각 데이터베이스에 독립적으로 적용됩니다.
아래 예시에서 플래그가 활성화되면, main 데이터베이스 연결은 :always 데이터 일관성을 사용하고 ci 데이터베이스 연결은 :sticky 데이터 일관성을 사용합니다.
class DelayedWorker
include ApplicationWorker
data_consistency :always, overrides: { ci: :sticky }, feature_flag: :load_balancing_for_delayed_worker
# ...
end
멱등성 job과 데이터 일관성#
:sticky 또는 :delayed 데이터 일관성을 선언하는 멱등성 job의 경우, 중복 제거 시 최신 WAL 위치를 보존하여 완전히 따라잡은 레플리카에서 읽을 수 있도록 합니다.
Job 일시 정지 제어#
pause_control 속성을 사용하면 job 처리를 조건부로 일시 정지할 수 있습니다. 전략이 활성화되면 job은 별도의 ZSET에 저장되고 전략이 비활성화될 때 다시 큐에 추가됩니다. PauseControl::ResumeWorker는 일시 정지된 job을 재시작해야 하는지 확인하는 크론 worker입니다.
pause_control을 사용하려면 다음 중 하나를 선택하세요:
-
lib/gitlab/sidekiq_middleware/pause_control/strategies/에 정의된 전략 중 하나를 사용합니다. -
lib/gitlab/sidekiq_middleware/pause_control/strategies/에 커스텀 전략을 정의하고lib/gitlab/sidekiq_middleware/pause_control.rb에 해당 전략을 추가합니다.
예시:
module Gitlab
module SidekiqMiddleware
module PauseControl
module Strategies
class CustomStrategy < Base
def should_pause?
ApplicationSetting.current.elasticsearch_pause_indexing?
end
end
end
end
end
end
class PausedWorker
include ApplicationWorker
pause_control :custom_strategy
# ...
end
worker의 미들웨어를 제거하려면 전략을 :deprecated로 설정하여 비활성화하고, 완전히 제거하기 전에 필수 중단을 기다리세요. 이렇게 하면 모든 일시 정지된 job이 올바르게 재개됩니다.
동시성 제한#
concurrency_limit 속성을 사용하면 worker의 동시성을 제한할 수 있습니다. 이 제한을 초과하는 job은 별도의 LIST에 저장되고 제한 이하로 떨어질 때 다시 큐에 추가됩니다. ConcurrencyLimit::ResumeWorker는 스로틀된 job을 다시 큐에 추가해야 하는지 확인하는 크론 worker입니다.
ops 기능 플래그 concurrency_limit_eager_resume_processing을 활성화하면 ConcurrencyLimit::ResumeWorker의 job 재개 처리량을 높일 수 있습니다. 그러나 이 경우 Sidekiq 큐에 백로그가 있을 때 재개된 job이 동시성 제한을 초과할 위험이 있습니다.
자세한 내용은 이슈 579350을 참조하세요.
정의된 동시성 제한을 처음 초과하는 job이 해당 클래스의 다른 모든 job에 대한 스로틀링 프로세스를 시작합니다. 이 시점까지는 job이 평소대로 스케줄링되고 실행됩니다.
스로틀링이 시작되면 새로 스케줄링되고 실행되는 job이 LIST의 끝에 추가되어 실행 순서가 보존됩니다. LIST가 다시 비워지면 스로틀링 프로세스가 종료됩니다.
동시성 제한 미들웨어를 사용하는 worker를 모니터링하기 위한 Prometheus 메트릭이 노출됩니다:
-
sidekiq_concurrency_limit_deferred_jobs_total -
sidekiq_concurrency_limit_queue_jobs -
sidekiq_concurrency_limit_queue_jobs_total -
sidekiq_concurrency_limit_max_concurrent_jobs -
sidekiq_concurrency_limit_current_concurrent_jobs_total
제한을 지속적으로 초과하는 워크로드가 있으면 제한이 비활성화되거나 워크로드가 제한 이하로 떨어질 때까지 LIST가 계속 증가합니다.
제한을 정의하기 위해 람다를 사용해야 합니다. nil 또는 0을 반환하면 제한이 적용되지 않습니다.
음수는 실행을 일시 정지합니다.
class LimitedWorker
include ApplicationWorker
concurrency_limit -> { 60 }
# ...
end
class LimitedWorker
include ApplicationWorker
concurrency_limit -> { ApplicationSetting.current.elasticsearch_concurrent_sidekiq_jobs }
# ...
end
기본 동시성 제한#
GitLab.com에서는 Sidekiq 샤드의 용량(threads * maxReplicas * maxPercentage)을 기반으로 계산된 모든 worker에 대한 기본 동시성 제한을 적용했습니다.
-
threads: 샤드에 구성된concurrency수.catchall샤드 예시. -
maxReplicas: 샤드에 구성된 최대 레플리카 수.catchall샤드 예시. -
maxPercentage: worker의 긴급도를 기반으로 한 비율. 구성.
이는 정적 concurrency_limit 속성이 설정되지 않은 경우에만 적용됩니다.
기본 maxPercentage를 재정의하려면 max_concurrency_limit_percentage 속성을 정의할 수 있습니다:
class LimitedWorker
include ApplicationWorker
max_concurrency_limit_percentage 0.5 # This will use 50% of the shard's maximum capacity.
# ...
end
Geo 보조 사이트에서 worker 실행 건너뛰기#
Geo 보조 사이트에서는 데이터베이스 쓰기가 비활성화됩니다.
Geo 보조 사이트에서 데이터베이스 쓰기를 시도하는 worker의 실행을 건너뛰어야 하며, 해당 worker가 Geo 보조 사이트에서 큐에 추가되는 경우에 해당합니다.
편리하게도 대부분의 worker는 Geo 보조 사이트에서 큐에 추가되지 않습니다. 이는 대부분의 비 GET HTTP 요청이 Geo 기본 사이트로 프록시되기 때문이며, Geo 보조 사이트에서 대부분의 Sidekiq-Cron job이 비활성화되어 있기 때문입니다.
확실하지 않은 경우 Geo 엔지니어에게 문의하세요.
실행을 건너뛰려면 worker 클래스에 ::Geo::SkipSecondary 모듈을 prepend하세요.
class DummyWorker
include ApplicationWorker
prepend ::Geo::SkipSecondary
# ...
end