InfoGrab DocsInfoGrab Docs

GitHub 임포터 개발자 문서

요약

GitHub 임포터는 Sidekiq를 사용하는 병렬 임포터입니다. 임포터의 코드베이스는 다음 디렉터리로 나뉩니다. GitHub 프로젝트를 임포트하면 작업이 여러 단계로 나뉘며, 각 단계는 실행되는 Sidekiq job 집합으로 구성됩니다.

GitHub 임포터는 Sidekiq를 사용하는 병렬 임포터입니다.

사전 요구 사항#

  • github_importer 및 github_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 job 집합으로 구성됩니다. 각 단계 사이에는 현재 단계의 모든 작업이 완료되었는지 주기적으로 확인하고, 완료되었으면 임포트 프로세스를 다음 단계로 진행시키는 job이 예약됩니다. 이를 처리하는 워커의 이름은 Gitlab::GithubImport::AdvanceStageWorker입니다.

단계#

1. RepositoryImportWorker#

이 워커는 Projects::ImportService.new.execute를 호출하고, 이 메서드는 importer.execute를 호출합니다.

여기서 importer는 Gitlab::ImportSources.importer(project.import_type)의 인스턴스이며, github 임포트 유형에서는 ParallelImporter에 매핑됩니다.

ParallelImporter는 다음 워커의 job을 예약합니다.

2. Stage::ImportRepositoryWorker#

이 워커는 리포지터리와 위키를 임포트하고, 완료되면 다음 단계를 예약합니다.

3. Stage::ImportBaseDataWorker#

이 워커는 레이블, 마일스톤, 릴리스 같은 기본 데이터를 임포트합니다. 이 작업은 충분히 빠르게 수행할 수 있어 병렬로 처리할 필요가 없으므로 단일 스레드에서 수행합니다.

4. Stage::ImportPullRequestsWorker#

이 워커는 모든 풀 리퀘스트를 임포트합니다. 풀 리퀘스트마다 Gitlab::GithubImport::ImportPullRequestWorker 워커의 job이 예약됩니다.

5. Stage::ImportCollaboratorsWorker#

이 워커는 외부 협업자가 아닌 직접 리포지터리 협업자만 임포트합니다. 협업자마다 Gitlab::GithubImport::ImportCollaboratorWorker 워커의 job을 예약합니다.

Note

이 단계는 선택 사항이며(Gitlab::GithubImport::Settings로 제어) 기본적으로 선택되어 있습니다.

6. Stage::ImportIssuesAndDiffNotesWorker#

이 워커는 모든 이슈와 풀 리퀘스트 댓글을 임포트합니다. 이슈마다 Gitlab::GithubImport::ImportIssueWorker 워커의 job을 예약합니다. 풀 리퀘스트 댓글에 대해서는 대신 Gitlab::GithubImport::DiffNoteImporter 워커의 job을 예약합니다.

이 워커는 이슈와 diff 노트를 병렬로 처리하므로 별도의 단계를 예약하고 이전 단계가 끝나기를 기다릴 필요가 없습니다.

이슈는 풀 리퀘스트와 별도로 임포트합니다. "issues" API만 이슈와 풀 리퀘스트 양쪽의 레이블을 포함하기 때문입니다. 같은 워커에서 이슈를 임포트하면서 레이블 링크까지 설정하면 API 데이터를 별도로 다시 훑을 필요가 없어져, 프로젝트를 임포트하는 데 필요한 API 호출 수가 줄어듭니다.

7. Stage::ImportIssueEventsWorker#

이 워커는 모든 이슈 이벤트와 풀 리퀘스트 이벤트를 임포트합니다. 이벤트마다 Gitlab::GithubImport::ImportIssueEventWorker 워커의 job을 예약합니다.

GitHub API의 특정한 성질 덕분에 이슈 이벤트와 풀 리퀘스트 이벤트를 한 단계에서 임포트할 수 있습니다. 내부적으로 GitHub의 이슈와 풀 리퀘스트는 단일 테이블에 저장되는 것으로 보입니다. 따라서 전역적으로 고유한 ID를 가지며 다음이 성립합니다.

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

