GitHub 가져오기 개발자 문서

GitHub 가져오기는 Sidekiq를 사용하는 병렬 가져오기 도구입니다.

준비 사항

  • github_importergithub_importer_advance_stage 큐를 처리하는 Sidekiq 워커(기본적으로 활성화됨).
  • GitHub API와 상호 작용하는 데 사용되는 Octokit.

코드 구조

가져오기의 코드베이스는 다음 디렉터리로 나뉘어집니다.

  • lib/gitlab/github_import: 이 디렉터리에는 자원을 가져오는 데 사용되는 클래스와 같은 대부분의 코드가 포함되어 있습니다.
  • app/workers/gitlab/github_import: 이 디렉터리에는 Sidekiq 워커가 포함되어 있습니다.
  • app/workers/concerns/gitlab/github_import: 이 디렉터리에는 여러 Sidekiq 워커에서 재사용되는 몇 가지 모듈이 포함되어 있습니다.

아키텍처 개요

GitHub 프로젝트를 가져올 때 작업은 별도의 단계로 나뉘며, 각 단계는 실행되는 일련의 Sidekiq 작업으로 구성됩니다. 각 단계 사이에는 현재 단계의 모든 작업이 완료되었는지 주기적으로 확인하는 작업이 예약되어, 이러한 경우 가져오기 프로세스가 다음 단계로 진행됩니다. 이 작업을 처리하는 워커는 Gitlab::GithubImport::AdvanceStageWorker라고 합니다.

단계

1. RepositoryImportWorker

이 워커는 Projects::ImportService.new.execute를 호출하며, 이는 importer.execute를 호출합니다.

이 문맥에서 importerGitlab::ImportSources.importer(project.import_type)의 인스턴스이며, github 가져오기 유형에 대해 ParallelImporter로 매핑됩니다.

ParallelImporter는 다음 워커를 예약합니다.

2. Stage::ImportRepositoryWorker

이 워커는 리포지터리 및 위키를 가져오고, 완료되면 다음 단계를 예약합니다.

3. Stage::ImportBaseDataWorker

이 워커는 레이블, 마일스톤 및 릴리스와 같은 기본 데이터를 가져옵니다. 이 작업은 병렬로 수행될 수 있을 만큼 빠르게 수행될 수 있기 때문에 단일 스레드에서 수행됩니다.

4. Stage::ImportPullRequestsWorker

이 워커는 모든 풀 리퀘스트를 가져옵니다. 각 풀 리퀘스트마다 Gitlab::GithubImport::ImportPullRequestWorker 워커를 위한 작업을 예약합니다.

5. Stage::ImportCollaboratorsWorker

이 워커는 외부 협력자가 아닌 리포지터리의 직접적인 협력자만 가져옵니다. 각 협력자에 대해 Gitlab::GithubImport::ImportCollaboratorWorker 워커를 위한 작업을 예약합니다.

note
이 단계는 선택 사항입니다(Gitlab::GithubImport::Settings로 제어) 및 기본적으로 선택됩니다.

6. Stage::ImportIssuesAndDiffNotesWorker

이 워커는 모든 이슈 및 풀 리퀘스트 코멘트를 가져옵니다. 각 이슈에 대해 Gitlab::GithubImport::ImportIssueWorker 워커를 위한 작업을 예약하고, 풀 리퀘스트 코멘트의 경우 대신 Gitlab::GithubImport::DiffNoteImporter 워커를 위한 작업을 예약합니다.

이 워커는 이슈 및 diff 코멘트를 병렬로 처리하기 때문에 별도의 단계를 예약하고 이전 작업의 완료를 기다릴 필요가 없습니다.

이슈는 풀 리퀘스트와 별도로 가져오기 때문에 “이슈” API에만 이슈 및 풀 리퀘스트의 레이블이 포함되어 있습니다. 이슈를 가져오고 레이블 링크를 설정하는 것은 별도의 API 데이터 크롤링이 필요하지 않으며, 프로젝트를 가져오는 데 필요한 API 호출 횟수를 줄일 수 있도록 합니다.

7. Stage::ImportIssueEventsWorker

이 워커는 모든 이슈 및 풀 리퀘스트 이벤트를 가져옵니다. 각 이벤트에 대해 Gitlab::GithubImport::ImportIssueEventWorker 워커를 위한 작업을 예약합니다.

GitHub API의 특정 측면 때문에 이슈 및 풀 리퀘스트 이벤트를 단일 단계로 가져올 수 있습니다. GitHub에는 보이지만 사실 같은 테이블에 저장된 이슈 및 풀 리퀘스트가 있습니다. 따라서 전역적으로 고유한 ID가 있으므로:

  • 모든 풀 리퀘스트는 이슈입니다.
  • 이슈는 풀 리퀘스트가 아닙니다.

