GitLab에서 Git 객체 중복 제거 작동 방법

GitLab 사용자가 프로젝트를 복제하면, GitLab은 포크 시점의 원본 프로젝트의 사본인 연관된 Git 저장소가 있는 새 프로젝트를 생성합니다. 큰 프로젝트가 자주 포크되면 Git 저장소 저장 공간 사용량이 급속히 증가할 수 있습니다. 이 문제를 해결하기 위해 GitLab은 포크용 Git 객체 중복을 추가하고 있습니다. 본 문서에서는 GitLab이 Git 객체 중복을 어떻게 구현하는지 설명합니다.

풀 저장소

Git alternates 이해

Git 수준에서 중복을 제거하기 위해 Git alternates를 사용합니다. Git alternates는 동일한 컴퓨터의 다른 저장소에서 객체를 빌려올 수 있는 메커니즘입니다.

저장소 A가 저장소 B에서 빌릴 수 있게 하려면:

  1. 특별한 파일인 A.git/objects/info/alternatesB.git/objects로 해석되는 경로를 쓰면 됩니다.
  2. 저장소 A에서 git repack을 실행하여 저장소 A에 있는 B에도 있는 모든 객체를 제거합니다.

리팩을 한 후에 저장소 A는 더 이상 자기 자신만으로 존재하지 않지만 자신의 참조와 설정은 여전히 가지고 있습니다. B에 없는 A의 객체는 A에 그대로 남아 있습니다. 이러한 구성이 작동하려면 B에서 객체를 삭제해서는 안 됩니다. A에서 그 객체들이 필요할 수 있기 때문입니다.

경고: @pools 디렉터리에 저장된 객체 풀 저장소에서 git prune 또는 git gc를 실행하지 마십시오. 이렇게 하면 객체 풀에 의존하는 일반 저장소에서 데이터가 손실될 수 있습니다.

문제는 git prunegit gcgit prune을 호출한다는 점에 있습니다. 문제는 객체 풀 저장소에서 실행 중인 git prune이 객체가 더 이상 필요하지 않은지 신뢰할 수 없다는 것입니다.

GitLab의 Git alternates: 풀 저장소

GitLab은 특별한 풀 저장소를 생성하여 객체 빌림을 구성합니다. 이 저장소들은 사용자에게는 숨겨져 있습니다. 그런 다음 Git alternates를 사용하여 프로젝트 저장소들의 컬렉션이 단일 풀 저장소에서 빌립니다. 이러한 프로젝트 저장소들의 컬렉션을 풀이라고 부릅니다. 풀은 사용자가 프로젝트를 포크할 때 형성되는 포크 네트워크와 비슷해 보이지만 (거의 동일하지는 않음) 단일 풀에서 빌립니다.

Git 수준에서 풀 저장소는 Gitaly RPC 호출을 사용하여 생성 및 관리됩니다. 일반 저장소와 마찬가지로 풀 저장소가 존재하고 그 저장소들이 빌리는지 여부는 SQL에서 Rails 응용 프로그램 수준에서 결정됩니다.

요약하자면, GitLab 프로젝트 저장소 컬렉션 간의 효과적인 객체 중복 삭제를 위해 Git 수준에서 세 가지가 필요합니다:

  1. 풀 저장소가 존재해야 합니다.
  2. 참여하는 프로젝트 저장소들은 각자 objects/info/alternates 파일을 사용하여 풀 저장소에 연결되어 있어야 합니다.
  3. 풀 저장소는 참여하는 프로젝트 저장소들과 공통의 Git 객체 데이터를 포함해야 합니다.

중복 삭제 요소

GitLab에서의 Git 객체 중복 제거의 효과는 풀 저장소와 각 참여 프로젝트 간의 중복 객체 양에 따라 달라집니다. 원본 프로젝트에서 가비지 수집이 실행될 때마다 원본 프로젝트의 Git 객체들은 풀 저장소로 이동됩니다. 가비지 수집이 실행될 때마다 새로운 객체들이 풀에 추가되어 다른 멤버 프로젝트들은 새 객체들로 이득을 볼 수 있습니다.

SQL 모델

GitLab의 프로젝트 저장소는 고유의 SQL 테이블을 가지고 있지 않습니다. 이들은 projects 테이블의 열을 통해 간접적으로 식별됩니다. 다시 말하면, 프로젝트 저장소를 찾을 수 있는 유일한 방법은 먼저 해당 프로젝트를 찾은 다음 project.repository를 호출하는 것입니다.

풀 저장소를 만들면 우리는 새롭게 시작합니다. 이들은 자체 pool_repositories SQL 테이블에 있습니다. 이 두 테이블 사이의 관계는 다음과 같습니다:

  • Project는 최대 하나의 PoolRepository에 속합니다 (project.pool_repository).
  • 위의 자동 결과로, 하나의 PoolRepository에는 여러 개의 Project가 있습니다.
  • PoolRepository에는 정확히 하나의 “소스 Project“가 있습니다 (pool.source_project).