따라서 이슈와 풀 리퀘스트는 관련된 대부분의 항목에 공통 API를 사용합니다.

타임라인 이벤트 엔드포인트로 pull request review requests를 임포트하려면 이벤트를 순차적으로 처리해야 합니다. 임포트 워커의 실행 순서는 보장되지 않으므로 pull request review requests 이벤트는 우선 Redis 정렬 리스트에 저장됩니다. 이후 Gitlab::GithubImport::ReplayEventsWorker가 순서대로 소비합니다.

8. Stage::ImportAttachmentsWorker#

이 워커는 Markdown 안에 링크된 노트 첨부 파일을 임포트합니다. 프로젝트에서 Markdown 텍스트가 있는 엔티티마다 다음 job을 예약합니다.

  • 릴리스마다 Gitlab::GithubImport::Importer::Attachments::ReleasesImporter.
  • 노트마다 Gitlab::GithubImport::Importer::Attachments::NotesImporter.
  • 이슈마다 Gitlab::GithubImport::Importer::Attachments::IssuesImporter.
  • 머지 리퀘스트마다 Gitlab::GithubImport::Importer::Attachments::MergeRequestsImporter.

각 job은 다음을 수행합니다.

  1. 특정 레코드 안의 모든 첨부 파일 링크를 순회합니다.
  2. 첨부 파일을 내려받습니다.
  3. 기존 링크를 새로 생성한 GitLab 링크로 바꿉니다.
Note

이 단계는 선택 사항이며 임포트 시간이 크게 늘어날 수 있습니다(Gitlab::GithubImport::Settings로 제어).

9. Stage::ImportProtectedBranchesWorker#

이 워커는 보호된 브랜치 규칙을 임포트합니다. GitHub에 존재하는 규칙마다 Gitlab::GithubImport::ImportProtectedBranchWorker job을 예약합니다.

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

10. Stage::FinishImportWorker#

이 워커는 캐시 비우기 같은 정리 작업을 수행하고 임포트를 완료로 표시해 임포트 프로세스를 마무리합니다.

단계 진행#

단계 진행 방식은 다음 두 가지 중 하나입니다.

  • 다음 단계의 워커를 직접 예약합니다.
  • 현재 단계의 모든 작업이 완료되면 단계를 진행시키는 Gitlab::GithubImport::AdvanceStageWorker job을 예약합니다.

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

첫 번째 방식의 예로는 ImportBaseDataWorker가 PullRequestWorker를 직접 호출하는 방식이 있습니다.

두 번째 방식의 예로는 PullRequestsWorker가 자신의 작업을 완료했을 때 AdvanceStageWorker를 호출하는 방식이 있습니다.

job을 예약하면 AdvanceStageWorker에 프로젝트 ID, Redis 키 목록, 다음 단계의 이름이 전달됩니다. Redis 키(Gitlab::JobWaiter가 생성)는 실행 중인 단계가 완료되었는지 확인하는 데 사용됩니다. 단계가 아직 완료되지 않았으면 AdvanceStageWorker는 자신을 다시 예약합니다. 단계가 끝난 후, 또는 마지막 호출 이후 더 많은 job이 완료된 경우에 AdvanceStageworker는 임포트 JID(아래에서 자세히 설명)를 갱신하고 다음 단계의 워커를 예약합니다.

예약되는 AdvanceStageWorker job 수를 줄이기 위해 이 워커는 다음 동작을 결정하기 전에 job이 완료되기를 잠시 기다립니다. 소규모 프로젝트에서는 임포트 프로세스가 다소 느려질 수 있지만, 시스템 전체의 부하는 줄어듭니다.

사용자 기여 매핑#

GitHub 임포터는 사용자 기여 매핑을 지원하며, 이를 통해 임포트한 레코드를 임포트 완료 후 실제 사용자를 지정할 수 있을 때까지 플레이스홀더 사용자에게 귀속할 수 있습니다.

