롱 폴링
롱 폴링 (Long polling)
기본적으로 GitLab Runner는 새 CI/CD 잡이 있는지 주기적으로 GitLab 인스턴스를 폴링해요. 실제 폴링 간격은 러너 구성 파일에 설정된 check_interval과 러너 수에 따라 달라져요.
많은 러너를 처리하는 서버에서 이 폴링은 다음과 같은 성능 문제를 일으킬 수 있어요:
- 더 긴 대기 시간.
- GitLab 인스턴스의 더 높은 CPU 사용률.
이런 문제를 완화하려면 롱 폴링을 활성화해야 해요.
전제 조건:
- 관리자여야 해요.
출처: 문서
본문
롱 폴링 활성화하기
GitLab 인스턴스가 러너의 잡 요청을 새 잡이 준비될 때까지 롱 폴에서 보류하도록 구성할 수 있어요.
이렇게 하려면 GitLab Workhorse 롱 폴링 시간(apiCiLongPollingDuration)을 구성해 롱 폴링을 활성화하세요:
/etc/gitlab/gitlab.rb를 수정하세요:gitlab_workhorse['api_ci_long_polling_duration'] = "50s"- 파일을 저장하고 GitLab을 재구성하세요:
sudo gitlab-ctl reconfigure
gitlab.webservice.workhorse.extraArgs 설정으로 롱 폴링을 활성화하세요.
-
Helm 값을 내보내세요:
helm get values gitlab > gitlab_values.yaml -
gitlab_values.yaml을 수정하세요:gitlab: webservice: workhorse: extraArgs: "-apiCiLongPollingDuration 50s" -
파일을 저장하고 새 값을 적용하세요:
helm upgrade -f gitlab_values.yaml gitlab gitlab/gitlab -
docker-compose.yml을 수정하세요:version: "3.6" services: gitlab: image: 'gitlab/gitlab-ee:latest' restart: always hostname: 'gitlab.example.com' environment: GITLAB_OMNIBUS_CONFIG: | gitlab_workhorse['api_ci_long_polling_duration'] = "50s" -
파일을 저장하고 GitLab을 재시작하세요:
docker compose up -d
메트릭
롱 폴링이 활성화되면 GitLab Workhorse가 Redis PubSub 채널을 구독하고 알림을 기다려요. 잡 요청은 러너 키가 변경되거나 apiCiLongPollingDuration에 도달하면 롱 폴에서 해제돼요. 다음 Prometheus 메트릭을 모니터링할 수 있어요:
| Metric | Type | Description | Labels |
|---|---|---|---|
gitlab_workhorse_keywatcher_keywatchers |
Gauge | GitLab Workhorse가 감시 중인 키 수 | |
gitlab_workhorse_keywatcher_redis_subscriptions |
Gauge | Redis PubSub 구독 수 | |
gitlab_workhorse_keywatcher_total_messages |
Counter | GitLab Workhorse가 PubSub 채널에서 받은 총 메시지 수 | |
gitlab_workhorse_keywatcher_actions_total |
Counter | 다양한 키 와처 동작의 개수 | action |
gitlab_workhorse_keywatcher_received_bytes_total |
Counter | PubSub 채널에서 받은 총 바이트 수 |
한 사용자가 이 메트릭으로 롱 폴링 문제를 발견한 예시를 볼 수 있어요.
롱 폴링 워크플로우
이 다이어그램은 롱 폴링이 활성화되었을 때 단일 러너가 잡을 얻는 과정을 보여줘요:
%%{init: { "fontFamily": "GitLab Sans" }}%%
sequenceDiagram
accTitle: Long polling workflow
accDescr: The flow of a single runner getting a job with long polling enabled
autonumber
participant C as Runner
participant W as Workhorse
participant Redis as Redis
participant R as Rails
participant S as Sidekiq
C->>+W: POST /api/v4/jobs/request
W->>+Redis: New job for runner A?
Redis->>+W: Unknown
W->>+R: POST /api/v4/jobs/request
R->>+Redis: Runner A: last_update = X
R->>W: 204 No job, X-GitLab-Last-Update = X
W->>C: 204 No job, X-GitLab-Last-Update = X
C->>W: POST /api/v4/jobs/request, X-GitLab-Last-Update: X
W->>Redis: Notify when last_update change
Note over W: Request held in long poll
Note over S: CI job created
Note over S, Redis: Update all registered runners
S->>Redis: Runner A: last_update = Z
Redis->>W: Runner: last_update changed
Note over W: Request released from long poll
W->>Rails: POST /api/v4/jobs/request
Rails->>W: 201 Job was scheduled
W->>C: 201 Job was scheduled
1단계에서 러너가 새 잡을 요청하면 GitLab 서버에 POST 요청(/api/v4/jobs/request)을 보내는데, 이것은 먼저 Workhorse가 처리해요.
Workhorse는 러너 토큰과 X-GitLab-Last-Update HTTP 헤더의 값을 읽고 키를 구성한 뒤, 그 키로 Redis PubSub 채널을 구독해요. 해당 키에 값이 없으면 Workhorse는 즉시 요청을 Rails로 전달해요(3, 4단계).
Rails는 잡 큐를 확인해요. 러너가 사용할 수 있는 잡이 없으면 Rails는 러너에게 last_update 토큰과 함께 204 No job을 반환해요(5~7단계).
러너는 그 last_update 토큰을 사용해 새 잡을 요청하면서 X-GitLab-Last-Update HTTP 헤더에 이 토큰을 채워 넣어요. 이번에는 Workhorse가 러너의 last_update 토큰이 변경되었는지 확인해요. 변경되지 않았다면 Workhorse는 apiCiLongPollingDuration에 지정된 시간까지 요청을 보류해요.
사용자가 새 파이프라인이나 잡 실행을 트리거하면, Sidekiq의 백그라운드 작업이 해당 잡에 사용 가능한 모든 러너의 last_update 값을 업데이트해요. 러너는 프로젝트, 그룹, 인스턴스 또는 이들의 조합으로 등록될 수 있어요.
10, 11단계의 이 "tick(틱)"은 Workhorse 롱 폴 큐에서 잡 요청을 해제하고, 요청은 Rails로 전송돼요(12단계). Rails는 사용 가능한 잡을 찾아 그 잡에 러너를 할당해요(13, 14단계).
롱 폴링을 사용하면 러너는 새 잡이 준비되는 즉시 알림을 받아요. 이는 잡 대기 시간을 줄일 뿐 아니라, 새 작업이 있을 때만 잡 요청이 Rails에 도달하므로 서버 오버헤드도 줄여줘요.
문제 해결
롱 폴링을 사용할 때 다음 문제가 발생할 수 있어요.
느린 잡 획득
러너 구성에 따라 러너가 잡을 제때 가져오지 못할 수 있기 때문에 롱 폴링은 기본적으로 활성화되어 있지 않아요. issue 27709을 참고하세요.
이는 러너 config.toml의 concurrent 설정이 정의된 러너 수보다 낮게 설정된 경우 발생할 수 있어요. 이 문제를 해결하려면 concurrent 값이 러너 수와 같거나 크게 만드세요.
예를 들어 config.toml에 [[runners]] 항목이 세 개 있다면, concurrent를 최소 3으로 설정하세요.
롱 폴링이 활성화되면 러너는:
concurrent수만큼 Goroutine을 실행해요.- 롱 폴링 후 Goroutine이 반환되기를 기다려요.
- 또 다른 배치의 요청을 실행해요.
예를 들어 단일 config.toml이 다음과 같이 구성된 경우를 생각해보세요:
- 프로젝트 A용 러너 3개.
- 프로젝트 B용 러너 1개.
concurrent는 3으로 설정.
이 예시에서 러너는 처음 3개 프로젝트에 대한 Goroutine을 실행해요. 최악의 경우 러너는 프로젝트 A에 대해 전체 롱 폴 간격을 기다린 뒤에야 프로젝트 B의 잡을 요청해요.
더 알아보기
다음으로는 GitLab Runner의 check_interval 작동 방식과 러너 등록 문서를 함께 보면, 러너 폴링 전략을 러너 수와 맞춰 최적화하는 방법을 익힐 수 있어요.