InfoGrab DocsInfoGrab Docs

GitLab에서 Git 오브젝트 중복 제거가 작동하는 방식

요약

GitLab 사용자가 프로젝트를 포크하면 GitLab은 포크 시점의 원본 프로젝트를 복사한 Git 리포지터리를 가진 새 프로젝트를 생성합니다. Git 수준에서는 Git alternates를 사용해 중복을 제거합니다. 리포지터리 A가 리포지터리 B에서 빌려 오게 하려면 다음과 같이 합니다.

GitLab 사용자가 프로젝트를 포크하면 GitLab은 포크 시점의 원본 프로젝트를 복사한 Git 리포지터리를 가진 새 프로젝트를 생성합니다. 큰 프로젝트가 자주 포크되면 Git 리포지터리 스토리지의 디스크 사용량이 빠르게 늘어날 수 있습니다. 이 문제를 완화하기 위해 GitLab에 포크용 Git 오브젝트 중복 제거를 추가하고 있습니다. 이 문서에서는 GitLab 이 Git 오브젝트 중복 제거를 어떻게 구현하는지 설명합니다.

풀 리포지터리#

Git alternates 이해하기#

Git 수준에서는 Git alternates를 사용해 중복을 제거합니다. Git alternates는 한 리포지터리가 같은 머신에 있는 다른 리포지터리에서 오브젝트를 빌려 올 수 있게 하는 메커니즘입니다.

리포지터리 A가 리포지터리 B에서 빌려 오게 하려면 다음과 같이 합니다.

  1. B.git/objects로 해석되는 경로를 특수 파일 A.git/objects/info/alternates에 기록해 alternates 링크를 만듭니다.
  2. 리포지터리 A에서 git repack을 실행해 리포지터리 B에도 존재하는 리포지터리 A의 모든 오브젝트를 제거합니다.

repack 이후 리포지터리 A는 더 이상 자체 완결적이지 않지만, 자신의 ref와 구성은 그대로 가지고 있습니다. B에 없는 A의 오브젝트는 A에 남습니다. 이 구성이 동작하려면 리포지터리 A가 필요로 할 수 있으므로 리포지터리 B에서 오브젝트를 삭제해서는 안 됩니다.

Warning

@pools 디렉터리에 저장되는 오브젝트 풀 리포지터리에서는 git prune 이나 git gc를 실행하지 않습니다. 해당 오브젝트 풀에 의존하는 일반 리포지터리에서 데이터 손실이 발생할 수 있습니다.

위험은 git prune에 있으며, git gc는 git prune을 호출합니다. 문제는 풀 리포지터리에서 실행되는 git prune 이 어떤 오브젝트가 더 이상 필요하지 않은지를 확실하게 판단할 수 없다는 점입니다.

GitLab에서의 Git alternates: 풀 리포지터리#

GitLab은 사용자에게 보이지 않는 특수한 풀 리포지터리를 생성해 이 오브젝트 빌려 오기를 관리합니다. 그런 다음 Git alternates를 사용해 여러 프로젝트 리포지터리가 하나의 풀 리포지터리에서 빌려 오게 합니다. 이러한 프로젝트 리포지터리의 집합을 풀이라고 부릅니다. 풀은 하나의 풀에서 빌려 오는 별 모양의 리포지터리 네트워크를 이루며, 사용자가 프로젝트를 포크할 때 형성되는 포크 네트워크와 비슷해 보이지만 동일하지는 않습니다.

Git 수준에서 풀 리포지터리는 Gitaly RPC 호출로 생성되고 관리됩니다. 일반 리포지터리와 마찬가지로, 어떤 풀 리포지터리가 존재하고 어떤 리포지터리가 거기에서 빌려 오는지에 대한 권한은 Rails 애플리케이션 수준의 SQL에 있습니다.