핵심 클래스#

클래스 목적
GithubImport::UserFinder GitHub API 응답의 사용자 데이터를 GitLab User 레코드에 매핑하기 위해 Import::SourceUserMapper를 호출하는 클래스
GithubImport::Importer::CollaboratorImporter 소스 사용자의 플레이스홀더 멤버십을 생성하기 위해 PlaceholderMemberships::CreateService를 호출

삭제된 사용자와 고스트 사용자 처리#

GitHub에는 삭제된 사용자를 나타내는 특수 "ghost" 사용자(사용자 이름: ghost)가 있습니다. 임포터가 이 사용자를 만나면 Import::SourceUser나 플레이스홀더 사용자를 만들지 않고 GitLab 고스트 사용자에 직접 매핑합니다.

임포트 job ID 갱신#

GitLab에는 Gitlab::Import::StuckProjectImportJobsWorker라는 워커가 있으며, 이 워커는 주기적으로 실행되어 24시간 넘게 갱신되지 않은 프로젝트 임포트를 실패로 표시합니다. GitHub 프로젝트에서는 이 점이 다소 문제가 됩니다. 대규모 프로젝트 임포트는 GitHub 속도 제한에 얼마나 자주 걸리는지에 따라 며칠이 걸릴 수 있지만(아래에서 자세히 설명), 그 때문에 Gitlab::Import::StuckProjectImportJobsWorker가 임포트를 실패로 표시하는 것은 바람직하지 않습니다.

이를 막기 위해 임포트의 만료 시간을 주기적으로 갱신합니다. 임포트 job의 JID를 데이터베이스에 저장한 뒤, 임포트 프로세스의 여러 단계에서 이 JID의 TTL을 갱신하는 방식으로 동작합니다. 이는 ProjectImportState#refresh_jid_expiration을 호출하거나, RefreshImportJidWorker에 현재 워커의 jid를 전달해 수행합니다. 이 TTL을 갱신하면 작업이 진행되는 동안에는 임포트가 실패로 표시되지 않도록 할 수 있습니다.

GitHub 속도 제한#

GitHub의 속도 제한은 시간당 API 호출 5,000회입니다. 프로젝트를 임포트하는 데 필요한 요청 수는 대체로 프로젝트에 관여한 고유 사용자 수(예: 이슈 작성자)에 좌우됩니다. 사용자를 GitLab 사용자에 매핑하려면 사용자의 이메일 주소가 필요하기 때문입니다. 이슈 페이지나 댓글 같은 다른 데이터는 보통 임포트에 수십 건의 요청만 필요합니다.

속도 제한은 다음과 같이 처리합니다.

  1. 속도 제한에 도달하면 제한이 초기화될 때까지 job이 실행되지 않도록 자동으로 다시 예약합니다.
  2. GitHub 사용자와 GitLab 사용자의 매핑을 Redis에 캐시합니다.

사용자 캐싱에 대한 자세한 내용은 아래에 있습니다.

사용자 조회 캐싱#

GitHub 사용자를 GitLab 사용자에 매핑할 때는 최악의 경우 다음을 수행해야 합니다.

  1. 사용자의 이메일 주소를 가져오는 API 호출 1회.
  2. 대응하는 GitLab 사용자가 있는지 확인하는 데이터베이스 쿼리 2회. 한 쿼리는 GitHub 사용자 ID로 사용자를 찾고, 두 번째 쿼리는 GitHub 이메일 주소로 사용자를 찾습니다.

사용자가 잘못 매칭되지 않도록 GitHub Enterprise에서 임포트할 때는 GitHub 사용자 ID로 검색하지 않습니다.