따라서 이슈 및 풀 리퀘스트는 대부분의 관련된 항목을 위한 공통 API를 가지고 있습니다.

타임라인 이벤트 엔드포인트를 사용하여 풀 리퀘스트 리뷰 요청을 가져오기 위해 이벤트는 순차적으로 처리되어야 합니다. 가져오기 워커는 보장된 순서로 실행되지 않기 때문에 풀 리퀘스트 리뷰 요청 이벤트는 초기에 Redis 정렬 디렉터리에 배치됩니다. 후에 Gitlab::GithubImport::ReplayEventsWorker에 의해 순차적으로 소비됩니다.

8. Stage::ImportAttachmentsWorker

이 워커는 마크다운 내에서 링크된 노트 첨부 파일을 가져옵니다. 프로젝트의 마크다운 텍스트마다 다음 작업을 예약합니다:

  • 각 릴리스마다 Gitlab::GithubImport::Importer::Attachments::ReleasesImporter.
  • 각 노트마다 Gitlab::GithubImport::Importer::Attachments::NotesImporter.
  • 각 이슈마다 Gitlab::GithubImport::Importer::Attachments::IssuesImporter.
  • 각 Merge Request마다 Gitlab::GithubImport::Importer::Attachments::MergeRequestsImporter.

각 작업:

  1. 해당 레코드 내의 모든 첨부 링크를 반복합니다.
  2. 첨부 파일을 다운로드합니다.
  3. 이전 링크를 GitLab로 새로 생성된 링크로 대체합니다.
note
이는 선택 사항으로 추가적인 가져오기 시간을 필요로 할 수 있습니다(Gitlab::GithubImport::Settings로 제어됨).

9. Stage::ImportProtectedBranchesWorker

이 워커는 보호된 브랜치 규칙을 가져옵니다. GitHub에 있는 각 규칙에 대해 Gitlab::GithubImport::ImportProtectedBranchWorker 작업을 예약합니다.

각 작업은 GitHub와 GitLab의 브랜치 보호 규칙을 비교하고 GitLab 브랜치에 가장 엄격한 규칙을 적용합니다.

10. Stage::FinishImportWorker

이 워커는 일부 하우스키피핑(캐시 플러싱과 같은)을 수행하고 가져오기가 완료되었다고 표시함으로써 가져오기 프로세스를 완료합니다.

스테이지 진행

스테이지 진행은 두 가지 방법 중 하나로 수행됩니다.

  • 직접 다음 스테이지를 위한 워커를 예약합니다.
  • Gitlab::GithubImport::AdvanceStageWorker 작업을 예약하여 현재 스테이지의 모든 작업이 완료되면 스테이지를 진행합니다.

첫 번째 방법은 작업이 단일 스레드에서 모든 작업을 수행하는 워커에만 사용해야 하며, AdvanceStageWorker는 그 외 모든 작업에 사용해야 합니다.

첫 번째 방법의 예는 ImportBaseDataWorkerPullRequestWorker직접 호출하는 방식입니다.

두 번째 방법의 예는 PullRequestsWorker가 자신의 작업이 완료된 경우 AdvanceStageWorker를 호출하는 방식입니다.

작업을 예약할 때, AdvanceStageWorker에는 프로젝트 ID, Redis 키 디렉터리 및 다음 스테이지의 이름이 주어집니다. Redis 키( Gitlab::JobWaiter에 의해 생성됨)는 현재 진행 중인 스테이지가 완료되었는지 여부를 확인하는 데 사용됩니다. 스테이지가 아직 완료되지 않았다면, AdvanceStageWorker는 자신을 다시 예약합니다. 스테이지가 완료되면 또는 마지막 호출 이후 더 많은 작업이 완료되면, AdvanceStageworker는 가져오기 JID를 갱신하고 다음 스테이지의 워커를 예약합니다.

AdvanceStageWorker 작업이 예약되는 횟수를 줄이기 위해 이 워커는 다음 작업이 무엇이어야 하는 지 결정하기 전에 짧은 시간 동안 작업이 완료될 때까지 기다립니다. 작은 프로젝트의 경우, 이로 인해 가져오기 프로세스가 약간 느려질 수 있지만, 전체 시스템에 대한 압력을 줄일 수 있습니다.

가져오기 작업 ID 갱신