정리하면, Git 수준에서 여러 GitLab 프로젝트 리포지터리에 걸쳐 오브젝트 중복 제거를 효과적으로 수행하려면 다음 세 가지가 필요합니다.

  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 스토리지 샤드로 옮겨지면 "PoolRepository에 속함" 관계가 끊어집니다. 샤드 간 리포지터리 이동이 구현된 방식 때문에, 새 스토리지 샤드에는 해당 프로젝트 리포지터리의 자체 완결적인 사본이 새로 만들어집니다.
  • 풀의 소스 프로젝트가 다른 Gitaly 스토리지 샤드로 옮겨지거나 삭제되어도 "소스 프로젝트" 관계는 끊어지지 않습니다. 다만 소스가 같은 Gitaly 샤드에 있지 않으면 풀은 소스에서 가져오지 않습니다.

SQL 풀 관계와 Gitaly 간의 일관성#

Gitaly 관점에서 SQL 풀 관계는 Gitaly 서버의 상태에 대해 두 가지를 주장합니다. 풀 리포지터리가 존재한다는 것과, 리포지터리와 풀 사이에 alternates 연결이 존재한다는 것입니다.

풀 존재#

GitLab 이 풀 리포지터리가 존재한다고 판단하지만(즉, SQL 상으로는 존재하지만) Gitaly 서버에는 없는 경우, Gitaly가 그 자리에서 생성합니다.

풀 관계 존재#

여기에서는 세 가지 문제가 발생할 수 있습니다.

1. SQL은 리포지터리 A가 풀 P에 속한다고 하지만 Gitaly는 A에 alternate 오브젝트가 없다고 하는 경우#

이 경우 디스크 공간 절감 효과는 놓치지만 A 자체에 대한 모든 RPC는 정상적으로 동작합니다. 다음번에 A에서 가비지 컬렉션이 실행되면 Gitaly에 alternates 연결이 만들어집니다. 이 작업은 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 오브젝트가 중복 제거되므로, 최종적 일관성을 갖는 상태가 됩니다.

GitLab에서 Git 오브젝트 중복 제거가 작동하는 방식

GitLab v19.4
원문 보기

요약

GitLab 사용자가 프로젝트를 포크하면 GitLab은 포크 시점의 원본 프로젝트를 복사한 Git 리포지터리를 가진 새 프로젝트를 생성합니다. Git 수준에서는 Git alternates를 사용해 중복을 제거합니다. 리포지터리 A가 리포지터리 B에서 빌려 오게 하려면 다음과 같이 합니다.

GitLab 사용자가 프로젝트를 포크하면 GitLab은 포크 시점의 원본 프로젝트를 복사한 Git 리포지터리를 가진 새 프로젝트를 생성합니다. 큰 프로젝트가 자주 포크되면 Git 리포지터리 스토리지의 디스크 사용량이 빠르게 늘어날 수 있습니다. 이 문제를 완화하기 위해 GitLab에 포크용 Git 오브젝트 중복 제거를 추가하고 있습니다. 이 문서에서는 GitLab 이 Git 오브젝트 중복 제거를 어떻게 구현하는지 설명합니다.

풀 리포지터리#

Git alternates 이해하기#

Git 수준에서는 Git alternates를 사용해 중복을 제거합니다. Git alternates는 한 리포지터리가 같은 머신에 있는 다른 리포지터리에서 오브젝트를 빌려 올 수 있게 하는 메커니즘입니다.

리포지터리 A가 리포지터리 B에서 빌려 오게 하려면 다음과 같이 합니다.

  1. B.git/objects로 해석되는 경로를 특수 파일 A.git/objects/info/alternates에 기록해 alternates 링크를 만듭니다.
  2. 리포지터리 A에서 git repack을 실행해 리포지터리 B에도 존재하는 리포지터리 A의 모든 오브젝트를 제거합니다.

