- 준비물
- 코드 구조
- 아키텍처 개요
-
단계
- 1. RepositoryImportWorker
- 2. Stage::ImportRepositoryWorker
- 3. Stage::ImportBaseDataWorker
- 4. Stage::ImportPullRequestsWorker
- 5. Stage::ImportCollaboratorsWorker
- 6. Stage::ImportIssuesAndDiffNotesWorker
- 7. Stage::ImportIssueEventsWorker
- 8. 단계::ImportAttachmentsWorker
- 9. 단계::ImportProtectedBranchesWorker
- 10. 단계::FinishImportWorker
- 단계 진행
- 가져오기 작업 ID 리프레시
- GitHub 요율 제한
- 사용자 조회를 캐싱
- Sidekiq 인터럽트 증가
sidekiq_options dead: false
- 라벨 및 마일스톤 매핑
- 로그
- 메트릭 대시보드
GitHub 가져오기 개발자 문서
GitHub 가져오기는 Sidekiq를 사용하는 병렬 가져오기 도구입니다.
준비물
-
github_importer
및github_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
라고 합니다.
- 가져오기는 API 요청을 통해 시작됩니다.
POST /import/github
- API 엔드포인트는
Import::GitHubService
를 호출합니다. - 이는
Gitlab::LegacyGithubImport::ProjectCreator
를 호출합니다. - 이는
Projects::CreateService
를 호출합니다. - 이는
@project.import_state.schedule
를 호출합니다. - 이는
project.add_import_job
를 호출합니다. - 이는
RepositoryImportWorker
를 호출합니다.
단계
1. RepositoryImportWorker
이 워커는
Projects::ImportService.new.execute
를 호출하며,
importer.execute
를 호출합니다.
이 맥락에서 importer
는 Gitlab::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
워커를 예약합니다.
참고:
이 단계는 선택 사항입니다(Gitlab::GithubImport::Settings
에 의해 제어) 및 기본적으로 선택됩니다.
6. Stage::ImportIssuesAndDiffNotesWorker
이 워커는 모든 이슈 및 풀 리퀘스트 코멘트를 가져옵니다. 각 이슈마다 Gitlab::GithubImport::ImportIssueWorker
워커를, 풀 리퀘스트 코멘트마다는 대신 Gitlab::GithubImport::DiffNoteImporter
워커의 작업을 예약합니다.
병렬로 이슈 및 diff 코멘트를 처리하기 때문에 별도의 단계를 예약하고 이전 작업이 완료될 때까지 기다릴 필요가 없습니다.
이슈는 풀 리퀘스트와 별도로 가져오기 때문에 “이슈” API에만 이슈 및 풀 리퀘스트용 레이블이 포함되어 있습니다. 이슈 가져오기와 레이블 링크 설정이 동일한 워커에서 수행되므로 별도의 API 데이터 크롤링이 필요하지 않아 프로젝트 가져오기에 필요한 API 호출 수가 줄어듭니다.
7. Stage::ImportIssueEventsWorker
이 워커는 모든 이슈와 풀 리퀘스트 이벤트를 가져옵니다. 각 이벤트마다 Gitlab::GithubImport::ImportIssueEventWorker
워커를 예약합니다.
GitHub API의 특정 측면으로 인해 이슈 및 풀 리퀘스트 이벤트를 단일 단계로 가져올 수 있습니다. 내부에서 이슈와 풀 리퀘스트는 동일한 테이블에 저장되며 따라서 전역적으로 고유한 ID가 있으므로:
- 모든 풀 리퀘스트는 이슈입니다.
- 이슈는 풀 리퀘스트가 아닙니다.
따라서 이슈 및 풀 리퀘스트는 가장 관련된 대부분의 것에 대한 공통 API를 가지고 있습니다.
타임라인 이벤트 엔드포인트를 사용하여 pull request 리뷰 요청을 가져오려면, 이벤트를 순차적으로 처리해야 합니다. 가져오기 워커가 보장된 순서로 실행되지 않기 때문에 pull request 리뷰 요청 이벤트는 처음에 Redis 정렬된 목록에 배치됩니다. 그런 다음 Gitlab::GithubImport::ReplayEventsWorker
에 의해 순차적으로 사용됩니다.
8. 단계::ImportAttachmentsWorker
이 워커는 마크다운 내부에 링크된 노트 첨부 파일을 가져옵니다. 프로젝트의 마크다운 텍스트가 포함된 각 엔티티에 대해 우리는 다음 작업을 예약합니다.
- 각 릴리스에 대해
Gitlab::GithubImport::Importer::Attachments::ReleasesImporter
를 예약합니다. - 각 노트에 대해
Gitlab::GithubImport::Importer::Attachments::NotesImporter
를 예약합니다. - 각 이슈에 대해
Gitlab::GithubImport::Importer::Attachments::IssuesImporter
를 예약합니다. - 각 병합 요청에 대해
Gitlab::GithubImport::Importer::Attachments::MergeRequestsImporter
를 예약합니다.
각 작업은 다음과 같은 단계로 진행됩니다.
- 특정 레코드 내의 모든 첨부 파일 링크를 반복합니다.
- 첨부 파일을 다운로드합니다.
- 이전 링크를 GitLab으로 새로 생성된 링크로 대체합니다.
참고:
이것은 Gitlab::GithubImport::Settings
에 의해 제어되는 상당한 추가 가져오기 시간을 소비할 수 있는 선택적인 단계입니다.
9. 단계::ImportProtectedBranchesWorker
이 워커는 보호된 브랜치 규칙을 가져옵니다.
GitHub에 있는 각 규칙에 대해, 우리는 Gitlab::GithubImport::ImportProtectedBranchWorker
작업을 예약합니다.
각 작업은 GitHub과 GitLab의 브랜치 보호 규칙을 비교하고 가장 엄격한 규칙을 GitLab의 브랜치에 적용합니다.
10. 단계::FinishImportWorker
이 워커는 몇 가지 하우스키피핑(예: 캐시 비우기)을 수행하고 가져오기를 완료로 표시하여 가져오기 프로세스를 완료합니다.
단계 진행
단계 진행은 다음 두 가지 방법 중 하나로 수행됩니다.
- 다음 단계를 직접 워커에 예약합니다.
- 현재 단계의 모든 작업이 완료되면
Gitlab::GithubImport::AdvanceStageWorker
작업을 예약합니다.
첫 번째 접근 방식은 작업이 단일 스레드에서 모든 작업을 수행하는 워커에만 사용해야 하며, 나머지에는 AdvanceStageWorker
를 사용해야 합니다.
첫 번째 접근 방식의 예는 ImportBaseDataWorker
가 PullRequestWorker
를 직접 호출하는 방법입니다.
두 번째 접근 방식의 예는 PullRequestsWorker
가 자신의 작업이 완료되었을 때 AdvanceStageWorker
를 호출하는 방법입니다.
작업을 예약할 때 AdvanceStageWorker
에 프로젝트 ID, Redis 키 목록, 다음 단계의 이름이 주어집니다. Redis 키( Gitlab::JobWaiter
에서 생성됨)는 현재 단계의 작업이 완료되었는지 여부를 확인하기 위해 사용됩니다. 단계가 아직 완료되지 않았으면 AdvanceStageWorker
는 자체를 다시 예약합니다. 단계가 완료되거나 마지막 호출 이후에 더 많은 작업이 완료되면 AdvanceStageworker
는 가져오기 JID를 리프레시하고 다음 단계의 워커를 예약합니다.
AdvanceStageWorker
의 예약 횟수를 줄이기 위해 이 워커는 다음 조치를 결정하기 전에 잠시 작업 완료를 기다립니다. 작은 프로젝트의 경우, 이는 가져오기 프로세스를 약간 느리게 할 수 있지만, 전반적으로 시스템에 가해지는 압력을 줄입니다.
가져오기 작업 ID 리프레시
GitLab에는 Gitlab::Import::StuckProjectImportJobsWorker
라는 워커가 주기적으로 실행되어 24시간 이상 업데이트되지 않았다면 프로젝트 가져오기를 실패로 표시합니다. GitHub 프로젝트의 경우, 큰 프로젝트를 가져오는 데 몇 일이 걸릴 수 있지만 (Gitlab::Import::StuckProjectImportJobsWorker
에서 자세한 정보 참조), 우리는 이러한 이유로 가져오기가 실패로 표시되기를 원하지 않습니다.
이것을 방지하기 위해 우리는 주기적으로 가져오기의 만료 시간을 리프레시합니다. 이 작업은 가져오기 작업의 JID를 데이터베이스에 저장하고, 가져오기 프로세스 중간에 이 JID TTL을 여러 단계에서 리프레시함으로써 수행됩니다. ProjectImportState#refresh_jid_expiration
를 호출하거나 현재 워커의 JID를 사용하여 RefreshImportJidWorker를 사용합니다. 이 TTL을 리프레시함으로써 우리는 여전히 작업을 수행하는 한 가져오기가 실패로 표시되지 않음을 보장할 수 있습니다.
GitHub 요율 제한
GitHub는 1시간에 5,000개의 API 호출을 요율 제한으로 정하고 있습니다. 프로젝트 가져오기에 필요한 요청 수는 대부분 프로젝트에 참여하는 고유한 사용자 수에 의해 주로 결정됩니다 (예: 이슈 작성자) 때문에, 사용자의 이메일 주소를 GitLab 사용자에 매핑해야 합니다. 다른 데이터(예: 이슈 페이지 및 코멘트)는 일반적으로 가져오기에 필요한 요청 수가 적습니다.
우리는 이 요율 제한을 다음과 같은 방법으로 처리합니다.
- 요율 제한에 도달한 후, 요율 제한이 재설정될 때까지 작업이 실행되지 않도록 작업을 자동으로 다시 예약합니다.
- GitHub 사용자를 GitLab 사용자에 대한 매핑을 Redis에서 캐싱합니다.
사용자 캐싱에 대한 자세한 정보는 아래에서 확인할 수 있습니다.
사용자 조회를 캐싱
GitHub 사용자를 GitLab 사용자에 매핑할 때(최악의 경우) 다음을 수행해야 합니다.
- 사용자의 이메일 주소를 얻기 위한 한 개의 API 호출.
- GitHub 사용자 ID를 기반으로 한 GitLab 사용자의 존재 여부를 확인하기 위한 두 개의 데이터베이스 쿼리. 하나는 GitHub 사용자 ID를 기반으로 사용자를 찾고, 두 번째 쿼리는 GitHub 이메일 주소를 기반으로 사용자를 찾습니다.
사용자 ID 검색은 GitHub Enterprise에서 가져올 때는 수행되지 않습니다.
이 프로세스가 비용이 많이 들기 때문에 이러한 조회 결과를 Redis에 캐시합니다. 각 사용자 조회에 대해 다음과 같이 다섯 가지 키를 저장합니다.
- GitHub 사용자 이름을 이메일 주소로 매핑하는 Redis 키.
- GitHub 이메일 주소를 GitLab 사용자 ID로 매핑하는 Redis 키.
- GitHub 사용자 ID를 GitLab 사용자 ID로 매핑하는 Redis 키.
- GitHub 사용자 이름을 ETAG 헤더로 매핑하는 Redis 키.
- 프로젝트에 대한 이메일 조회가 수행되었는지를 나타내는 Redis 키.
우리는 두 종류의 조회를 캐시합니다.
- 긍정적인 조회는 GitLab 사용자 ID를 찾은 경우입니다.
- 부정적인 조회는 GitLab 사용자 ID를 찾지 못한 경우입니다. 이러한 결과를 캐시하여 존재하지 않는 사용자에 대해 똑같은 작업을 수행하지 않도록 합니다.
이러한 키의 만료 시간은 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 프로세스가 종료되면 실행 중인 작업이 완료될 때까지 잠시 기다린 후 그 다음 작업을 중단시킵니다. 인터럽트는 작업을 종료시키고 다시 대기열에 넣습니다.
vendored sidekiq-reliable-fetcher
gem은 작업이 영구적으로 종료되기 전에 3
번의 인터럽트 제한을 두었습니다. 인터럽트된 작업은 Kibana에 json.interrupted_count
를 기록합니다.
이 한계는 Sidekiq 재시작 사이의 시간에 완료될 수 없는 작업으로부터 보호합니다.
대규모 가져오기의 경우, GitHub stage 워커(네임스페이스 Stage::
에서)가 완료되기까지 많은 시간이 소요됩니다. 기본적으로 sidekiq-reliable-fetcher
는 이러한 워커들이 완료되기 전에 영구적으로 중지되어 실패할 위험을 갖고 있습니다.
재시작 시 이전 상태에서 작업을 다시 시작하는 Stage 워커는 .resumes_work_when_interrupted!
를 호출하여 sidekiq-reliable-fetcher
의 인터럽트 한도를 20
까지 늘릴 수 있습니다.
module Gitlab
module GithubImport
module Stage
class MyWorker
resumes_work_when_interrupted!
# ...
end
end
end
end
재시작 시 전체 작업을 완전히 다시 시작하지 않는 Stage 워커는 이 방법을 호출해서는 안 됩니다. 예를 들어 이미 가져온 객체를 건너뛰고 각 번 실행을 시작하는 워커입니다.
전체적으로 작업을 다시 시작하는 Stage 워커의 예로는 다음과 같은 서비스를 실행하는 워커들이 있습니다:
sidekiq_options dead: false
일반적으로 워커의 재시도 횟수가 소진되면 Sidekiq dead set으로 이동하여 인스턴스 관리자가 다시 시도할 수 있습니다.
GithubImport::Queue
는 GitHub 가져오기 워커들이 이러한 상황이 발생하지 않도록 Sidekiq 워커 옵션 dead: false
를 설정합니다.
그 이유는:
- dead 세트에는 최대 한도가 있으며 임계치를 초과한 객체 가져오기 워커(여기에
ObjectImporter
가 포함된 워커)가 실패하면 다른 워커들을 밀어내고 dead 세트를 스팸으로 만들 수 있습니다. - Stage 워커들(여기에
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 가져오기 대시보드는 시간별로 가져온 객체의 총 수와 가져온 수에 대한 정보를 제공합니다.