관리자를 위한 작업 아티팩트 문제 해결

작업 아티팩트를 관리할 때 다음과 같은 문제에 직면할 수 있습니다.

디스크 공간이 너무 많이 사용되는 작업 아티팩트

작업 아티팩트는 예상보다 빠르게 디스크 공간을 채울 수 있습니다. 몇 가지 가능한 이유는:

이와 같은 경우에는 디스크 공간 사용량이 가장 많은 프로젝트를 확인하고, 어떤 유형의 아티팩트가 가장 많은 공간을 사용하는지 파악하며, 경우에 따라 수동으로 작업 아티팩트를 삭제하여 디스크 공간을 회수해야 합니다.

아티팩트 housekeeping

아티팩트 housekeeping은 유효 기간이 만료된 아티팩트를 식별하고 삭제할 프로세스입니다.

GitLab 15.0에서 15.2까지 사용 중지된 housekeeping

GitLab 15.0에서 아티팩트 housekeeping이 크게 개선되었으며 기본적으로 비활성화된 피쳐 플래그를 통해 도입되었습니다. 이 플래그는 기본적으로 GitLab 15.3에서 활성화되었습니다.

GitLab 15.0에서 15.2에서는 아티팩트 housekeeping이 정상적으로 작동하지 않는 것으로 보인다면 피쳐 플래그가 활성화되었는지 확인해야 합니다.

피쳐 플래그가 활성화되었는지 확인하려면 다음을 수행하세요:

  1. Rails 콘솔을 시작합니다.

  2. 피쳐 플래그가 활성화되었는지 확인합니다.

    Feature.enabled?(:ci_detect_wrongly_expired_artifacts)
    Feature.enabled?(:ci_update_unlocked_job_artifacts)
    Feature.enabled?(:ci_job_artifacts_backlog_work)
    
  3. 만약 피쳐 플래그 중 하나라도 비활성화되어 있다면, 활성화합니다:

    Feature.enable(:ci_detect_wrongly_expired_artifacts)
    Feature.enable(:ci_update_unlocked_job_artifacts)
    Feature.enable(:ci_job_artifacts_backlog_work)
    

이러한 변경 내용에는 아티팩트를 잠긴 상태에서 보관해야 한다는 것이 포함되어 있습니다. (가장 최근 성공한 작업에서 최신 아티팩트 유지 참조)

unknown 상태의 아티팩트

housekeeping이 업데이트되기 전에 생성된 아티팩트는 unknown 상태를 가집니다. 만료되면 새 housekeeping에 의해 이러한 아티팩트가 처리되지 않습니다.

데이터베이스를 확인하여 인스턴스에 unknown 상태의 아티팩트가 있는지 확인할 수 있습니다:

  1. 데이터베이스 콘솔을 시작합니다:

    리눅스 패키지 (Omnibus)
    sudo gitlab-psql
    
    헬름 차트 (쿠버네티스)
    # 툴박스 pod 찾기
    kubectl --namespace <namespace> get pods -lapp=toolbox
    # PostgreSQL 콘솔에 연결
    kubectl exec -it <toolbox-pod-name> -- /srv/gitlab/bin/rails dbconsole --include-password --database main
    
    도커
    sudo docker exec -it <container_name> /bin/bash
    gitlab-psql
    
    직접 컴파일 (소스)
    sudo -u git -H psql -d gitlabhq_production
    
  2. 다음 쿼리를 실행합니다:

    select expire_at, file_type, locked, count(*) from ci_job_artifacts
    where expire_at is not null and
    file_type != 3
    group by expire_at, file_type, locked having count(*) > 1;
    

기록이 반환된다면 housekeeping 작업이 처리할 수 없는 아티팩트가 있음을 의미합니다. 예시:

           expire_at           | file_type | locked | count
-------------------------------+-----------+--------+--------
 2021-06-21 22:00:00+00        |         1 |      2 |  73614
 2021-06-21 22:00:00+00        |         2 |      2 |  73614
 2021-06-21 22:00:00+00        |         4 |      2 |   3522
 2021-06-21 22:00:00+00        |         9 |      2 |     32
 2021-06-21 22:00:00+00        |        12 |      2 |    163

상태가 2인 아티팩트는 unknown입니다. 자세한 내용은 issue #346261를 참조하세요.

unknown 아티팩트 정리