이 과정은 비용이 상당히 크기 때문에 조회 결과를 Redis에 캐시합니다. 조회한 사용자마다 키 5개를 저장합니다.

  • 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 프로세스가 종료될 때는 실행 중인 job이 끝나기를 일정 시간 기다린 뒤 인터럽트합니다. 인터럽트는 job을 종료하고 다시 큐에 넣습니다. GitLab이 벤더링한 sidekiq-reliable-fetcher gem은 인터럽트 3회를 한도로 두며, 이를 넘으면 job을 다시 큐에 넣지 않고 영구히 종료합니다. 인터럽트된 job은 Kibana에 json.interrupted_count를 기록합니다.

이 한도는 Sidekiq 재시작 사이의 시간 안에 결코 완료될 수 없는 job으로부터 시스템을 보호합니다.

대규모 임포트에서는 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 dead set으로 이동하며 인스턴스 관리자가 재시도할 수 있습니다.

GithubImport::Queue는 GitHub 임포터 워커에서 이런 일이 일어나지 않도록 Sidekiq 워커 옵션 dead: false를 설정합니다.

이유는 다음과 같습니다.

  • dead set에는 최대 한도가 있어 객체 임포터 워커(ObjectImporter를 포함하는 워커)가 대량으로 실패하면 dead set을 가득 채워 다른 워커를 밀어낼 수 있습니다.
  • 단계 워커(StageMethods를 포함하는 워커)는 재시도가 모두 소진되면 임포트를 실패 처리하므로, 재시도해도 아무 동작도 하지 않습니다.

레이블과 마일스톤 매핑#

데이터베이스 부하를 줄이기 위해 이슈와 머지 리퀘스트에 레이블과 마일스톤을 설정할 때는 데이터베이스를 조회하지 않습니다. 대신 레이블과 마일스톤을 임포트할 때 데이터를 캐시해 두고, 이슈나 머지 리퀘스트에 할당할 때 이 캐시를 재사용합니다. 사용자 조회와 마찬가지로 이 캐시 키도 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 임포터 대시보드를 사용합니다. 이 대시보드는 시간에 따라 가져온 객체 총수와 임포트된 객체 총수를 보여 줍니다.

GitHub 임포터 개발자 문서

GitLab v19.4
원문 보기

요약

GitHub 임포터는 Sidekiq를 사용하는 병렬 임포터입니다. 임포터의 코드베이스는 다음 디렉터리로 나뉩니다. GitHub 프로젝트를 임포트하면 작업이 여러 단계로 나뉘며, 각 단계는 실행되는 Sidekiq job 집합으로 구성됩니다.

GitHub 임포터는 Sidekiq를 사용하는 병렬 임포터입니다.

사전 요구 사항#

  • github_importer 및 github_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 job 집합으로 구성됩니다. 각 단계 사이에는 현재 단계의 모든 작업이 완료되었는지 주기적으로 확인하고, 완료되었으면 임포트 프로세스를 다음 단계로 진행시키는 job이 예약됩니다. 이를 처리하는 워커의 이름은 Gitlab::GithubImport::AdvanceStageWorker입니다.

단계#

1. RepositoryImportWorker#

이 워커는 Projects::ImportService.new.execute를 호출하고, 이 메서드는 importer.execute를 호출합니다.

여기서 importer는 Gitlab::ImportSources.importer(project.import_type)의 인스턴스이며, github 임포트 유형에서는 ParallelImporter에 매핑됩니다.

ParallelImporter는 다음 워커의 job을 예약합니다.

2. Stage::ImportRepositoryWorker#

이 워커는 리포지터리와 위키를 임포트하고, 완료되면 다음 단계를 예약합니다.

3. Stage::ImportBaseDataWorker#

이 워커는 레이블, 마일스톤, 릴리스 같은 기본 데이터를 임포트합니다. 이 작업은 충분히 빠르게 수행할 수 있어 병렬로 처리할 필요가 없으므로 단일 스레드에서 수행합니다.

4. Stage::ImportPullRequestsWorker#

이 워커는 모든 풀 리퀘스트를 임포트합니다. 풀 리퀘스트마다 Gitlab::GithubImport::ImportPullRequestWorker 워커의 job이 예약됩니다.