GitLab에는 주기적으로 실행되어 프로젝트 가져오기가 24시간 이상 갱신되지 않은 경우 실패로 표시되는 Gitlab::Import::StuckProjectImportJobsWorker라는 워커가 포함되어 있습니다. GitHub 프로젝트의 경우, 프로젝트 가져오기에는 GitHub 속도 제한에 따라 여러 일이 소요될 수 있지만, 이로 인해 Gitlab::Import::StuckProjectImportJobsWorker가 이를 실패로 표시하지 않도록 하려 합니다.

이를 방지하기 위해 우리는 가져오기의 만료 시간을 주기적으로 갱신합니다. 현재 작업자의 jid를 사용하여 이 JID TTL을 가져오는 작업인 ProjectImportState#refresh_jid_expiration을 호출하거나 RefreshImportJidWorker를 사용합니다. 이 TTL을 갱신함으로써 우리는 아직 작업을 수행 중인 한 가져오기가 실패로 표시되지 않도록 할 수 있습니다.

GitHub 속도 제한

GitHub는 시간당 5,000개의 API 호출 속도 제한을 가지고 있습니다. 프로젝트 가져오기에 필요한 요청 수는 대부분 프로젝트에 참여한 고유한 사용자 수에 따라 결정됩니다(예: 이슈 작성자) 이유는 사용자의 이메일 주소를 가져와서 그들을 GitLab 사용자와 매핑해야하기 때문입니다. 다른 데이터(예: 이슈 페이지 및 코멘트)는 일반적으로 몇십 개의 요청만 필요로 합니다.

우리는 속도 제한을 다음과 같은 방법으로 처리합니다:

  1. 속도 제한에 도달한 후, 속도 제한이 재설정될 때까지 작업이 실행되지 않도록 자동으로 작업을 재예약합니다.
  2. 우리는 GitHub 사용자를 GitLab 사용자로 매핑하는 캐시를 Redis에 저장합니다.

사용자 캐싱에 대한 자세한 정보는 아래에서 찾을 수 있습니다.

사용자 조회 캐싱

GitHub 사용자를 GitLab 사용자로 매핑할 때, (최악의 경우) 다음을 수행해야합니다:

  1. 유저의 이메일 주소를 가져오기 위한 하나의 API 호출.
  2. 해당하는 GitLab 사용자가 있는지 확인하기 위한 두 개의 데이터베이스 쿼리. 하나는 GitHub 사용자 ID를 기반으로 사용자를 찾으려고 하고, 다른 하나는 GitHub 이메일 주소를 사용하여 사용자를 찾으려고 합니다.

불일치를 피하기 위해, GitHub 사용자 ID로 검색은 GitHub Enterprise에서 가져올 때 수행되지 않습니다.

이 프로세스가 꽤 비용이 많이 드는 작업이기 때문에 우리는 이러한 조회 결과를 Redis에 캐시합니다. 조회한 각 사용자에 대해 다섯 가지 키를 저장합니다:

  • GitHub 사용자 이름을 그들의 이메일 주소로 매핑하는 Redis 키.
  • GitHub 이메일 주소를 GitLab 사용자 ID로 매핑하는 Redis 키.
  • GitHub 사용자 ID를 GitLab 사용자 ID로 매핑하는 Redis 키.
  • GitHub 사용자 이름을 ETAG 헤더로 매핑하는 Redis 키.
  • 프로젝트의 이메일 조회를 수행했는지를 나타내는 Redis 키.

우리는 두 가지 유형의 조회를 캐시합니다:

  • 긍정적 조회, 즉 GitLab 사용자 ID를 찾은 경우.
  • 부정적 조회, 즉 GitLab 사용자가 존재하지 않는 것으로 알고있는 사용자에 대해 작업을 수행하지 않도록 캐싱합니다.

이러한 키의 만료 시간은 24시간입니다. 긍정적 조회 캐시를 검색할 때, 만료 시간을 자동으로 갱신합니다. 거짓 조회의 TTL은 절대로 갱신되지 않습니다.

이메일 조회를 위한 조회가 비어 있거나 부정적 조회를 반환하는 경우, 캐시된 ETAG와 함께 조건적으로 요청이 만들어집니다. 각 프로젝트에 대해 조건적 요청은 GitHub API 속도 제한에 포함되지 않습니다.

이 캐싱 레이어로 인해, 새롭게 등록된 GitLab 계정은 대응하는 GitHub 계정에 연결되지 않을 수 있습니다. 그러나, 이는 캐시 키가 만료되거나 새 프로젝트가 가져오기되면 해결됩니다.

사용자 캐시 조회는 프로젝트 전체에서 공유됩니다. 이는 가져오기가 필요한 GitHub API 호출이 적을수록 가져 오는 프로젝트가 더 많을수록 작동합니다.

이 코드는 다음 위치에 있습니다:

  • lib/gitlab/github_import/user_finder.rb
  • lib/gitlab/github_import/caching.rb