모든 unknown 아티팩트를 처리하는 Sidekiq 워커는 GitLab 15.3 및 이후에 기본적으로 활성화되어 있습니다. 이는 위의 데이터베이스 쿼리에 의해 반환된 아티팩트를 분석하고 필요한 경우 이러한 아티팩트를 잠금 또는 잠금 해제로 처리합니다. 그런 다음 필요한 경우 워커에 의해 이를 삭제합니다.

자체 관리되는 인스턴스에서 워커를 활성화할 수 있습니다:

  1. Rails 콘솔을 시작합니다.

  2. 기능이 활성화되었는지 확인합니다.

    Feature.enabled?(:ci_job_artifacts_backlog_work)
    
  3. 필요한 경우 기능을 활성화합니다:

    Feature.enable(:ci_job_artifacts_backlog_work)
    

워커는 매 7분마다 1만 개의 unknown 아티팩트를 처리하며, 24시간 동안 대략 200만 개의 아티팩트를 처리합니다.

ci_job_artifacts_backlog_large_loop_limit 기능 플래그도 관련이 있으며 워커를 사용하여 unknown 아티팩트를 처리하고 5배 큰 일괄 처리량으로 처리합니다. 자체 관리되는 인스턴스에서 이 플래그를 사용하는 것은 권장되지 않습니다.

특정 만료 기간(또는 만료되지 않은)이 있는 프로젝트 및 빌드에 대한 목록

Rails console을 사용하여 다음과 같이 작업의 아티팩트가 있는 프로젝트를 찾을 수 있습니다.

  • 만료일이 없는 경우
  • 7일 후보다 더 먼 미래에 만료되는 경우

아티팩트를 삭제하는 것과 유사하게 다음 예제 시간 프레임을 사용하고 필요에 따라 수정하세요.

  • 7.days.from_now
  • 10.days.from_now
  • 2.weeks.from_now
  • 3.months.from_now
  • 1.year.from_now

다음 스크립트는 검색 결과를 50개로 제한하지만, 필요에 따라 이 숫자를 변경할 수 있습니다.

# 만료되지 않는 빌드 및 프로젝트의 아티팩트 찾기
builds_with_artifacts_that_never_expire = Ci::Build.with_downloadable_artifacts.where(artifacts_expire_at: nil).limit(50)
builds_with_artifacts_that_never_expire.find_each do |build|
  puts "Build with id #{build.id} has artifacts that don't expire and belongs to project #{build.project.full_path}"
end

# 오늘로부터 7일 후에 만료되는 아티팩트를 가진 빌드 및 프로젝트 찾기
builds_with_artifacts_that_expire_in_a_week = Ci::Build.with_downloadable_artifacts.where('artifacts_expire_at > ?', 7.days.from_now).limit(50)
builds_with_artifacts_that_expire_in_a_week.find_each do |build|
  puts "Build with id #{build.id} has artifacts that expire at #{build.artifacts_expire_at} and belongs to project #{build.project.full_path}"
end

작업 아티팩트 저장의 총크기별 프로젝트 목록

Rails console에서 다음 코드를 실행하여 작업 아티팩트 저장 크기별로 상위 20개 프로젝트를 나열합니다.

include ActionView::Helpers::NumberHelper
ProjectStatistics.order(build_artifacts_size: :desc).limit(20).each do |s|
  puts "#{number_to_human_size(s.build_artifacts_size)} \t #{s.project.full_path}"
end

.limit(20)을 수정하여 나열할 프로젝트 수를 변경할 수 있습니다.

단일 프로젝트에서 가장 큰 아티팩트 목록

단일 프로젝트에서 다음 코드를 실행하여 상위 50개의 가장 큰 작업 아티팩트를 나열합니다.

include ActionView::Helpers::NumberHelper
project = Project.find_by_full_path('path/to/project')
Ci::JobArtifact.where(project: project).order(size: :desc).limit(50).map { |a| puts "ID: #{a.id} - #{a.file_type}: #{number_to_human_size(a.size)}" }

.limit(50)을 수정하여 나열할 작업 아티팩트 수를 변경할 수 있습니다.

단일 프로젝트의 아티팩트 목록

아티팩트 크기 순으로 정렬된 단일 프로젝트의 아티팩트 목록입니다. 결과에는 다음이 포함됩니다.

  • 아티팩트를 생성한 작업의 ID
  • 아티팩트 크기
  • 아티팩트 파일 유형
  • 아티팩트 생성 날짜
  • 아티팩트의 디스크 상 위치
p = Project.find_by_id(<project_id>)
arts = Ci::JobArtifact.where(project: p)