5. Stage::ImportCollaboratorsWorker#

이 워커는 외부 협업자가 아닌 직접 리포지터리 협업자만 임포트합니다. 협업자마다 Gitlab::GithubImport::ImportCollaboratorWorker 워커의 job을 예약합니다.

Note

이 단계는 선택 사항이며(Gitlab::GithubImport::Settings로 제어) 기본적으로 선택되어 있습니다.

6. Stage::ImportIssuesAndDiffNotesWorker#

이 워커는 모든 이슈와 풀 리퀘스트 댓글을 임포트합니다. 이슈마다 Gitlab::GithubImport::ImportIssueWorker 워커의 job을 예약합니다. 풀 리퀘스트 댓글에 대해서는 대신 Gitlab::GithubImport::DiffNoteImporter 워커의 job을 예약합니다.

이 워커는 이슈와 diff 노트를 병렬로 처리하므로 별도의 단계를 예약하고 이전 단계가 끝나기를 기다릴 필요가 없습니다.

이슈는 풀 리퀘스트와 별도로 임포트합니다. "issues" API만 이슈와 풀 리퀘스트 양쪽의 레이블을 포함하기 때문입니다. 같은 워커에서 이슈를 임포트하면서 레이블 링크까지 설정하면 API 데이터를 별도로 다시 훑을 필요가 없어져, 프로젝트를 임포트하는 데 필요한 API 호출 수가 줄어듭니다.

7. Stage::ImportIssueEventsWorker#

이 워커는 모든 이슈 이벤트와 풀 리퀘스트 이벤트를 임포트합니다. 이벤트마다 Gitlab::GithubImport::ImportIssueEventWorker 워커의 job을 예약합니다.

GitHub API의 특정한 성질 덕분에 이슈 이벤트와 풀 리퀘스트 이벤트를 한 단계에서 임포트할 수 있습니다. 내부적으로 GitHub의 이슈와 풀 리퀘스트는 단일 테이블에 저장되는 것으로 보입니다. 따라서 전역적으로 고유한 ID를 가지며 다음이 성립합니다.

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

따라서 이슈와 풀 리퀘스트는 관련된 대부분의 항목에 공통 API를 사용합니다.

타임라인 이벤트 엔드포인트로 pull request review requests를 임포트하려면 이벤트를 순차적으로 처리해야 합니다. 임포트 워커의 실행 순서는 보장되지 않으므로 pull request review requests 이벤트는 우선 Redis 정렬 리스트에 저장됩니다. 이후 Gitlab::GithubImport::ReplayEventsWorker가 순서대로 소비합니다.

8. Stage::ImportAttachmentsWorker#

이 워커는 Markdown 안에 링크된 노트 첨부 파일을 임포트합니다. 프로젝트에서 Markdown 텍스트가 있는 엔티티마다 다음 job을 예약합니다.

  • 릴리스마다 Gitlab::GithubImport::Importer::Attachments::ReleasesImporter.
  • 노트마다 Gitlab::GithubImport::Importer::Attachments::NotesImporter.
  • 이슈마다 Gitlab::GithubImport::Importer::Attachments::IssuesImporter.
  • 머지 리퀘스트마다 Gitlab::GithubImport::Importer::Attachments::MergeRequestsImporter.

각 job은 다음을 수행합니다.

  1. 특정 레코드 안의 모든 첨부 파일 링크를 순회합니다.
  2. 첨부 파일을 내려받습니다.
  3. 기존 링크를 새로 생성한 GitLab 링크로 바꿉니다.
Note

이 단계는 선택 사항이며 임포트 시간이 크게 늘어날 수 있습니다(Gitlab::GithubImport::Settings로 제어).

9. Stage::ImportProtectedBranchesWorker#

이 워커는 보호된 브랜치 규칙을 임포트합니다. GitHub에 존재하는 규칙마다 Gitlab::GithubImport::ImportProtectedBranchWorker job을 예약합니다.

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

