GitLab에서 Git 오브젝트 중복 제거가 작동하는 방식
GitLab v19.1GitLab 사용자가 프로젝트를 포크하면, GitLab은 포크 시점의 원본 프로젝트 복사본인 Git 리포지터리가 연결된 새 프로젝트를 생성합니다. Git 수준에서는 Git alternates를 사용하여 중복 제거를 달성합니다.
GitLab 사용자가 프로젝트를 포크하면, GitLab은 포크 시점의 원본 프로젝트 복사본인 Git 리포지터리가 연결된 새 프로젝트를 생성합니다. 대규모 프로젝트가 자주 포크될 경우, Git 리포지터리 스토리지 디스크 사용량이 빠르게 증가할 수 있습니다. 이 문제를 해결하기 위해 GitLab에 포크를 위한 Git 오브젝트 중복 제거 기능을 추가하고 있습니다. 이 문서에서는 GitLab이 Git 오브젝트 중복 제거를 어떻게 구현하는지 설명합니다.
풀 리포지터리#
Git alternates 이해하기#
Git 수준에서는 Git alternates를 사용하여 중복 제거를 달성합니다. Git alternates는 리포지터리가 동일 머신의 다른 리포지터리에서 오브젝트를 빌려올 수 있게 해주는 메커니즘입니다.
리포지터리 A가 리포지터리 B에서 빌려오도록 만들려면 다음을 수행합니다:
-
특수 파일
A.git/objects/info/alternates에B.git/objects로 해석되는 경로를 작성하여 alternates 링크를 설정합니다. -
리포지터리 A에서
git repack을 실행하여 리포지터리 B에도 존재하는 모든 오브젝트를 A에서 제거합니다.
repack 후, 리포지터리 A는 더 이상 자급자족하지 않지만 자체 refs와 설정은 유지됩니다. B에 없는 A의 오브젝트는 A에 남아 있습니다. 이 구성이 제대로 작동하려면 리포지터리 B에서 오브젝트를 삭제하면 안 됩니다. 리포지터리 A가 해당 오브젝트를 필요로 할 수 있기 때문입니다.
@pools 디렉터리에 저장된 오브젝트 풀 리포지터리에서는 git prune 또는 git gc를 실행하지 마십시오.
이는 오브젝트 풀에 의존하는 일반 리포지터리에서 데이터 손실을 일으킬 수 있습니다.
위험은 git prune에 있으며, git gc는 git prune을 호출합니다.
풀 리포지터리에서 실행될 때 git prune은 오브젝트가 더 이상 필요하지 않은지 신뢰할 수 있게
판단하지 못하는 문제가 있습니다.
GitLab에서의 Git alternates: 풀 리포지터리#
GitLab은 사용자에게 숨겨진 특수 풀 리포지터리를 생성하여 이 오브젝트 빌려오기를 체계적으로 관리합니다. 그런 다음 Git alternates를 사용하여 프로젝트 리포지터리 컬렉션이 단일 풀 리포지터리에서 빌려오도록 합니다. 이러한 프로젝트 리포지터리 컬렉션을 풀이라고 부릅니다. 풀은 단일 풀에서 빌려오는 리포지터리의 별 모양 네트워크를 형성하며, 이는 사용자가 프로젝트를 포크할 때 형성되는 포크 네트워크와 유사하지만(완전히 동일하지는 않습니다).
Git 수준에서 풀 리포지터리는 Gitaly RPC 호출을 사용하여 생성 및 관리됩니다. 일반 리포지터리와 마찬가지로, 어떤 풀 리포지터리가 존재하는지와 어떤 리포지터리가 풀에서 빌려오는지에 대한 권한은 SQL의 Rails 애플리케이션 수준에 있습니다.
결론적으로, Git 수준에서 GitLab 프로젝트 리포지터리 컬렉션 전체에 걸쳐 효과적인 오브젝트 중복 제거를 위해서는 세 가지가 필요합니다:
-
풀 리포지터리가 존재해야 합니다.
-
참여하는 프로젝트 리포지터리는 각각의
objects/info/alternates파일을 통해 풀 리포지터리에 연결되어야 합니다. -
풀 리포지터리는 참여하는 프로젝트 리포지터리에 공통된 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 스토리지 샤드로 이동되면, "PoolRepository에 속함" 관계가 끊어집니다. 샤드 간 리포지터리 이동이 구현된 방식 때문에, 새 스토리지 샤드에서 프로젝트 리포지터리의 신선한 자급자족 복사본을 얻게 됩니다.
-
풀의 소스 프로젝트가 다른 Gitaly 스토리지 샤드로 이동되거나 삭제되더라도 "소스 프로젝트" 관계는 끊어지지 않습니다. 그러나 소스가 동일한 Gitaly 샤드에 있지 않으면 풀은 소스에서 페치하지 않습니다.
SQL 풀 관계와 Gitaly 간의 일관성#
Gitaly 관점에서 SQL 풀 관계는 Gitaly 서버의 상태에 대해 두 가지 유형의 주장을 합니다: 풀 리포지터리 존재 여부, 그리고 리포지터리와 풀 간의 alternates 연결 존재 여부입니다.
풀 존재#
GitLab이 풀 리포지터리가 존재한다고 생각하지만(즉, SQL에 따라 존재하지만) Gitaly 서버에는 없는 경우, Gitaly에 의해 즉시 생성됩니다.
풀 관계 존재#
여기서 잘못될 수 있는 세 가지 경우가 있습니다.
1. SQL은 리포지터리 A가 풀 P에 속한다고 하지만 Gitaly는 A에 alternate 오브젝트가 없다고 함#
이 경우 디스크 공간 절약을 놓치게 되지만 A 자체에 대한 모든 RPC는 정상적으로 작동합니다.
다음에 A에서 가비지 컬렉션이 실행될 때, alternates 연결이 Gitaly에서 설정됩니다.
이는 GitLab Rails의 Projects::GitDeduplicationService에 의해 수행됩니다.
2. SQL은 리포지터리 A가 풀 P1에 속한다고 하지만 Gitaly는 A에 풀 P2의 alternate 오브젝트가 있다고 함#
이 경우 Projects::GitDeduplicationService가 예외를 던집니다.
3. SQL은 리포지터리 A가 어떤 풀에도 속하지 않는다고 하지만 Gitaly는 A가 P에 속한다고 함#
이 경우 Projects::GitDeduplicationService는 DisconnectGitAlternates RPC를 사용하여
리포지터리 A를 "재중복"하려고 시도합니다.
Git 오브젝트 중복 제거와 GitLab Geo#
Geo 프라이머리의 SQL에서 풀 리포지터리 레코드가 생성되면, 이는 결국 Geo 세컨더리에서 이벤트를 트리거합니다. 그러면 Geo 세컨더리는 Gitaly에 풀 리포지터리를 생성합니다. 각 풀 참여자가 동기화됨에 따라 Geo가 결국 세컨더리의 Gitaly에서 가비지 컬렉션을 트리거하고, 이 단계에서 Git 오브젝트가 중복 제거되므로 "결과적 일관성" 상황이 발생합니다.