list = arts.order(size: :desc).limit(50).each do |art|
    puts "Job ID: #{art.job_id} - Size: #{art.size}b - Type: #{art.file_type} - Created: #{art.created_at} - File loc: #{art.file}"
end

limit(50)의 숫자를 변경하여 나열할 작업 아티팩트 수를 변경할 수 있습니다.

이전 빌드 및 아티팩트 삭제

경고: 이러한 명령은 데이터를 데이터베이스와 저장소에서 영구적으로 제거합니다. 이러한 명령을 실행하기 전에 Support Engineer의 지침을 따르거나, 백업된 인스턴스를 복원할 준비가 된 테스트 환경에서 실행하는 것을 강력히 권장합니다.

프로젝트의 이전 아티팩트 삭제

이 단계는 사용자가 유지하기로 선택한 아티팩트도 지웁니다.

project = Project.find_by_full_path('path/to/project')
builds_with_artifacts =  project.builds.with_downloadable_artifacts
builds_with_artifacts.where("finished_at < ?", 1.year.ago).each_batch do |batch|
  batch.each do |build|
    Ci::JobArtifacts::DeleteService.new(build).execute
  end

  batch.update_all(artifacts_expire_at: Time.current)
end

GitLab 15.3 및 이전 버전에서는 다음을 사용하세요.

project = Project.find_by_full_path('path/to/project')
builds_with_artifacts =  project.builds.with_downloadable_artifacts
builds_with_artifacts.where("finished_at < ?", 1.year.ago).each_batch do |batch|
  batch.each do |build|
    build.artifacts_expire_at = Time.current
    build.erase_erasable_artifacts!
  end
end

인스턴스 전역에서 이전 아티팩트 삭제

이 단계는 사용자가 유지하기로 선택한 아티팩트도 지웁니다.

builds_with_artifacts = Ci::Build.with_downloadable_artifacts
builds_with_artifacts.where("finished_at < ?", 1.year.ago).each_batch do |batch|
  batch.each do |build|
    Ci::JobArtifacts::DeleteService.new(build).execute
  end

  batch.update_all(artifacts_expire_at: Time.current)
end

GitLab 15.3 및 이전 버전에서는 다음을 사용하세요.

builds_with_artifacts =  Ci::Build.with_downloadable_artifacts
builds_with_artifacts.where("finished_at < ?", 1.year.ago).each_batch do |batch|
  batch.each do |build|
    build.artifacts_expire_at = Time.current
    build.erase_erasable_artifacts!
  end
end

프로젝트의 이전 작업 로그 및 아티팩트 삭제

project = Project.find_by_full_path('path/to/project')
builds =  project.builds
admin_user = User.find_by(username: 'username')
builds.where("finished_at < ?", 1.year.ago).each_batch do |batch|
  batch.each do |build|
    print "Ci::Build ID #{build.id}... "

    if build.erasable?
      Ci::BuildEraseService.new(build, admin_user).execute
      puts "Erased"
    else
      puts "Skipped (Nothing to erase or not erasable)"
    end
  end
end

인스턴스 전체에서 이전 작업 로그 및 아티팩트 삭제

builds = Ci::Build.all
admin_user = User.find_by(username: 'username')
builds.where("finished_at < ?", 1.year.ago).each_batch do |batch|
  batch.each do |build|
    print "Ci::Build ID #{build.id}... "

    if build.erasable?
      Ci::BuildEraseService.new(build, admin_user).execute
      puts "Erased"
    else
      puts "Skipped (Nothing to erase or not erasable)"
    end
  end
end

GitLab 15.3 및 이전 버전에서 Ci::BuildEraseService.new(build, admin_user).executebuild.erase(erased_by: admin_user)로 바꿉니다.

1.year.ago는 Rails의 ActiveSupport::Duration 메서드입니다. 아직 사용 중인 아티팩트를 실수로 삭제하는 위험을 줄이기 위해 긴 기간으로 시작합니다. 필요에 따라 더 짧은 기간으로 삭제를 다시 실행하려면 예를 들어 3.months.ago, 2.weeks.ago, 또는 7.days.ago를 사용하세요.

erase_erasable_artifacts! 메서드는 동기적이며 실행 시 즉시 아티팩트가 제거됩니다. 백그라운드 대기열에 의해 예약되지 않습니다.

작업 아티팩트 업로드가 오류 500으로 실패하는 경우

