GitHub 가져오기 개발자 문서

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

필수 조건

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

코드 구조

가져오기 도구의 코드베이스는 다음과 같은 디렉토리로 나뉩니다:

  • 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

이 작업자는 모든 Pull Request를 가져옵니다. 각 Pull Request에 대해 Gitlab::GithubImport::ImportPullRequestWorker 작업자를 위한 작업이 예약됩니다.

5. Stage::ImportCollaboratorsWorker

이 작업자는 외부 협력자가 아닌 직접 리포지토리 협력자만 가져옵니다. 각 협력자에 대해 Gitlab::GithubImport::ImportCollaboratorWorker 작업자를 위한 작업이 예약됩니다.

note
이 단계는 선택 사항이며(Gitlab::GithubImport::Settings에 의해 제어됨) 기본적으로 선택됩니다.

6. Stage::ImportIssuesAndDiffNotesWorker

이 작업자는 모든 문제와 Pull Request 코멘트를 가져옵니다. 각 문제에 대해 Gitlab::GithubImport::ImportIssueWorker 작업자를 위한 작업이 예약됩니다. Pull Request 코멘트의 경우, 대신 Gitlab::GithubImport::DiffNoteImporter 작업자를 위한 작업이 예약됩니다.

이 작업자는 문제와 Diff 노트를 병렬로 처리하므로 별도의 단계를 예약하고 이전 단계가 완료될 때까지 기다릴 필요가 없습니다.

문제는 Pull Request와 별도로 가져옵니다. 왜냐하면 “문제” API만이 문제와 Pull Request 모두에 대한 레이블을 포함하기 때문입니다. 문제를 가져오고 동일한 작업자에서 레이블 링크를 설정함으로써 API 데이터를 통해 별도로 크롤링할 필요성을 줄이고 프로젝트를 가져오기 위해 필요한 API 호출 수를 줄일 수 있습니다.

7. Stage::ImportIssueEventsWorker

이 작업자는 모든 문제와 Pull Request 이벤트를 가져옵니다. 각 이벤트에 대해 Gitlab::GithubImport::ImportIssueEventWorker 작업자를 위한 작업이 예약됩니다.

특정 GitHub API의 특성 덕분에 문제와 Pull Request 이벤트를 단일 단계로 가져올 수 있습니다. 내부적으로 GitHub의 문제와 Pull Request는 하나의 테이블에 저장되는 것처럼 보입니다. 따라서 전역적으로 고유한 ID가 있습니다:

  • 각 Pull Request는 문제입니다.
  • 문제는 Pull Request가 아닙니다.

따라서 대부분의 관련 사항에 대해 문제와 Pull Request는 공통 API를 가지고 있습니다.

Pull Request Review Requests를 타임라인 이벤트 엔드포인트를 사용하여 가져오기 위해서는 이벤트를 순차적으로 처리해야 합니다. 가져오기 작업자가 보장된 순서로 실행되지 않기 때문에 Pull Request Review Requests 이벤트는 처음에 Redis 정렬 목록에 배치됩니다. 이후 Gitlab::GithubImport::ReplayEventsWorker에 의해 순차적으로 소비됩니다.

8. Stage::ImportAttachmentsWorker

이 작업자는 Markdown 내에 링크된 노트 첨부 파일을 가져옵니다. 프로젝트 내의 Markdown 텍스트가 있는 각 항목에 대해 다음 작업을 예약합니다:

  • 릴리스마다 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에는 Gitlab::Import::StuckProjectImportJobsWorker라는 워커가 포함되어 있어, 정기적으로 실행되고 24시간 이상 새로 고치지 않은 프로젝트 가져오기를 실패로 마크합니다. GitHub 프로젝트의 경우, 이는 약간의 문제를 일으킬 수 있습니다: 대규모 프로젝트의 가져오기는 GitHub 비율 한도에 도달하는 빈도에 따라 며칠이 걸릴 수 있지만, Gitlab::Import::StuckProjectImportJobsWorker가 이러한 이유로 우리의 가져오기를 실패로 마크하게 하고 싶지는 않습니다.

이런 일이 발생하지 않도록, 우리는 주기적으로 가져오기 만료 시간을 새로 고칩니다. 이는 가져오기 작업의 JID를 데이터베이스에 저장한 다음, 가져오기 프로세스의 다양한 단계에서 이 JID TTL을 새로 고침으로써 작동합니다. 이는 ProjectImportState#refresh_jid_expiration을 호출하거나 RefreshImportJidWorker를 사용하여 현재 워커의 jid를 전달함으로써 수행됩니다.

이 TTL을 새로 고침함으로써, 작업을 계속 수행하는 한 우리의 가져오기 작업이 실패로 표시되는 것을 방지할 수 있습니다.

GitHub 속도 제한

GitHub는 시간당 5,000 API 호출의 속도 제한을 가지고 있습니다. 프로젝트를 가져오는 데 필요한 요청의 수는 주로 프로젝트에 관련된 고유 사용자 수에 의해 지배됩니다(예: 이슈 작성자). 사용자를 GitLab 사용자와 매핑하기 위해 사용자의 이메일 주소가 필요하기 때문입니다. 이슈 페이지 및 댓글과 같은 다른 데이터는 일반적으로 가져오기에 몇십 개의 요청만 필요합니다.