10. Stage::FinishImportWorker#

이 워커는 캐시 비우기 같은 정리 작업을 수행하고 임포트를 완료로 표시해 임포트 프로세스를 마무리합니다.

단계 진행#

단계 진행 방식은 다음 두 가지 중 하나입니다.

  • 다음 단계의 워커를 직접 예약합니다.
  • 현재 단계의 모든 작업이 완료되면 단계를 진행시키는 Gitlab::GithubImport::AdvanceStageWorker job을 예약합니다.

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

첫 번째 방식의 예로는 ImportBaseDataWorker가 PullRequestWorker를 직접 호출하는 방식이 있습니다.

두 번째 방식의 예로는 PullRequestsWorker가 자신의 작업을 완료했을 때 AdvanceStageWorker를 호출하는 방식이 있습니다.

job을 예약하면 AdvanceStageWorker에 프로젝트 ID, Redis 키 목록, 다음 단계의 이름이 전달됩니다. Redis 키(Gitlab::JobWaiter가 생성)는 실행 중인 단계가 완료되었는지 확인하는 데 사용됩니다. 단계가 아직 완료되지 않았으면 AdvanceStageWorker는 자신을 다시 예약합니다. 단계가 끝난 후, 또는 마지막 호출 이후 더 많은 job이 완료된 경우에 AdvanceStageworker는 임포트 JID(아래에서 자세히 설명)를 갱신하고 다음 단계의 워커를 예약합니다.

예약되는 AdvanceStageWorker job 수를 줄이기 위해 이 워커는 다음 동작을 결정하기 전에 job이 완료되기를 잠시 기다립니다. 소규모 프로젝트에서는 임포트 프로세스가 다소 느려질 수 있지만, 시스템 전체의 부하는 줄어듭니다.

사용자 기여 매핑#

GitHub 임포터는 사용자 기여 매핑을 지원하며, 이를 통해 임포트한 레코드를 임포트 완료 후 실제 사용자를 지정할 수 있을 때까지 플레이스홀더 사용자에게 귀속할 수 있습니다.

핵심 클래스#

클래스 목적
GithubImport::UserFinder GitHub API 응답의 사용자 데이터를 GitLab User 레코드에 매핑하기 위해 Import::SourceUserMapper를 호출하는 클래스
GithubImport::Importer::CollaboratorImporter 소스 사용자의 플레이스홀더 멤버십을 생성하기 위해 PlaceholderMemberships::CreateService를 호출

삭제된 사용자와 고스트 사용자 처리#

GitHub에는 삭제된 사용자를 나타내는 특수 "ghost" 사용자(사용자 이름: ghost)가 있습니다. 임포터가 이 사용자를 만나면 Import::SourceUser나 플레이스홀더 사용자를 만들지 않고 GitLab 고스트 사용자에 직접 매핑합니다.

임포트 job ID 갱신#

GitLab에는 Gitlab::Import::StuckProjectImportJobsWorker라는 워커가 있으며, 이 워커는 주기적으로 실행되어 24시간 넘게 갱신되지 않은 프로젝트 임포트를 실패로 표시합니다. GitHub 프로젝트에서는 이 점이 다소 문제가 됩니다. 대규모 프로젝트 임포트는 GitHub 속도 제한에 얼마나 자주 걸리는지에 따라 며칠이 걸릴 수 있지만(아래에서 자세히 설명), 그 때문에 Gitlab::Import::StuckProjectImportJobsWorker가 임포트를 실패로 표시하는 것은 바람직하지 않습니다.

이를 막기 위해 임포트의 만료 시간을 주기적으로 갱신합니다. 임포트 job의 JID를 데이터베이스에 저장한 뒤, 임포트 프로세스의 여러 단계에서 이 JID의 TTL을 갱신하는 방식으로 동작합니다. 이는 ProjectImportState#refresh_jid_expiration을 호출하거나, RefreshImportJidWorker에 현재 워커의 jid를 전달해 수행합니다. 이 TTL을 갱신하면 작업이 진행되는 동안에는 임포트가 실패로 표시되지 않도록 할 수 있습니다.