아티팩트에 대해 객체 저장소를 사용하고 작업 아티팩트가 업로드에 실패한 경우, 다음 사항을 검토하세요:

  • 오류 메시지가 있는 작업 로그에 대해:

    WARNING: Uploading artifacts as "archive" to coordinator... failed id=12345 responseStatus=500 Internal Server Error status=500 token=abcd1234
    
  • 다음과 비슷한 오류 메시지에 대해 workhorse 로그를 확인하세요:

    {"error":"MissingRegion: could not find region configuration","level":"error","msg":"error uploading S3 session","time":"2021-03-16T22:10:55-04:00"}
    

두 경우 모두 작업 아티팩트에 대한 객체 저장소 구성에 region을 추가해야 할 수 있습니다.

500 Internal Server Error (Missing file)로 작업 아티팩트 업로드가 실패하는 경우

폴더 경로를 포함하는 버킷 이름은 통합된 객체 저장소에서 지원되지 않습니다. 예를 들어 bucket/path와 같이 폴더 경로가 포함된 버킷 이름이 있는 경우 500 Internal Server Error (Missing file)와 유사한 오류가 발생할 수 있습니다.

다음과 같이 통합된 객체 저장소를 사용할 때 위의 오류로 작업 아티팩트 업로드가 실패하는 경우, 각 데이터 유형에 대해 별도의 버킷을 사용했는지 확인하세요.

Windows 마운트 사용 시 FATAL: invalid argument로 작업 아티팩트 업로드가 실패하는 경우

CIFS와 Windows 마운트를 사용하여 작업 아티팩트를 사용하는 경우, 러너가 아티팩트를 업로드하려고 할 때 invalid argument 오류가 발생할 수 있습니다:

WARNING: Uploading artifacts as "dotenv" to coordinator... POST https://<your-gitlab-instance>/api/v4/jobs/<JOB_ID>/artifacts: 500 Internal Server Error  id=1296 responseStatus=500 Internal Server Error status=500 token=*****
FATAL: invalid argument

이 문제를 해결하려면 다음을 시도할 수 있습니다:

  • CIFS 대신 ext4 마운트로 전환합니다.
  • CIFS 파일 임대와 관련된 중요한 버그 수정 사항이 포함된 적어도 Linux 커널 5.15로 업그레이드합니다.
  • 더 오래된 커널의 경우 파일 임대를 비활성화하기 위해 nolease 마운트 옵션을 사용합니다.

자세한 정보는 조사 세부 정보를 참조하세요.

사용량 할당량이 잘못된 아티팩트 저장소 사용량을 표시

가끔 아티팩트 저장소 사용량이 아티팩트에 의해 사용된 총 저장 공간에 대해 잘못된 값을 표시합니다. 인스턴스의 모든 프로젝트에 대해 아티팩트 저장 공간 사용량을 다시 계산하려면 다음 백그라운드 스크립트를 실행할 수 있습니다:

gitlab-rake gitlab:refresh_project_statistics_build_artifacts_size[https://example.com/path/file.csv]

https://example.com/path/file.csv 파일에는 다음과 같은 형식으로 다시 계산하려는 모든 프로젝트의 프로젝트 ID가 나열되어 있어야 합니다: csv PROJECT_ID 1 2

스크립트가 실행 중일 때 아티팩트 사용량 값은 0으로 변동할 수 있습니다. 다시 계산한 후, 사용량은 다시 예상대로 표시되어야 합니다.

아티팩트 다운로드 흐름 다이어그램

다음 플로우 다이어그램은 작업 아티팩트의 작동 방식을 설명합니다. 이 다이어그램은 객체 저장소가 작업 아티팩트에 구성된 것으로 가정합니다.

프록시 다운로드 비활성화

proxy_downloadfalse로 설정하면 GitLab 은 러너에게 객체 저장소에서 사전 서명된 URL을 사용하여 아티팩트를 다운로드하도록 리디렉트합니다. 러너가 원본에서 직접 가져오는 것이 일반적으로 더 빠르므로 이러한 구성을 일반적으로 권장합니다. 그리고 데이터가 GitLab로 가져와진 후에 러너로 보내지는것보다 대역폭 사용을 줄여야 합니다. 그러나 이는 러너에게 직접적인 객체 저장소 액세스가 필요합니다.

요청 흐름은 다음과 같습니다:

sequenceDiagram autonumber participant C as Runner participant O as Object Storage participant W as Workhorse participant R as Rails participant P as PostgreSQL C->>+W: GET /api/v4/jobs/:id/artifacts?direct_download=true Note over C,W: gitlab-ci-token@<CI_JOB_TOKEN> W-->+R: GET /api/v4/jobs/:id/artifacts?direct_download=true Note over W,R: gitlab-ci-token@<CI_JOB_TOKEN> R->>P: CI_JOB_TOKEN에 대한 작업 조회 R->>P: 작업을 시작한 사용자를 찾음 R->>R: 해당 사용자에 대한 :read_build 액세스가 있는지 확인 alt Yes R->>W: 객체 저장소 사전 서명된 URL로 302 리디렉트를 보냄 R->>C: 302 리디렉트 C->>O: <사전 서명된 URL>에서 GET else No R->>W: 401 Unauthorized W->>C: 401 Unauthorized end

이 다이어그램에서:

  1. 러너는 먼저 GET /api/v4/jobs/:id/artifacts 엔드포인트를 사용하여 작업 아티팩트를 가져오려고 시도합니다. 러너는 객체 저장소에서 직접 다운로드할 수 있음을 나타내기 위해 첫 번째 시도에서 direct_download=true 쿼리 매개변수를 추가합니다. 직접 다운로드는 러너 설정에서 FF_USE_DIRECT_DOWNLOAD 기능 플래그를 통해 비활성화할 수 있습니다. 이 플래그는 기본적으로 true로 설정됩니다.

  2. 러너는 자동 생성된 CI/CD 작업 토큰을 비밀번호로 사용하여 gitlab-ci-token 사용자와 함께 HTTP 기본 인증을 사용하여 GET 요청을 보냅니다. 이 토큰은 GitLab에서 러너에게 제공됩니다.

  3. GET 요청은 GitLab API로 전달되어 토큰을 데이터베이스에서 찾아 작업을 시작한 사용자를 찾습니다.

  4. 5-8 단계에서:

    • 사용자가 빌드에 액세스 할 수 있는 경우, GitLab은 사전 서명된 URL을 생성하고 해당 URL로 Location을 설정한 302 리디렉트를 보냅니다. 러너는 302 리디렉트를 따라 아티팩트를 다운로드합니다.

    • 작업을 찾을 수 없거나 사용자가 작업에 액세스 권한이 없는 경우 API는 401 Unauthorized을 반환합니다.

    러너는 다음과 같은 HTTP 상태 코드를 받으면 재시도하지 않습니다:

    • 200 OK
    • 401 Unauthorized
    • 403 Forbidden
    • 404 Not Found

    그러나 러너가 500과 같은 기타 상태 코드를 받으면 1초씩 재우며 다시 2회 시도합니다. 이후의 재시도에서는 direct_download=true를 생략합니다. ```

프록시 다운로드 활성화

만약 proxy_downloadtrue이면, GitLab은 항상 러너에 데이터를 전송하기 위해 artifact를 객체 저장소에서 가져옵니다. 심지어 러너가 direct_download=true 쿼리 매개변수를 보내더라도입니다. 프록시 다운로드는 러너가 제한된 네트워크 액세스를 갖고 있는 경우에 유용할 수 있습니다.

다음 다이어그램은 프록시 다운로드가 비활성화된 예제와 유사하지만, 단계 6-9에서 GitLab은 러너에게 302 Redirect를 보내지 않습니다. 대신에 GitLab은 Workhorse에게 데이터를 가져오도록 지시하고, 그것을 러너에게 스트리밍합니다. 러너의 관점에서, /api/v4/jobs/:id/artifacts에 대한 원래의 GET 요청은 이진 데이터를 직접 반환합니다.

sequenceDiagram autonumber participant C as Runner participant O as Object Storage participant W as Workhorse participant R as Rails participant P as PostgreSQL C->>+W: GET /api/v4/jobs/:id/artifacts?direct_download=true Note over C,W: gitlab-ci-token@<CI_JOB_TOKEN> W-->+R: GET /api/v4/jobs/:id/artifacts?direct_download=true Note over W,R: gitlab-ci-token@<CI_JOB_TOKEN> R->>P: CI_JOB_TOKEN에 대한 작업 조회 R->>P: 작업을 시작한 사용자 찾기 R->>R: 사용자가 :read_build 액세스를 갖고 있는가? alt Yes R->>W: 객체 저장소 사전 서명된 URL과 함께 URL 보내기 W->>O: <사전 서명된 URL로 GET> O->>W: <artifact 데이터> W->>C: <artifacts 데이터> else No R->>W: 401 Unauthorized W->>C: 401 Unauthorized end