왜 큐 대기시간이 문제가 되는가

CI 파이프라인의 병목은 대부분 잡 실행 시간이 아니라 큐에서 러너를 기다리는 시간이다. 러너 풀은 유한한데, 아침 배포 직전이나 리베이스가 몰리는 시간대에 잡이 폭증하면 급한 핫픽스 검증이 대규모 나이틀리 잡 뒤에 줄을 선다. FIFO 큐에서는 "먼저 온 순서"만 존중되므로, 30분짜리 회귀 테스트가 2분짜리 배포 게이트를 막는 역전이 일상적으로 발생한다.

이 대기시간은 지표에 잘 안 잡힌다. 잡 자체는 3분에 끝났지만 큐에서 20분을 기다렸다면 개발자 체감 리드타임은 23분이다. 먼저 queue time과 run time을 분리해 측정해야 개선 대상이 보인다.

대기시간부터 측정한다

GitLab이나 Jenkins는 잡별 타임스탬프를 API로 제공한다. 큐잉 시간을 P50/P95로 뽑아 우선순위 정책의 효과를 검증한다.

import requests, statistics

def queue_delays(project_id, token):
    r = requests.get(
        f"https://gitlab.example.com/api/v4/projects/{project_id}/jobs",
        headers={"PRIVATE-TOKEN": token}, params={"per_page": 100})
    delays = []
    for j in r.json():
        if j.get("started_at") and j.get("queued_duration") is not None:
            delays.append(j["queued_duration"])  # 초 단위
    delays.sort()
    p95 = delays[int(len(delays) * 0.95)]
    print(f"P50={statistics.median(delays):.1f}s  P95={p95:.1f}s  n={len(delays)}")

우선순위 정책 설계

모든 잡을 똑같이 취급하면 안 된다. 잡을 지연 민감도로 계층화한다. 배포 게이트와 PR 필수 체크는 사람이 기다리므로 최우선, 나이틀리·스케줄 잡은 지연돼도 무방하다.

클래스예시우선순위선점 여부
interactivePR 필수 체크, 배포 게이트높음선점 가능
batch나이틀리, 커버리지 리포트낮음선점 대상
best-effort실험적 매트릭스 잡최저즉시 양보

러너 태그와 우선순위 큐 구성

GitLab CI는 잡 단위 정수 우선순위와 러너 태그로 계층을 구현한다. 낮은 우선순위 잡은 별도 러너 그룹으로 격리해 긴급 잡의 러너를 잠식하지 못하게 한다.

deploy-gate:
  stage: verify
  tags: [fast-pool]     # 인터랙티브 전용 러너
  priority: 100         # 값이 클수록 먼저 스케줄
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

nightly-regression:
  stage: test
  tags: [batch-pool]    # 별도 풀로 격리
  priority: 10
  interruptible: true   # 상위 잡 도착 시 취소 허용
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'

interruptible: true는 새 파이프라인이 오면 중복 잡을 자동 취소해 러너를 회수한다. 배치 잡에 지정하면 사실상 소프트 선점 효과를 얻는다.

동적 러너 오토스케일링과의 결합

우선순위만으로는 총량 부족을 못 푼다. 큐 길이 기반 오토스케일링을 병행하되, 인터랙티브 풀은 여유 있게, 배치 풀은 스팟 인스턴스로 나눠 비용과 지연을 동시에 잡는다.

# 큐 대기 잡 수를 보고 러너 증설 (docker-machine 예시)
PENDING=$(curl -s --header "PRIVATE-TOKEN: $TOK" \
  "$API/projects/$PID/jobs?scope=pending" | jq 'length')

if [ "$PENDING" -gt 5 ]; then
  # 인터랙티브 풀만 우선 증설
  gitlab-runner-scale --pool fast-pool --idle-count 3
fi

주의점

첫째, 우선순위는 기아(starvation)를 유발한다. best-effort 잡이 영원히 밀리지 않도록 대기시간에 비례해 우선순위를 올리는 에이징(aging)이나 배치 풀 최소 보장 슬롯을 둔다.

둘째, 선점·취소는 캐시 워밍이나 아티팩트 업로드를 중단시켜 오히려 재실행 비용을 키울 수 있다. interruptible은 부작용이 없는 잡에만 적용한다.

셋째, 풀을 과도하게 쪼개면 각 풀의 유휴율이 올라 전체 활용도가 떨어진다. 2~3개 클래스로 시작해 지표를 보며 조정하는 편이 안전하다.