GitHub 속도 제한#

GitHub의 속도 제한은 시간당 API 호출 5,000회입니다. 프로젝트를 임포트하는 데 필요한 요청 수는 대체로 프로젝트에 관여한 고유 사용자 수(예: 이슈 작성자)에 좌우됩니다. 사용자를 GitLab 사용자에 매핑하려면 사용자의 이메일 주소가 필요하기 때문입니다. 이슈 페이지나 댓글 같은 다른 데이터는 보통 임포트에 수십 건의 요청만 필요합니다.

속도 제한은 다음과 같이 처리합니다.

  1. 속도 제한에 도달하면 제한이 초기화될 때까지 job이 실행되지 않도록 자동으로 다시 예약합니다.
  2. GitHub 사용자와 GitLab 사용자의 매핑을 Redis에 캐시합니다.

사용자 캐싱에 대한 자세한 내용은 아래에 있습니다.

사용자 조회 캐싱#

GitHub 사용자를 GitLab 사용자에 매핑할 때는 최악의 경우 다음을 수행해야 합니다.

  1. 사용자의 이메일 주소를 가져오는 API 호출 1회.
  2. 대응하는 GitLab 사용자가 있는지 확인하는 데이터베이스 쿼리 2회. 한 쿼리는 GitHub 사용자 ID로 사용자를 찾고, 두 번째 쿼리는 GitHub 이메일 주소로 사용자를 찾습니다.

사용자가 잘못 매칭되지 않도록 GitHub Enterprise에서 임포트할 때는 GitHub 사용자 ID로 검색하지 않습니다.

이 과정은 비용이 상당히 크기 때문에 조회 결과를 Redis에 캐시합니다. 조회한 사용자마다 키 5개를 저장합니다.

  • 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 프로세스가 종료될 때는 실행 중인 job이 끝나기를 일정 시간 기다린 뒤 인터럽트합니다. 인터럽트는 job을 종료하고 다시 큐에 넣습니다. GitLab이 벤더링한 sidekiq-reliable-fetcher gem은 인터럽트 3회를 한도로 두며, 이를 넘으면 job을 다시 큐에 넣지 않고 영구히 종료합니다. 인터럽트된 job은 Kibana에 json.interrupted_count를 기록합니다.

이 한도는 Sidekiq 재시작 사이의 시간 안에 결코 완료될 수 없는 job으로부터 시스템을 보호합니다.

대규모 임포트에서는 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 dead set으로 이동하며 인스턴스 관리자가 재시도할 수 있습니다.

GithubImport::Queue는 GitHub 임포터 워커에서 이런 일이 일어나지 않도록 Sidekiq 워커 옵션 dead: false를 설정합니다.

이유는 다음과 같습니다.

  • dead set에는 최대 한도가 있어 객체 임포터 워커(ObjectImporter를 포함하는 워커)가 대량으로 실패하면 dead set을 가득 채워 다른 워커를 밀어낼 수 있습니다.
  • 단계 워커(StageMethods를 포함하는 워커)는 재시도가 모두 소진되면 임포트를 실패 처리하므로, 재시도해도 아무 동작도 하지 않습니다.

레이블과 마일스톤 매핑#

데이터베이스 부하를 줄이기 위해 이슈와 머지 리퀘스트에 레이블과 마일스톤을 설정할 때는 데이터베이스를 조회하지 않습니다. 대신 레이블과 마일스톤을 임포트할 때 데이터를 캐시해 두고, 이슈나 머지 리퀘스트에 할당할 때 이 캐시를 재사용합니다. 사용자 조회와 마찬가지로 이 캐시 키도 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 임포터 대시보드를 사용합니다. 이 대시보드는 시간에 따라 가져온 객체 총수와 임포트된 객체 총수를 보여 줍니다.