repack 이후 리포지터리 A는 더 이상 자체 완결적이지 않지만, 자신의 ref와 구성은 그대로 가지고 있습니다. B에 없는 A의 오브젝트는 A에 남습니다. 이 구성이 동작하려면 리포지터리 A가 필요로 할 수 있으므로 리포지터리 B에서 오브젝트를 삭제해서는 안 됩니다.

Warning

@pools 디렉터리에 저장되는 오브젝트 풀 리포지터리에서는 git prune 이나 git gc를 실행하지 않습니다. 해당 오브젝트 풀에 의존하는 일반 리포지터리에서 데이터 손실이 발생할 수 있습니다.

위험은 git prune에 있으며, git gc는 git prune을 호출합니다. 문제는 풀 리포지터리에서 실행되는 git prune 이 어떤 오브젝트가 더 이상 필요하지 않은지를 확실하게 판단할 수 없다는 점입니다.

GitLab에서의 Git alternates: 풀 리포지터리#

GitLab은 사용자에게 보이지 않는 특수한 풀 리포지터리를 생성해 이 오브젝트 빌려 오기를 관리합니다. 그런 다음 Git alternates를 사용해 여러 프로젝트 리포지터리가 하나의 풀 리포지터리에서 빌려 오게 합니다. 이러한 프로젝트 리포지터리의 집합을 풀이라고 부릅니다. 풀은 하나의 풀에서 빌려 오는 별 모양의 리포지터리 네트워크를 이루며, 사용자가 프로젝트를 포크할 때 형성되는 포크 네트워크와 비슷해 보이지만 동일하지는 않습니다.

Git 수준에서 풀 리포지터리는 Gitaly RPC 호출로 생성되고 관리됩니다. 일반 리포지터리와 마찬가지로, 어떤 풀 리포지터리가 존재하고 어떤 리포지터리가 거기에서 빌려 오는지에 대한 권한은 Rails 애플리케이션 수준의 SQL에 있습니다.

정리하면, Git 수준에서 여러 GitLab 프로젝트 리포지터리에 걸쳐 오브젝트 중복 제거를 효과적으로 수행하려면 다음 세 가지가 필요합니다.

  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 스토리지 샤드로 옮겨지면 "PoolRepository에 속함" 관계가 끊어집니다. 샤드 간 리포지터리 이동이 구현된 방식 때문에, 새 스토리지 샤드에는 해당 프로젝트 리포지터리의 자체 완결적인 사본이 새로 만들어집니다.
  • 풀의 소스 프로젝트가 다른 Gitaly 스토리지 샤드로 옮겨지거나 삭제되어도 "소스 프로젝트" 관계는 끊어지지 않습니다. 다만 소스가 같은 Gitaly 샤드에 있지 않으면 풀은 소스에서 가져오지 않습니다.

SQL 풀 관계와 Gitaly 간의 일관성#

Gitaly 관점에서 SQL 풀 관계는 Gitaly 서버의 상태에 대해 두 가지를 주장합니다. 풀 리포지터리가 존재한다는 것과, 리포지터리와 풀 사이에 alternates 연결이 존재한다는 것입니다.

풀 존재#

GitLab 이 풀 리포지터리가 존재한다고 판단하지만(즉, SQL 상으로는 존재하지만) Gitaly 서버에는 없는 경우, Gitaly가 그 자리에서 생성합니다.

풀 관계 존재#

여기에서는 세 가지 문제가 발생할 수 있습니다.

1. SQL은 리포지터리 A가 풀 P에 속한다고 하지만 Gitaly는 A에 alternate 오브젝트가 없다고 하는 경우#

이 경우 디스크 공간 절감 효과는 놓치지만 A 자체에 대한 모든 RPC는 정상적으로 동작합니다. 다음번에 A에서 가비지 컬렉션이 실행되면 Gitaly에 alternates 연결이 만들어집니다. 이 작업은 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 오브젝트가 중복 제거되므로, 최종적 일관성을 갖는 상태가 됩니다.