Sidekiq 작업 중단 증가

Sidekiq 프로세스가 종료될 때 실행 중인 작업이 완료될 때까지 기다린 후 중단합니다. 중단은 작업을 종료하고 다시 대기열에 넣습니다. 우리의 vendored sidekiq-reliable-fetcher gem은 작업이 영구적으로 종료되기 전에 3번까지 중단 한도를 둡니다. 중단된 작업은 Kibana에 json.interrupted_count를 기록합니다.

이 한도는 Sidekiq 재시작 사이에 작업이 완료될 수 없는 작업으로부터 보호를 제공합니다.

큰 가져오기에 대해서는 GitHub stages 워커( Stage::로 네임스페이스 지정됨)가 많은 시간이 소요됩니다. 기본적으로 이러한 가져오기가 완료되기 전에 sidekiq-reliable-fetcher가 작업을 영구적으로 중단하기 때문에 가져오기가 실패할 위험이 있습니다.

재시작 시에서 작업을 재개하는 스테이지 워커는 다음을 호출하여 sidekiq-reliable-fetcher의 중단 제한을 20으로 높일 수 있습니다:

module Gitlab
  module GithubImport
    module Stage
      class MyWorker
        resumes_work_when_interrupted!
        
        # ...
      end
    end
  end
end

재시작 시에 작업을 완전히 재개하지 않는 스테이지 워커는 이 메서드를 호출해서는 안됩니다. 예를 들어, 이미 가져온 객체를 건너 뛰지만 매번 처음부터 루프를 시작하는 워커입니다.

재시작 시에 전체 작업을 완전히 재개하는 스테이지 워커의 예는 다음과 같습니다:

sidekiq_options dead: false

일반적으로 워커의 재시도가 고갈되면 Sidekiq 데드 세트로 이동하여 인스턴스 관리자가 다시 시도할 수 있습니다.

GithubImport::Queue는 GitHub 가져오기 워커에 대해 이러한 상황이 발생하지 않도록 Sidekiq 워커 옵션 dead: false를 설정합니다.

그 이유는 다음과 같습니다:

  • 데드 세트에는 최대 한계가 있으며 객체 가져오기 워커(여기에는 ObjectImporter가 포함된 워커)가 대량으로 실패하게 되면 데드 세트를 스팸으로 만들어 다른 워커들을 밀어낼 수 있습니다.
  • 스테이지 워커(여기에는 StageMethods가 포함된 워커)는 재시도가 고갈되면 가져오기를 실패합니다 따라서 재시도는 무효화될 것이 보장됩니다.

라벨 및 마일스톤 매핑

데이터베이스에 압력을 줄이기 위해 이슈와 Merge Request에 라벨 및 마일스톤을 설정할 때 쿼리하지 않습니다. 대신, 라벨 및 마일스톤을 가져올 때 이 데이터를 캐시에 저장한 후, 이를 이슈/Merge Request에 할당할 때 재사용합니다. 사용자 조회와 유사하게, 해당 캐시 키는 사용되지 않은 후 24시간이 경과하면 자동으로 만료됩니다.

이미 사용자 조회 캐시와는 달리, 라벨 및 마일스톤 캐시는 가져오고 있는 프로젝트에 대해 범위가 지정됩니다.

해당 코드는 다음 위치에 있습니다:

  • lib/gitlab/github_import/label_finder.rb
  • lib/gitlab/github_import/milestone_finder.rb
  • lib/gitlab/cache/import/caching.rb

로그

가져오기 진행 상황은 logs/importer.log 파일에서 확인할 수 있습니다. 각 관련 가져오기는 "import_type": "github""project_id"로 로그에 기록됩니다.

마지막 로그 항목은 가져와서 가져온 객체의 수를 보고합니다:

{
  "message": "GitHub project import finished",
  "duration_s": 347.25,
  "objects_imported": {
    "fetched": {
      "diff_note": 93,
      "issue": 321,
      "note": 794,
      "pull_request": 108,
      "pull_request_merged_by": 92,
      "pull_request_review": 81
    },
    "imported": {
      "diff_note": 93,
      "issue": 321,
      "note": 794,
      "pull_request": 108,
      "pull_request_merged_by": 92,
      "pull_request_review": 81
    }
  },
  "import_source": "github",
  "project_id": 47,
  "import_stage": "Gitlab::GithubImport::Stage::FinishImportWorker"
}

메트릭 대시보드

GitHub 가져오기의 상태를 평가하기 위해 GitHub 가져오기 대시보드는 시간 경과에 따른 가져온 총 객체 수에 대한 정보를 제공합니다.