우리는 속도 제한을 다음과 같이 처리합니다:

  1. 속도 제한에 도달한 후, 우리는 자동으로 작업을 재일정하여 속도 제한이 재설정될 때까지 실행되지 않도록 합니다.

  2. GitHub 사용자를 GitLab 사용자로 매핑하는 작업을 Redis에 캐시합니다.

사용자 캐싱에 대한 추가 정보는 아래에서 확인할 수 있습니다.

사용자 조회 캐싱

GitHub 사용자를 GitLab 사용자로 매핑할 때 (최악의 경우)에 수행해야 할 작업은 다음과 같습니다:

  1. 사용자의 이메일 주소를 가져오기 위한 API 호출 1회.
  2. 해당 GitLab 사용자가 존재하는지 확인하기 위한 데이터베이스 쿼리 2회. 하나의 쿼리는 GitHub 사용자 ID를 기반으로 사용자를 찾으려고 하며, 두 번째 쿼리는 GitHub 이메일 주소를 사용하여 사용자를 찾는 데 사용됩니다.

사용자 불일치를 피하기 위해 GitHub Enterprise에서 가져올 때는 GitHub 사용자 ID에 의한 검색을 수행하지 않습니다.

이 과정은 상당히 비용이 많이 들기 때문에 이러한 조회 결과를 Redis에 캐시합니다. 조회된 각 사용자에 대해 우리는 다섯 개의 키를 저장합니다:

  • GitHub 사용자 이름을 이메일 주소에 매핑하는 Redis 키.
  • GitHub 이메일 주소를 GitLab 사용자 ID에 매핑하는 Redis 키.
  • GitHub 사용자 ID를 GitLab 사용자 ID에 매핑하는 Redis 키.
  • GitHub 사용자 이름을 ETAG 헤더에 매핑하는 Redis 키.
  • 프로젝트에 대한 이메일 조회가 수행되었는지 여부를 나타내는 Redis 키.

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

  • 긍정적인 조회, 즉 GitLab 사용자 ID를 찾은 경우.
  • 부정적인 조회, 즉 GitLab 사용자 ID를 찾지 못한 경우. 이 캐싱은 우리가 GitLab 데이터베이스에 존재하지 않는 사용자를 위해 동일한 작업을 수행하는 것을 방지합니다.

이 키들의 만료 시간은 24시간입니다. 긍정적인 조회의 캐시를 검색할 때 TTL을 자동으로 갱신합니다. 부정적 조회의 TTL은 결코 갱신되지 않습니다.

이메일 조회가 비어 있거나 부정적인 조회를 반환할 경우, 조건부 요청이 각 프로젝트에 대해 한 번 캐시된 ETAG를 헤더에 담아 수행됩니다. 조건부 요청은 GitHub API 속도 제한에 포함되지 않습니다.

이 캐싱 계층 때문에 새로 등록된 GitLab 계정이 해당 GitHub 계정과 연결되지 않을 수 있습니다. 그러나 이는 캐시된 키가 만료되거나 새로운 프로젝트가 가져오면 해결됩니다.

사용자 캐시 조회는 프로젝트 간에 공유됩니다. 이는 가져오는 프로젝트 수가 많을수록 필요한 GitHub API 호출 수가 적다는 것을 의미합니다.

이 코드의 위치는 다음과 같습니다:

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

Sidekiq 인터럽트 증가

Sidekiq 프로세스가 종료되면 실행 중인 작업이 완료될 때까지 일정 시간 기다린 후 중단시킵니다. 인터럽트는 작업을 종료하고 다시 큐에 추가합니다. 우리의 벤더드 sidekiq-reliable-fetcher gem은 작업이 더 이상 재큐에 추가되지 않고 영구적으로 종료되기 전에 3회의 인터럽트를 제한합니다. 인터럽트된 작업은 Kibana에 json.interrupted_count로 기록됩니다.

이 제한은 Sidekiq가 다시 시작될 때 작업이 완료될 수 없는 경우에 대한 보호를 제공합니다.

대규모 가져오기를 위해 우리의 GitHub stage 작업자는 완료하는 데 여러 시간이 걸립니다. 기본적으로 sidekiq-reliable-fetcher가 이러한 작업자를 영구적으로 정지시켜 가져오기가 실패할 위험이 있습니다.

다시 시작할 때 중단된 지점에서 작업을 재개하는 스테이지 작업자는 .resumes_work_when_interrupted!를 호출하여 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를 포함하는 작업자)는 가져오기를 실패합니다 재시도가 소진되면, 따라서 재시도는 반드시 no-op이 될 것입니다.

레이블 및 마일스톤 매핑

데이터베이스에 대한 압박을 줄이기 위해, 우리는 문제 및 병합 요청에 레이블과 마일스톤을 설정할 때 이를 쿼리하지 않습니다. 대신, 레이블과 마일스톤을 가져올 때 이 데이터를 캐시한 후, 문제/병합 요청에 할당할 때 이 캐시를 재사용합니다. 사용자 조회와 유사하게 이 캐시 키는 사용되지 않은 후 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 가져오기 대시보드는 시간에 따른 총 객체 수의 가져옴 vs. 가져온 정보를 제공합니다.