가정 사항

  • 풀에 있는 모든 저장소는 동일한 Gitaly 저장소 샤드에 있어야 합니다. Git alternates 메커니즘은 여러 저장소 간에 직접적인 디스크 액세스에 의존하며, 우리는 Gitaly 저장소 샤드 내에서만 직접적인 디스크 액세스가 가능하다고 가정할 수 있습니다.
  • 풀 내의 멤버 프로젝트를 제거하는 유일한 두 가지 방법은 (1) 프로젝트를 삭제하거나 (2) 프로젝트를 다른 Gitaly 저장소 샤드로 이동하는 것입니다.

풀 및 풀 멤버십 생성

  • 풀이 생성될 때는 반드시 소스 프로젝트가 있어야 합니다. 풀 저장소의 초기 내용은 소스 프로젝트 저장소의 Git 복제본입니다.
  • 풀을 만드는 기회는 기존의 적합한 (비공개, 해시 저장, 포크되지 않은) GitLab 프로젝트가 포크되었을 때입니다. 포크 원본 프로젝트는 새 풀의 소스 프로젝트가 되고, 포크 원본 및 포크 자식 프로젝트 모두 새 풀의 멤버가 됩니다.
  • 한 번 A 프로젝트가 풀의 소스 프로젝트가 되면 A의 적합한 포크는 향후 모두 풀 멤버가 됩니다.
  • 만약 포크 원본이 포크인 경우, 결과적인 저장소는 저장소에 들어가지도 않고 새 풀 저장소가 시작되지도 않습니다.

    예를 들면:

    가정해보자. 포크 A가 풀 저장소의 일부인 경우, 포크 A에서 만들어진 어떤 포크도 포크 A가 일부인 풀 저장소의 일부가 될 수 없습니다.

    B가 A의 포크이고 A가 객체 풀을 갖지 않는다고 가정해봅시다. 이제 C가 B의 포크로 만들어집니다. C는 풀 저장소의 일부가 아닙니다.

결과

  • 풀에 참여하는 일반적인 프로젝트가 다른 Gitaly 저장소 샤드로 이동되면 “풀 저장소에 속합니다” 관계가 깨집니다. 저장소를 샤드로 옮기는 방법에 따라 프로젝트의 저장소에 새롭고 독립적인 복사본을 얻게 됩니다.
  • 풀의 소스 프로젝트가 다른 Gitaly 저장소 샤드로 이동하거나 삭제된 경우 “소스 프로젝트” 관계는 깨지지 않습니다. 그러나 소스가 동일한 Gitaly 샤드에 있을 때만 풀이 소스에서 가져옵니다.

SQL 풀 관계와 Gitaly 간의 일관성

Gitaly 측면에서 SQL 풀 관계는 Gitaly 서버의 현황에 대해 두 가지 유형의 주장을 제기합니다: 풀 저장소의 존재와 저장소와 풀 간의 대체 연결의 존재에 관한 주장입니다.

풀 존재

만일 GitLab이 풀 저장소가 존재한다고 판단하지만(GitLab 데이터베이스에 따르면 존재함), Gitaly 서버에는 실제로 존재하지 않는 경우, Gitaly에서 동적으로 생성됩니다.

풀 관계의 존재

여기서 세 가지 다른 문제점이 발생할 수 있습니다.

1. SQL에서 저장소 A가 풀 P에 속한다고 주장하지만 Gitaly에서 A에는 대체 개체가 없다고 주장하는 경우

이 경우, 디스크 공간 절약 기회를 놓치지만 A 자체에 대한 모든 RPC는 정상적으로 작동합니다. 다음으로 A에서 가비지 수집이 실행되면 Gitaly에서 대체 연결이 설정됩니다. 이 작업은 GitLab 레일즈의 Projects::GitDeduplicationService에서 수행됩니다.

2. SQL에서 저장소 A가 풀 P1에 속한다고 주장하지만 Gitaly에서 A가 풀 P2에 있는 대체 객체를 가지고 있다고 주장하는 경우

이 경우 Projects::GitDeduplicationService에서 예외가 발생합니다.

3. SQL에서 저장소 A가 어떤 풀에 속하지 않는다고 주장하지만 Gitaly에서 A가 P에 속해있다고 주장하는 경우

이 경우 Projects::GitDeduplicationService에서 저장소 A를 “다시 중복”하여 DisconnectGitAlternates RPC를 사용합니다.

Git 객체 중복 및 GitLab Geo

Geo 기본 서버에서 SQL에 풀 저장소 레코드가 생성되면, 이는 최종적으로 Geo 이중 서버에서 이벤트를 트리거합니다. 그 후, Geo 이중 서버에서 Gitaly에 풀 저장소를 생성합니다. 이로 인해 “결국 일관성 있는” 상황이 발생하며, 각 풀 참가자가 동기화되면 Geo는 결국 이중 서버에서 Gitaly에서 가비지 수집을 트리거하여 Git 객체를 중복 처리합니다.