This page contains information related to upcoming products, features, and functionality. It is important to note that the information presented is for informational purposes only. Please do not rely on this information for purchasing or planning purposes. The development, release, and timing of any products, features, or functionality may be subject to change or delay and remain at the sole discretion of GitLab Inc.
Status Authors Coach DRIs Owning Stage Created
proposed @pks-gitlab devops systems 2023-03-30

객체 풀의 디자인 반복

요약

리포지터리를 포크하는 것은 GitLab에 호스팅된 프로젝트의 현대적인 작업 흐름의 핵심입니다. 포크와 상위 프로젝트 사이의 대부분의 객체는 일반적으로 동일할 것이므로, 이는 최적화의 가능성을 열어줍니다.

  • 상위 리포지터리의 많은 부분을 재사용한다면 포크 생성은 이론적으로 매우 빠를 수 있습니다.

  • 공유된 객체를 중복으로 저장하여 저장 공간을 절약할 수 있습니다.

현재 이 아키텍처는 주 리포지터리의 객체를 보유하는 객체 풀로 구현되어 있습니다. 그러나 객체 풀의 디자인은 유기적으로 성장하였으며 현재는 한계를 보여주고 있습니다.

본 청사진은 객체 풀의 디자인을 반복하여 장기간에 걸친 문제를 해결하는 방법을 탐색합니다. 더 나아가, 객체 풀의 구체적인 구현 세부사항에 대해 보다 쉽게 반복할 수 있는 디자인에 도달하는 것이 목표입니다.

동기

현재의 객체 풀 디자인은 다양한 방식으로 확장성 문제를 보여주고 있습니다. 문제의 큰 부분은 객체 풀이 유기적으로 성장했고 우리가 배우며 진행해왔기 때문입니다.

객체 풀의 전반적인 디자인을 수정하는 것이 어려운데 그 이유는 명확한 소유권이 없기 때문입니다. Gitaly는 이들이 작동할 수 있도록 하는 저수준 빌딩 블록을 제공하지만, 구현 세부내용에 대해 반복할 수 있을 정도로 충분한 제어를 가지고 있지 않습니다.

따라서 두 가지 주요 목표가 있습니다: 객체 풀의 소유권을 가져와서 디자인을 더 쉽게 반복할 수 있도록 만들고, 반복할 수 있게 되면 확장성 문제를 해결하는 것입니다.

라이프사이클 소유권

Gitaly는 객체 풀을 관리하기 위한 인터페이스를 제공하지만, 실제로 그들의 라이프사이클은 클라이언트에 의해 제어됩니다. 객체 풀의 전형적인 라이프사이클은 다음과 같습니다.

  1. 객체 풀은 CreateObjectPool()을 통해 생성됩니다. 호출자는 객체 풀이 생성될 경로와 리포지터리가 만들어질 원본 리포지터리를 제공합니다.

  2. 원본 리포지터리는 명시적으로 LinkRepositoryToObjectPool()을 호출하여 객체 풀에 연결되어야 합니다.

  3. 객체 풀은 주 객체 풀 멤버의 모든 변경 사항을 가져오는 FetchIntoObjectPool()을 통해 정기적으로 업데이트되어야 합니다.

  4. 포크를 생성하기 위해 클라이언트는 CreateFork() 다음에 LinkRepositoryToObjectPool()을 호출해야 합니다.

  5. 포크의 리포지터리는 DisconnectGitAlternates()를 호출하여 연결을 해제해야 합니다. 이렇게 하면 객체가 중복으로 저장됩니다.

  6. 객체 풀은 DeleteObjectPool()을 통해 삭제됩니다.

이 라이프사이클은 복잡하며 구현 세부사항을 호출자에게 많이 누설하게 됩니다. 이는 원래 Rails 측에게 Git 객체 가시성을 제어하고 관리하기 위해 일부로 이루어졌습니다. GitLab 프로젝트 가시성 규칙은 복잡하며 Gitaly에게 관련이 없습니다. 이러한 세부사항을 노출함으로써 Rails는 풀 멤버십 링크가 생성되고 해제되는 시점을 제어할 수 있습니다. 현재 시점에서 시스템 전체가 어떻게 작동하는지 명확하지 않으며 그 한계가 명시적으로 문서화되어 있지 않습니다.

라이프사이클 소유권 문제와 관련된 복잡성 외에도 풀 멤버십에 대한 여러 가지 신뢰할 수 있는 출처가 있습니다. Gitaly는 풀 리포지터리의 멤버 세트를 추적하지 않고, 특정 리포지터리가 해당 풀의 일부인지만 확인할 수 있습니다. 결과적으로 Rails는 이 정보를 데이터베이스에서 유지해야 합니다. 그러나 해당 정보를 오래되게 유지하는 것은 어렵습니다.

리포지터리 유지 관리

라이프사이클 소유권 문제와 관련하여 리포지터리 유지 관리의 문제가 있습니다. 객체 풀을 최신 상태로 유지하는 것은 정기적으로 FetchIntoObjectPool()을 호출해야 한다는 것을 의미합니다. 이는 클라이언트로부터 구현 세부사항을 누설시키지만, 이것은 개인 리포지터리가 동기화되지 않도록 하고 결과적으로 다른 리포지터리에 객체가 누출되는 것을 방지하기위해 그렇게 되었습니다.

우리는 디스크 세부사항에 대한 클라이언트의 지식이 필요 없도록하기 위해 리포지터리 유지관리를 Gitaly로 이전하면 좋은 성과를 보았습니다. 이상적으로, 우리는 객체 풀의 주요 멤버인 리포지터리에 대해 동일한 원디스크 상태를 최적화할 경우에 자동으로 객체 풀을 업데이트할 것입니다.

우리를 그렇게 하지 못하게 하는 두 가지 문제가 있습니다.

  • Gitaly는 객체 풀과 그 멤버 간의 관계를 알 지 못합니다.

  • 객체 풀을 업데이트하는 것은 비용이 많이 듭니다.

Gitaly를 객체 풀 멤버십의 유일한 신뢰할 출처로 만들면 두 가지 문제를 해결할 수 있을 것입니다.

빠른 포크

현재의 구현에서는 Rails가 먼저 CreateFork()를 호출하고, 이로써 포크 리포지터리를 생성하기 위해 완전한 git-clone(1)이 수행됩니다. 그런 다음 LinkRepositoryToObjectPool()을 통해 포크를 객체 풀과 연결합니다. 이는 클라이언트로부터 구현 세부사항을 누설시킬 뿐만 아니라, 객체 풀의 전체 잠재적 이점을 누리는 것을 방해합니다.

특히, 포크를 생성하는 것은 링크 이전에 포크를 항상 생성한다는 것 때문에 예상보다 훨씬 느립니다. 포크를 생성하고 포크를 풀 리포지터리에 연결하는 단계를 통합하게 된다면 초기 클론을 피할 수 있을 것입니다.

클러스터화된 객체 풀

Gitaly Cluster와 객체 풀 개발은 서로 겹쳤습니다. 결과적으로 이 둘은 잘 작동하지 않는다는 것이 알려져 있습니다. Praefect는 객체 풀을 가진 리포지터리가 모든 노드에 있는지 확인하지 않으며, 객체 풀의 상태가 알려진 상태인지 확인하지 않습니다. 객체 풀은 우연히만 작동됩니다.

현재 상태는 객체 풀이 누락되거나 노드별로 서로 다른 콘텐츠를 가지는 경우가 있었습니다. 이는 객체 풀 멤버의 일관되게 관찰되는 상태가 되지 못하게 하고, 객체 풀의 콘텐츠에 의존하는 쓰기가 실패하게 할 수 있습니다.

클러스터링된 Gitaly의 경우 객체 풀을 처리하는 방법 중 하나는 풀 리포지터리를 해당 리포지터리에 의존하는 노드들에 중복으로 만들게 하는 것입니다. 이것은 포크 네트워크의 구성원이 서로 다른 노드에 존재할 수 있게 할 것입니다. 이걸 작동시키기 위해서는 리포지터리 복제가 객체 풀을 알고 있어야 하고 특정 노드에 그것들을 중복해서 만들어야 하는지를 알고 있어야 합니다.

요구 사항

어떠한 특정 솔루션에도 제공해야 할 요구사항과 불변식 집합이 있습니다.

개인 상위 리포지터리는 포크에 객체를 누설해서는 안 됩니다.

프로젝트가 공개가 아닌 가시성 설정을 가진 경우, 리포지터리의 객체는 객체 풀로 가져와서는 안 됩니다. 객체 풀은 언젠가는 공개되었던 원 리포지터리의 객체만을 가지도록 하는 것이 좋습니다. 이는 공개된 머지 요청을 통해 개인 상위 리포지터리로부터 포크들로 객체가 누출되지 않도록 합니다.

포크가 객체를 상위 프로젝트로 불법적으로 끼워넣을 수 없어야 합니다.

포크 리포지터리에 업로드된 객체를 공유된 객체 풀을 통해 상위 리포지터리에서 접근할 수 없도록 해야 합니다. 그렇지 않으면 불법적 사용자가 단순히 포크를 만들어서 리포지터리에 객체를 “끼워넣을” 수 있는 가능성이 있습니다.

혼란을 초래할 수 있는데 더불어 이는 깨진 것으로 알려진 객체들을 도입하여 상위 리포지터리를 손상시키는 메커니즘으로 작용할 수 있습니다.

객체 풀 수명이 상위 리포지터리 수명을 초과해야 합니다.

상위 리포지터리가 삭제되면 해당 객체 풀은 여전히 다른 포크 네트워크의 다른 리포지터리들 간의 공유된 객체를 계속하여 중복 저장하는 역할을 해야 합니다. 따라서 객체 풀의 수명은 상위 리포지터리의 수명보다 길다고 할 수 있습니다. 객체 풀은 더 이상 그것을 참조하는 리포지터리가 없을 때에만 삭제되어야 합니다.

객체 수명

포크 네트워크의 객체 풀에 중복으로 저장된 객체들로 인해 리포지터리들은 객체 풀에 의존하게 됩니다. 객체 풀 리포지터리의 누락된 객체들은 포크 네트워크의 리포지터리를 손상시킬 수 있습니다. 따라서 객체 풀의 리포지터리에 있는 객체들은 해당 리포지터리들에 의해 참조되는 한 계속 존재해야 합니다.

풀 리포지터리에서 참조되는 리포지터리가 하나 이상인지를 정확하게 결정하는 메커니즘이 없다면, 풀 리포지터리의 모든 객체들은 유지되어야 합니다. 참조하는 리포지터리가 없을 때에만 풀 리포지터리와 따라서 모든 객체들이 삭제될 수 있습니다.

객체 공유

중복 저장된 객체는 특정 리포지터리의 모든 포크에서 접근 가능해지게 됩니다. 그 객체가 포크 중 어디에서도 도달 가능하지 않은 경우에도 말이죠. 따라서 객체 풀에 대한 어떠한 쓰기 작업은 즉시 해당 멤버들 중 모든 것에 영향을 끼치게 됩니다.

리포지터리와 객체 풀이 복제되고 나서 사용자가 관측할 수 있는 상태가 모든 복제본에서 동일하도록 하기 위해서, 리포지터리와 해당 객체 풀이 다른 노드에 걸쳐 일관성을 유지할 필요가 있습니다.

제안

현재 설계에서는 객체 풀의 관리가 대부분 클라이언트 측에서 처리됩니다. 클라이언트는 객체 풀의 완전한 라이프사이클을 관리해야 하는데, 이는 Rails가 객체 풀 관계를 Rails 데이터베이스에 저장하고 객체 풀의 각 단계를 세밀하게 관리하며 idempotent한 Gitaly RPC를 주기적으로 호출하여 상태를 강제해야 하기 때문입니다. 이러한 설계는 이미 복잡한 메커니즘을 더욱 복잡하게 만듭니다.

클라이언트 측에서 객체 풀의 전체 라이프사이클을 처리하는 대신 본 문서에서는 Gitaly 내부에 객체 풀 라이프사이클 관리를 캡슐화하는 것을 제안합니다. 객체 풀을 유지하는 데 저수준 작업을 수행하는 대신 클라이언트는 리포지터리와 해당 객체 풀 간의 업데이트된 관계에 대해 Gitaly에만 알리면 됩니다.

다음과 같은 여러 가지 이점이 있습니다:

  • 라이프사이클 관리의 복잡성이 Gitaly와 같은 한 곳에 캡슐화됩니다.

  • Gitaly는 앞으로 “대체물”에 비해 더 나은 솔루션을 찾았을 때 객체 풀의 저수준 기술 설계를 수정할 수 있습니다.

  • Gitaly Cluster, 객체 풀 및 리포지터리 유지 관리 간의 더 나은 상호 작용을 보장할 수 있습니다.

  • Gitaly는 객체 풀 관계의 단일 소스로 설정되어 관리를 더 잘할 수 있게 됩니다.

전반적으로, 목표는 클라이언트가 기술적인 세부 정보에 대해 더 적게 걱정해도 되도록 추상화 수준을 높이는 것이며, 동시에 Gitaly가 그것들에 대해 더 나은 위치에서 수정할 수 있도록 하는 것입니다.

객체 풀의 라이프사이클 관리를 Gitaly로 이동

현재 객체 풀의 라이프사이클 관리는 클라이언트에게 너무 많은 세부 정보를 누설하고 있으며, 이로 인해 일부 사항들이 이해하기 어렵고 비효율적해지고 있습니다.

현재 솔루션은 리포지터리와 해당 객체 풀 간의 관계를 관리하는 세밀한 RPC 세트에 의존합니다. 대조적으로 우리는 클라이언트에게 포크의 고수준 개념만 노출시키는 단순화된 접근법을 추구하고 있습니다. 이를 위해 다음과 같이 세 가지 RPC 형태로 이루어집니다:

  • ForkRepository()는 주어진 리포지터리의 포크를 생성합니다. 상위 리포지터리에 객체 풀이 아직 없는 경우 Gitaly는 이를 생성합니다. 그런 다음 새로운 리포지터리를 만들고 자동으로 해당 객체 풀에 연결합니다. 상위 리포지터리는 객체 풀의 주요 멤버로 기록되고 포크는 객체 풀의 보조 멤버로 기록됩니다.

  • UnforkRepository()는 리포지터리를 연결된 객체 풀에서 제거합니다. 이로써 객체의 중복이 중단됩니다. 주요 객체 풀 멤버의 경우 Gitaly가 객체 풀로 새로운 객체를 끌어들이는 것을 중단합니다.

  • GetObjectPool()은 주어진 리포지터리에 대한 객체 풀을 반환합니다. 객체 풀 설명에는 객체 풀의 주요 객체 풀 멤버와 모든 보조 객체 풀 멤버에 관한 정보가 포함됩니다.

또한 다음과 같은 변경 사항이 구현됩니다:

  • RemoveRepository()는 리포지터리를 해당 객체 풀에서 제거합니다. 이것이 마지막 객체 풀 멤버였다면 객체 풀이 제거됩니다.

  • OptimizeRepository()는 주요 객체 풀 멤버에서 실행될 때 객체 풀을 업데이트하고 최적화합니다.

  • ReplicateRepository()는 객체 풀을 인식하고 올바르게 복제해야 합니다. 리포지터리는 필요에 따라 객체 풀에 연결 또는 분리되어야 합니다. 이것은 Praefect world를 수정하기 위한 단계이지만, 우리가 결국 Praefect를 폐기할 계획이므로 불필요한 것으로 보일 수 있지만, 이 RPC 호출은 리포지터리 리밸런싱과 같은 다른 사용 사례에도 사용됩니다.

이러한 변경 사항으로 인해 Gitaly는 객체 풀의 라이프사이클에 훨씬 더 엄격한 제어가 가능해집니다. 또한, 리포지터리의 객체 풀 멤버십을 추적하기 시작함으로써 포크 네트워크의 단일 진실의 소스로 설정될 수 있습니다.

비효율적인 객체 풀 유지관리 수정

Gitaly는 객체 풀로부터 새 객체를 가져오기 위해 기본 객체 풀 멤버로부터 객체를 검색합니다. 그러나 이러한 검색은 비효율적입니다. 왜냐하면 기본 객체 풀 멤버에서 새로운 객체인 객체를 불필요하게 협상해야하기 때문입니다. 그러나 객체는 이미 기본 객체 풀 멤버에서 중복 처리되었기 때문에 해당 객체 풀 멤버의 객체 데이터베이스에는 아직 존재하지 않는 객체만 포함되어 있어야 합니다. 따라서 협상을 완전히 건너 뛰고 소스 리포지터리에 모든 객체를 연결하는 것이 가능해야 합니다.

현재 설계에서, 이러한 객체들은 방금 가져온 객체에 대한 참조를 생성함으로써 유지됩니다. 만약 가져오기가 참조를 삭제하거나 참조를 강제로 업데이트하면 이전에 참조된 객체가 참조되지 않을 수 있습니다. 따라서 Gitaly는 그러한 참조가 절대로 삭제되지 않도록 keep-around 참조를 생성합니다. 또한 그러한 참조는 참조 기반으로 복제되기 때문에 객체 풀의 복제 처리에도 필요합니다.

이 두 가지는 다음과 같은 방법으로 해결할 수 있습니다:

  • preciousObjects 리포지터리 확장을 설정할 수 있습니다. 이렇게 하면 이 확장을 이해하는 모든 Git 버전은 git-prune(1) 또는 유사한 명령을 실행하더라도 모든 객체를 삭제하지 않도록 지시합니다. 이 확장을 이해하지 못하는 Git 버전은 이 리포지터리에서 작동할 수 없습니다.

  • git-fetch(1)를 통해 객체 풀을 복제하는 대신 객체 데이터베이스의 모든 객체를 전송하여 객체 풀을 복제할 수 있습니다.

이러한 결합으로 인해 객체 풀에서 참조를 쓰지 않을 수 있습니다. 새로운 객체를 모두 연결함으로써 객체 풀의 효율적인 업데이트가 가능해지고 객체 풀에서 참조의 무한 증가 문제를 해결할 수 있습니다.

설계 및 구현 세부 사항

객체 풀의 라이프사이클 관리를 Gitaly로 이동

언급한 바대로, 목표는 객체 풀의 소유권을 Gitaly로 이동하는 것입니다. 이상적인 상황에서는 객체 풀의 개념은 호출자에게 노출되지 않아야 합니다. 대신 서로 객체를 공유하면서 리포지터리 네트워크의 고수준 개념만 노출되기를 원합니다.

다음 하위 섹션에서는 현재 객체 풀 기반 아키텍처를 검토한 후 새로운 객체 중복 네트워크 기반 아키텍처를 제안합니다.

객체 풀 기반 아키텍처

현재 아키텍처에서 객체 풀의 라이프사이클을 관리하기 위해 다수의 RPC 호출이 필요하며 호출 측에서 많은 지식이 필요합니다. 다음 순서도는 객체 풀의 간소화된 라이프사이클을 고려한 단순화된 버전을 보여줍니다. 이 것은 단일 객체 풀 멤버만 고려한다는 점에서 단순화되었습니다.

sequenceDiagram Rails->>+Gitaly: CreateObjectPool Gitaly->>+Object Pool: Create activate Object Pool Object Pool-->>-Gitaly: Success Gitaly-->>-Rails: Success Rails->>+Gitaly: LinkRepositoryToObjectPool Gitaly->>+Upstream: Link Upstream-->>-Gitaly: Success Gitaly-->-Rails: Success Rails->>+Gitaly: OptimizeRepository Gitaly->>+Upstream: Optimize Upstream-->-Gitaly: Success Gitaly-->-Rails: Success Rails->>+Gitaly: CreateFork Gitaly->>+Fork: Create activate Fork Fork-->>-Gitaly: Success Gitaly-->>-Rails: CreateFork note over Rails, Fork: Fork exists but is not connected to the object pool. Rails->>+Gitaly: LinkRepositoryToObjectPool Gitaly->>+Fork: Link Fork-->>-Gitaly: Success Gitaly-->-Rails: Success note over Rails, Fork: Fork is connected to object pool, but objects are duplicated. Rails->>+Gitaly: OptimizeRepository Gitaly->>+Fork: Optimize Fork-->-Gitaly: Success Gitaly-->-Rails: Success loop Regularly note over Rails, Fork: Rails needs to ensure that the object pool is regularly updated. Rails->>+Gitaly: FetchIntoObjectPool Gitaly->>+Object Pool: Fetch Object Pool-->>-Gitaly: Success Gitaly-->>-Rails: Success end alt Disconnect Fork note over Rails, Fork: Forks can be disconnected to stop deduplicating objects. Rails->>+Gitaly: DisconnectGitAlternates Gitaly->>+Fork: Disconnect Fork-->>-Gitaly: Success Gitaly-->>-Rails: Success else Delete Fork note over Rails, Fork: Or the fork is deleted eventually. Rails->>+Gitaly: RemoveRepository Gitaly->>+Fork: Remove Fork-->>-Gitaly: Success deactivate Fork Gitaly-->>-Rails: Success end Rails->>+Gitaly: DisconnectGitAlternates Gitaly->>+Upstream: Disconnect Upstream-->>-Gitaly: Success Gitaly-->>-Rails: Success Rails->>+Gitaly: DeleteObjectPool Gitaly->>+Object Pool: Remove Object Pool-->>-Gitaly: Success deactivate Object Pool Gitaly-->>-Rails: Success

다음 단계가 객체 풀을 생성하는 데 관여됩니다:

  1. 객체 풀은 CreateObjectPool()을 호출하여 상위 리포지터리에서 생성됩니다. 이 때 첫 번째 리포지터리에 있는 모든 객체가 포함됩니다.
  2. 상위 리포지터리는 LinkRepositoryToObjectPool()을 호출하여 객체 풀에 연결됩니다. 해당 객체는 자동으로 중복 처리되지 않습니다.
  3. 상위 리포지터리의 객체는 OptimizeRepository()를 호출하여 중복 처리됩니다.
  4. CreateFork()를 호출하여 포크가 생성됩니다. 이 RPC 호출은 인풋으로 상위 리포지터리만 가져오므로 이미 생성된 객체 풀에 대해 알지 못합니다. 따라서 두 번째 전체 객체 복사를 수행합니다.
  5. 포크와 객체 풀은 LinkRepositoryToObjectPool()을 호출하여 연결됩니다. 이러한 작업은 포크의 info/alternates 파일을 작성하여 추가 객체 데이터베이스에 대해 알게 하지만, 순간적인 중복 처리를 일으키지는 않습니다.
  6. 포크의 객체는 OptimizeRepository()를 호출하여 중복 처리됩니다.
  7. 호출 측은 이제 상위 리포지터리에서 FetchIntoObjectPool()를 정기적으로 호출하여 새 객체를 객체 풀로 가져오는지 확인해야 합니다. 가져온 객체는 자동으로 상위 리포지터리에서 중복 처리되지 않습니다.
  8. 포크는 객체를 중복 처리하지 않도록 분리할 수 있습니다:
    • 명시적으로 DisconnectGitAlternates()를 호출하여 info/alternates 파일을 제거하고 모든 객체를 다시 복제합니다.
    • 포크를 완전히 삭제할 수 있는 RemoveRepository()를 호출합니다.
  9. 객체 풀이 비어 있는 경우 DeleteObjectPool()을 호출하여 제거해야 합니다.

전체적으로 여기에 기술된 라이프사이클 관리는 잘 추상화되지 않았으며, 클라이언트들이 많은 부분을 인식해야 합니다. 또한, 객체 풀 멤버십에 대한 여러 진실 소스가 존재하고(사실상 작동하는)지(및 실제로) 차이가 날 수 있습니다.

객체 중복 제거 네트워크 기반 아키텍처

제안된 새 아키텍처는 객체 풀의 개념을 완전히 제거하여 이 프로세스를 단순화합니다. Gitaly는 대신 “객체 중복 제거 네트워크”의 고수준 개념을 노출합니다. 리포지터리는 두 가지 역할 중 하나로 이러한 네트워크에 참여할 수 있습니다:

  • 읽기-쓰기 객체 중복 제거 네트워크 구성원은 정기적으로 객체 중복 제거 네트워크의 일부인 객체 집합을 업데이트합니다.
  • 읽기 전용 객체 중복 제거 네트워크 구성원은 매뉴얼 멤버로써 객체 중복 제거 네트워크의 일부인 객체 집합을 업데이트하지 않습니다.

따라서 객체 중복 제거 네트워크의 구성원 간에 중복 처리할 수 있는 객체 집합은 읽기-쓰기 멤버에서 가져온 객체로만 구성됩니다. 모든 구성원은 역할에 관계없이 중복 처리의 혜택을 받습니다. 일반적으로:

  • 원본 상위 리포지터리는 객체 중복 제거 네트워크의 읽기-쓰기 멤버로 지정됩니다.
  • 포크는 읽기 전용 객체 중복 제거 네트워크 구성원입니다.

객체 중복 제거 네트워크가 읽기 전용 멤버만으로 구성되는 것도 유효합니다. 이 경우 네트워크는 새로 공유된 객체로 업데이트되지 않지만 기존의 공유 객체는 계속 사용됩니다.

객체 풀은 계속해서 기저 메커니즘으로 남지만, 더 높은 추상화 수준은 우리가 그것을 바꾸기로 결정할 경우에도 그 메커니즘을 바꿀 수 있게 합니다.

Gitaly의 클라이언트들은 객체 풀 기반 아키텍처에서 객체 풀의 세부적인 라이프사이클 관리를 수행해야 했지만, 객체 중복 제거 네트워크 기반 아키텍처에서는 객체 중복 제거 네트워크의 멤버쉽만 관리하면 됩니다. 다음 다이어그램은 객체 중복 제거 네트워크 기반 아키텍처에서 객체 풀 기반 아키텍처에 해당하는 흐름을 보여줍니다.

sequenceDiagram Rails->>+Gitaly: CreateFork Gitaly->>+Object Pool: Create activate Object Pool Object Pool -->>-Gitaly: Success Gitaly->>+Fork: Create Fork->>+Object Pool: Join Object Pool-->>-Fork: Success Fork-->>-Gitaly: Success activate Fork Gitaly-->>-Rails: CreateFork loop 정기적으로 Rails->>+Gitaly: OptimizeRepository Gitaly->>+Fork: Optimize Gitaly->>+Object Pool: Optimize Object Pool-->>-Gitaly: Success Fork-->>-Gitaly: Success Gitaly-->>-Rails: Success end alt 포크를 연결 해제 Rails->>+Gitaly: RemoveRepositoryFromObjectDeduplicationNetwork Gitaly->>+Fork: Disconnect alt 마지막 멤버 Gitaly->>+Object Pool: Remove Object Pool-->>-Gitaly: Success end Fork-->>-Gitaly: Success Gitaly-->>-Rails: Success else 포크 삭제 Rails->>+Gitaly: RemoveRepository Gitaly->>+Fork: Remove alt 마지막 멤버 Gitaly->>+Object Pool: Remove Object Pool-->>-Gitaly: Success end Fork-->>-Gitaly: Success deactivate Fork Gitaly-->>-Rails: Success end

다음은 주요 단계입니다:

  1. 포크가 생성되는데, 이 요청은 상위 플젝 및 포크 리포지터리가 객체 중복 제거 네트워크에 참여하도록 지시합니다. 상위 프로젝트가 이미 객체 중복 제거 네트워크의 일부인 경우, 포크도 해당 객체 중복 제거 네트워크에 참여합니다. 그렇지 않은 경우, Gitaly는 객체 풀을 생성하고 상위 리포지터리를 읽기-쓰기 멤버로, 그리고 포크를 읽기 전용 멤버로 참여시킵니다. 포크의 객체는 즉시 중복 처리됩니다. Gitaly는 두 리포지터리의 멤버십을 객체 풀에 기록합니다.
  2. 클라이언트는 이미 알고 있는 것이지만 상위 프로젝트나 포크 프로젝트 중 하나에 대해 정기적으로 OptimizeRepository()를 호출하게 됩니다. 객체 중복 제거 네트워크의 구성원 역할에 따라 동작이 변경됩니다:
    • 읽기-쓰기 객체 중복 제거 네트워크 멤버에서 실행되는 경우, 객체 풀은 일련의 휴리스틱에 따라 업데이트될 수 있습니다. 이는 읽기-쓰기 객체 중복 제거 네트워크 멤버에서 새로 생성된 객체를 객체 풀에 가져와서 해당 네트워크의 모든 구성원이 사용할 수 있도록 만듭니다.
    • 읽기 전용 객체 중복 제거 네트워크 멤버에서 실행되는 경우, 객체 풀은 업데이트되지 않으므로, 읽기 전용 객체 중복 제거 네트워크 멤버에만 속한 객체가 다른 멤버들 간에 공유되지 않습니다. 그러나 필요한 경우 객체 풀은 여전히 최적화될 수 있습니다. 예를 들어, 객체를 다시 패킹(packing)하는 등 필요한 대로 최적화될 수 있습니다.
  3. 상위 프로젝트 및 포크 프로젝트는 RemoveRepositoryFromObjectNetwork()를 호출하여 객체 중복 제거 네트워크를 떠날 수 있습니다. 이렇게 하면 모든 객체가 중복 처리되고 리포지터리는 객체 풀에서 연결이 끊깁니다. 또한, 리포지터리가 읽기-쓰기 객체 중복 제거 네트워크 멤버였던 경우, Gitaly는 해당 리포지터리를 풀을 업데이트하는 소스로 사용하지 않게 됩니다.

    대조적으로, 포크는 RemoveRepository()를 호출하여 삭제될 수 있습니다.

    이 두 호출은 객체 풀에서 리포지터리의 멤버십을 업데이트하게 됩니다. 만약 멤버가 더 이상 남아있지 않다면, Gitaly는 객체 풀을 삭제합니다.

이 제안된 흐름을 통해 객체 풀의 생성, 유지관리, 및 제거는 Gitaly 내부에서 불투명하게 처리됩니다. 더불어, 다음과 같은 두 가지 RPC도 추가로 제공될 수 있습니다:

  • AddRepositoryToObjectDeduplicationNetwork(): 기존 리포지터리가 지정된 역할을 가지고 객체 중복 제거 네트워크에 참여할 수 있도록 합니다.
  • ListObjectDeduplicationNetworkMembers(): 리포지터리가 멤버로 참여하고 있는 객체 중복 제거 네트워크의 모든 구성원과 역할을 나열합니다.

객체 중복 제거 네트워크 기반 아키텍처로의 이전

객체 중복 제거 네트워크 기반 아키텍처로의 이전은 많은 작은 단계들을 포함합니다:

  1. CreateFork()는 자동으로 기존 객체 풀에 링크를 시작합니다. 이는 빠른 포크를 가능하게 하고 포크를 생성할 때 객체 풀에 대한 호출자의 객체 풀 개념을 제거합니다.
  2. AddRepositoryToObjectDeduplicationNetwork()RemoveRepositoryFromObjectDeduplicationNetwork()을 도입합니다. AddRepositoryToObjectPool()DisconnectGitAlternates()을 더 이상 사용하지 않게 하고 Rails가 새로운 RPC를 사용하도록 이전합니다. 객체 중복 제거 네트워크는 리포지터리를 통해 식별되므로 멤버십을 처리할 때 객체 풀의 개념을 삭제합니다.
  3. CreateFork(), AddRepositoryToObjectDeduplicationNetwork(), RemoveRepositoryFromObjectDeduplicationNetwork(), 그리고 RemoveRepository()에서 객체 중복 제거 네트워크의 멤버십을 기록하기 시작합니다. 이 정보는 Gitaly가 객체 풀 라이프사이클을 제어할 수 있게 합니다.
  4. 모든 객체 풀의 구성원에 대한 최신 정보가 Gitaly에 있도록 이전을 구현합니다. Gitaly가 객체 풀의 읽기-쓰기 멤버로부터 자동으로 객체를 가져올 수 있게 하고, 비어있는 객체 풀을 자동으로 삭제할 수 있도록 하기 위해 전환이 필요합니다.
  5. OptimizeRepository()를 변경하여 리포지터리에 연결된 객체 풀을 최적화하도록 수정하여 FetchIntoObjectPool()을 폐기하고 최종적으로 삭제할 수 있도록 합니다.
  6. RemoveRepositoryFromObjectDeduplicationNetwork()RemoveRepository()를 수정하여 비어있는 객체 풀을 제거합니다.
  7. CreateFork()를 수정하여 자동으로 객체 풀을 생성하도록 하여 CreateObjectPool() RPC를 제거합니다.
  8. Gitaly 공개 API에서 ObjectPoolService 및 객체 풀의 개념을 제거합니다.

물론 이 계획은 변경될 수 있습니다.

Gitaly 클러스터 관련 사항

리포지터리 생성

리포지터리가 처음으로 포크될 때, Rails는 CreateObjectPool() RPC를 통해 객체 풀을 생성합니다. 이는 객체 풀 생성이 Gitaly 외부에서 처리된다는 것을 의미합니다. 그 후 객체 풀은 상위 및 포크 리포지터리에 연결됩니다. 리포지터리의 Git alternates 파일이 다른 리포지터리에 연결되도록 구성된 경우, 이 두 리포지터리는 동일한 물리적 리포지터리에 있어야 합니다.

리포지터리와 해당 객체 풀이 동일한 물리적 리포지터리에 있는 것은 Praefect에게 매우 중요합니다. 복제 팩터는 리포지터리의 복제에 영향을 주는 구성으로, 이는 Praefect 가상 리포지터리의 리포지터리별 복제 개수를 제어합니다. 기본적으로 복제 팩터는 Praefect의 리포지터리 수와 동일합니다. 이는 기본 복제 팩터를 사용할 때 리포지터리가 클러스터 내의 모든 리포지터리에서 사용 가능하다는 것을 의미합니다. 사용자 정의 복제 팩터를 사용하면 레플리카 수를 줄여서 리포지터리가 Praefect의 일부 리포지터리에만 존재하도록 할 수 있습니다.

Gitaly 클러스터는 리포지터리 및 할당된 리포지터리를 Praefect PostgreSQL 데이터베이스에 지속적으로 저장합니다. 데이터베이스는 가상 리포지터리에 새로운 리포지터리가 생성될 때 업데이트됩니다. 새 리포지터리가 생성되면 복제 팩터는 리포지터리에 임의로 할당되는 리포지터리 수를 지정합니다. 다음 시나리오는 사용자 정의 복제 팩터가 객체 풀에 문제를 일으킬 수 있는 방법을 보여줍니다:

  1. 다섯 개의 리포지터리 노드를 가진 Gitaly 클러스터에 새 리포지터리가 생성됩니다. 복제 팩터가 3으로 설정되어 있으므로 Praefect에서 이 새 리포지터리에 임의로 선택된 3개의 리포지터리가 할당됩니다. 예를 들어, 할당은 리포지터리 1, 2, 3에 이뤄집니다. 리포지터리 4와 5에는 이 리포지터리의 복사본이 없습니다.
  2. 리포지터리가 처음으로 포크되어 CreateObjectPool() RPC를 통해 객체 풀 리포지터리를 생성해야 할 때, 복제 팩터가 3으로 설정되어 있으므로 Praefect에서 새 객체 풀 리포지터리에 대한 또 다른 임의로 선택된 3개의 리포지터리가 할당됩니다. 예를 들어, 객체 풀 리포지터리는 리포지터리 3, 4, 5에 할당됩니다. 이러한 할당은 상위 리포지터리의 할당과 완전히 일치하지 않음에 유의하십시오.
  3. 포크된 리포지터리의 사본이 CreateFork() RPC를 통해 생성되고 또한 임의로 선택된 3개의 리포지터리에 할당됩니다. 예를 들어, 포크 리포지터리는 리포지터리 1, 3, 5에 할당됩니다. 이러한 할당은 상위 및 객체 풀 리포지터리의 리포지터리 할당과 완전히 일치하지 않습니다.
  4. 상위 및 포크 리포지터리는 LinkRepositoryToObjectPool()를 별도로 호출하여 객체 풀에 연결됩니다. 이 RPC가 성공하려면 해당 리포지터리와 동일한 리포지터리에 객체 풀이 있어야 합니다. 상위 리포지터리는 리포지터리 1과 2에서 연결에 실패합니다. 포크 리포지터리는 리포지터리 2에서 연결에 실패합니다. LinkRepositoryToObjectPool() RPC는 트랜잭션이 아닙니다. 따라서 어떤 리포지터리에서 RPC가 실패하면 오류가 클라이언트로 다시 전달됩니다. 따라서 이 시나리오에서 상위 및 포크 리포지터리의 LinkRepositoryToObjectPool()는 항상 오류 응답으로 끝납니다.

이 문제를 해결하려면 Praefect가 항상 CreateObjectPool()CreateFork() RPC 요청을 상위 리포지터리와 동일한 리포지터리 집합으로 라우팅하도록 보장해야 합니다. 이는 이러한 리포지터리가 항상 필요한 객체 풀 리포지터리를 가지고 있어 연결에 성공할 수 있도록 보장합니다.

이러한 방법의 주요 단점은 객체 중복 제거 네트워크의 리포지터리가 동일한 리포지터리 집합에 고정된다는 것입니다. 이는 객체 중복 제거 네트워크가 커짐에 따라 개별 리포지터리에 불균형한 부하를 줄 수 있습니다. 나중에 Praefect가 필요한 곳에 객체 풀을 생성할 수 있지만 이미 있는 경우 이를 피할 수 있습니다.

리포지터리 복제

ReplicateRepository() RPC는 객체 풀을 인식하지 않고 소스 리포지터리에서만 복제합니다. 이는 소스 리포지터리에 연결된 객체 풀 리포지터리를 복제하면 Git alternates 파일이 없고 따라서 객체의 중복이 없는 대상 리포지터리가 생성되는 것을 의미합니다.

ReplicateRepository() RPC에는 두 가지 주요 용도가 있습니다.

  • GitLab API에서 수행된 리포지터리 이동은 ReplicateRepository() RPC를 통해 리포지터리를 다른 리포지터리로 복제합니다. 현재 이 RPC는 객체 풀을 인식하지 않으므로 대상 리포지터리의 복제본에는 소스 리포지터리로부터 Git alternates 파일이 복제되지 않거나 객체 풀이 다시 생성되지 않습니다. 대신, 복제본은 항상 소스 리포지터리의 완전한 자체 복사본입니다. 따라서 객체 중복 제거 네트워크의 리포지터리를 다른 리포지터리로 이동할 때, 복제된 리포지터리는 대상 리포지터리에서 객체 중복이 없어져 저장 공간을 더 차지할 수 있습니다.
  • Praefect에서 리포지터리 복사본이 오래되었을 때, ReplicateRepository() RPC는 오래된 복사본을 최신 복사본으로부터 내부적으로 복제합니다. 복제 작업은 복사본이 오래되었을 때 Praefect 복제 관리자에 의해 대기열에 들어갑니다. ReplicateRepository() RPC는 객체 풀을 인식하지 않지만 복제 작업은 소스 리포지터리가 객체 풀에 연결되어 있는지 확인합니다. 소스 리포지터리가 연결된 경우, 작업은 대상 리포지터리에서 해당하는 Git alternates 파일을 다시 생성합니다. 그러나 대상 리포지터리에 이미 해당 객체 풀이 없는 경우 복제가 항상 실패합니다. 이는 복사본이 영구적으로 오래될 수 있다는 것을 의미합니다.

소스 리포지터리에서 필요한 객체 풀은 ReplicateRepository() RPC를 통해 대상 리포지터리로 복제되어야 합니다. 이는 객체 중복 제거 네트워크의 리포지터리에 대한 객체 중복을 보존합니다. GitLab API에서 수행되는 리포지터리 이동은 객체 풀 관계를 제거하기 때문에 대상 리포지터리에서 객체 풀을 다시 생성하면 고아 상태의 객체 풀이 생기게 됩니다. ReplicateRepository() RPC의 새 객체 풀 복제 동작은 클라이언트에 의해 제어되어야 하며 이에 따른 변경 사항을 방지해야 합니다. 리포지터리 이동을 위한 객체 풀 복제는 다음 중 하나가 되면 가능합니다:

  • Rails 측이 객체 풀 관계를 보존하도록 업데이트된 경우
  • 객체 풀 수명 주기가 Gitaly 내에서 관리되는 경우

객체 풀 복제에 대해 Praefect가 처리할 수 있어야 하는 시나리오가 있습니다. 모든 관리 중인 리포지터리를 Praefect의 PostgreSQL 데이터베이스에 추적하여 항상 최신 상태를 유지해야 합니다.

  • 객체 풀과 연결된 외부 소스 리포지터리의 복제가 Gitaly 클러스터로 이루어질 수 있습니다. 이에는 대상 가상 리포지터리의 Praefect가 새 객체 풀 리포지터리를 생성해야 하는 상황이 발생할 수 있습니다. 이를 처리하기 위해 소스 리포지터리가 객체 풀을 사용하는지 여부를 파악해야 합니다. 그런 다음 Praefect의 repositories 데이터베이스 테이블에 해당 객체 풀 리포지터리를 위한 항목이 있는지 확인하고, 없는 경우 생성해야 합니다. 이후에 객체 풀에 대한 Praefect 리포지터리 할당이 생성되어야 하며 이를 repository_assignments 데이터베이스 테이블에 유지해야 합니다.
  • 대상 리포지터리의 Praefect에 이미 필요한 객체 풀이 있는지 보장할 수 없습니다. 따라서 개별 리포지터리가 해당 객체 풀에 할당될 수 있습니다. 이러한 새로운 할당은 Praefect의 객체 풀 리포지터리에도 추적되어야 합니다. 이를 처리하기 위해 Praefect는 대상 리포지터리에 필요한 객체 풀이 없을 때 감지해야 하며 이 새로운 할당을 repository_assignments 데이터베이스 테이블에 유지해야 합니다.

디자인의 문제점

이전에 언급했듯이, 객체 풀은 완벽한 해결책이 아닙니다. 이 섹션에서는 가장 중요한 문제점을 다룹니다.

라이프사이클 관리의 복잡성

객체 풀의 라이프사이클은 Gitaly에 완전히 소유될 때 처리하기 쉬워지지만 여전히 복잡하며 다양한 방법으로 고려해야 합니다. 리포지터리의 객체 풀을 처리하는 것은 그들의 리포지터리와 결합하여 원자적인 작업이 아닙니다. 필연적으로 어떤 조치도 적어도 두 가지 다른 리소스에 걸칩니다.

성능 문제

객체 풀이 객체를 중복으로 처리하기 때문에, 최종 결과는 객체 풀 구성원이 단일 팩파일의 모든 객체를 보유하지 않게 됩니다. 이것은 주된 객체 풀 구성원에게 일반적으로 문제가 되지 않지만 정의상 객체 풀의 내용과 다르게 향할 수 있는 보조 객체 풀 구성원에서는 문제가 됩니다.

이로 인해 보조 객체 풀 구성원에는 두 개의 서로 다른 Reachable 객체 집합이 생깁니다. 유감스럽게도 Git 자체의 제약으로 인해 이로써 최적화의 일부분을 사용할 수 없게 됩니다:

  • 이미 델타화된 객체들을 제공하기 위해 팩파일을 효율적으로 재사용할 수 없습니다. 이는 객체 풀에서 객체 풀로부터 벗어난 객체들을 위해 Git이 실시간으로 델타를 재계산해야 함을 의미합니다.

  • 팩파일 비트맵은 여러 객체 데이터베이스에 대해 커버하기가 불가능하거나 쉽게 실행 가능하지 않기 때문에 객체 풀 내에서만 존재할 수 있습니다. 이로써 많은 작업 및 특히 fetch를 제공할 때, Git이 객체 그래프의 더 큰 부분을 탐색해야 하는 요구됩니다.

리포지터리 간 종속적인 쓰기

객체 풀의 설계는 모든 리포지터리에 대한 변경에 대해 쓰기 전 로그를 사용하는 Raft 환경에 중요한 복잡성을 도입합니다. 이상적인 경우에는 Raft 기반 설계에서 요청을 고려할 때 하나의 리포지터리에 대한 쓰기 전 로그에만 관심을 가져야 할 것입니다. 그러나 객체 풀을 사용하다 보니 풀링된 리포지터리에 대한 모든 쓰기와 읽기가 적용될 수 있는 쓰기 전 로그에 종속적으로 고려해야 하는 것입니다.

대안 솔루션

제시된 솔루션은 분명히 최선의 선택이 아니며, 복잡성(라이프사이클 관리)과 성능(비효율적인 풀 구성원들에 대한 fetch 제공)에 문제가 있습니다.

이 섹션에서는 객체 풀에 대한 대안과 새로운 타깃 아키텍처로 선택되지 않은 이유를 탐구합니다.

객체 풀을 완전히 사용 중지

모든 복잡성을 피하는 명백한 방법은 객체 풀을 완전히 사용 중지하는 것입니다. 아키텍처를 크게 단순화할 수 있어 엔지니어링적인 관점에서 매력적이지만 제품적 관점에서는 효율적인 포킹 워크플로우를 지원할 수 없게 되므로 실용적인 방법은 아닙니다.

주 리포지터리를 객체 풀로 사용

명시적인 객체 풀 리포지터리 대신, 기존 리포지터리를 모든 포크의 대체 객체 데이터베이스로 사용할 수 있습니다. 이는 객체 풀의 수명을 관리하는 데 관련하여 많은 복잡성을 피하게 해줍니다. 또한 이렇게 하면 객체 풀을 어떻게 업데이트할지에 대한 문제를 항상 상위 리포지터리의 내용과 항상 일치하도록 해결할 수 있습니다.

그러나 이 방법에는 몇 가지 단점이 있습니다:

  • 이제 리포지터리는 일부 리포지터리가 객체를 소거할 수 있고 다른 리포지터리는 그렇지 못하게 하는 다른 상태를 가질 수 있습니다. 이는 불확실성의 요소를 도입하고 실수로 객체를 삭제하여 포크를 손상시킬 수 있게 합니다.

  • 상위 리포지터리가 비공개로 전환되면 포크 네트워크 구성원 간에 중복 제거된 객체 집합을 동결할 필요가 있습니다. 따라서 어쩌면 결국 객체 풀을 만들어야 할 수 있습니다.

  • 리포지터리 삭제가 더 복잡해지게 됩니다. 리포지터리가 포크에 의해 참조되는지 여부를 고려해야 합니다.

참조 네임스페이스

gitnamespaces(7)를 사용하면 참조를 다른 네임스페이스 집합으로 분할하는 메커니즘을 제공합니다. 이를 통해 모든 포크를 포함하는 단일 리포지터리에서 모든 포크를 제공할 수 있습니다.

한 가지 장점은 모든 포크에 의해 참조된 객체들에 대해 전역적인 객체 데이터베이스의 점검을 할 수 있습니다. 따라서 우리는 한 번에 모든 포크에 대해 객체를 삭제하는 등의 공동 가관을 쉽게 수행할 수 있습니다. 객체에 대해서는 가능한 최적화된 솔루션이 될 것으로 보입니다.

그러나 몇 가지 단점이 있습니다:

  • 사용량 할당량을 계산하는 것은 객체들의 실제 도달성을 반드시 고려해야 하기 때문에 계산 비용이 많이듭니다. 이것이 결정적인 장벽은 아니지만 염두에 두어야 할 사항입니다.

  • 명시적인 요구 사항 중 하나는 포크에서 다른 리포지터리에 객체를 도달 가능하게 만들 수 없도록 하는 것입니다. 이 속성은 이론적으로 참조되는 객체에만 액세스할 수 있도록 허용함으로써 강제될 수 있습니다. 이 방법은 도달성 검사가 너무 계산 집중적이기 때문에 실용적이지 못합니다.

  • 참조가 분할되더라도 대규모 포크 네트워크는 여전히 많은 수의 참조를 가지게 될 것입니다. 이러한 경우의 성능에 미치는 영향이 불분명합니다.

  • 리포지터리 수준의 공격에 대한 폭탄 범위가 크게 증가될 것입니다. 여러분은 개인 리포지터리뿐만 아니라 모든 포크까지 영향을 미치게 됩니다.

  • 사용자 정의 후크는 각 가상 리포지터리에 대해 격리되어야 합니다. Git 후크의 실행이 컨트롤되므로 각 네임스페이스에 대해 처리할 수 있는 가능성이 있습니다.

파일 시스템 기반 중복

파일 시스템 수준에서 객체를 중복하는 것은 여러 차례에 걸쳐 떠들어졌습니다. 다른 컴포넌트에게 이 작업의 부담을 떠넘길 수 있다면 멋진 일이겠지만, Git의 작동 방식 때문에 구현하기가 어려울 것으로 생각됩니다.

리포지터리 크기에 가장 중요한 영향을 미치는 요소는 Git 객체들입니다. 객체를 그들의 루즈 표현으로 저장하고 이로써 중복을 할 수 있겠지만, 이것은 실행 불가능합니다:

  • Git은 객체의 델타를 계산할 수 없게 될 것입니다. 이것은 디스크 크기를 줄이는 매우 중요한 메커니즘이기 때문에 중복 제거로 인한 크기 감소가 델타화 기능에 의한 크기 감소를 눌러야 함은 확실하지 않습니다.

  • 루즈 객체는 리포지터리에 액세스하는 데 훨씬 효율적이지 않습니다.

  • Fetch 제공은 팩파일을 클라이언트에게 보내야하기 때문에 필요합니다. 일반적으로 Git은 이미 존재하는 팩파일의 큰 부분을 재사용하여 계산 오버헤드를 크게 줄일 수 있습니다.

따라서 루즈 객체 수준에서 중복을 제거하는 것은 실현 가능하지 않습니다.

다른 유닛으로 중복을 시도할 수 있는 것은 팩파일입니다. 그러나 팩파일은 Git에 의해 결정론적으로 생성되지 않으며 리포지터리들이 서로 다르게 변하기 시작하면 더 이상 다르게 될 것입니다. 따라서 팩파일은 파일 시스템 수준의 중복에 자연스럽게 맞지 않습니다.

다른 대안은 리포지터리 간에 팩파일의 하드 링크를 사용하는 것이 될 수 있습니다. 이는 어떤 리포지터리가 객체의 리팩을 수행하기로 결정하면 저장 공간을 복제해야 하므로 예측 불가능하고 관리하기 어려울 것입니다.

사용자 정의 객체 백엔드

원리상으로 객체를 중복 저장할 수 있는 사용자 정의 객체 백엔드를 구현하는 것이 가능할 것으로 보입니다. 그러나 상당한 상향식 투자 없이는 이를 수행하기가 어렵습니다:

  • Git은 현재 객체에 대해 다른 백엔드를 가질 수 없도록 설계되어 있지 않습니다. 객체 데이터베이스의 파일 액세스가 코드 기반에 흩어져 있으며 추상화 수준이 없습니다. 이것은 적어도 어떤 수준의 추상화를 가지고 있는 참조 데이터베이스와는 다릅니다.

  • 사용자 정의 객체 백엔드를 구현하는 것은 아마도 Git 프로젝트의 포크를 필요로 할 것입니다. 심지어 이를 수행할 리소스가 있더라도 이것은 상류 변경으로 인한 호환성 문제로 인해 주요한 위험 요소를 도입할 것입니다. 일반적으로 GitLab을 패키징하는 Linux 배포 환경에서 요구되는 요구 사항 중 하나가 종종 순정 Git을 사용할 수 있는 것이기 때문에 이러한 변경이 불가능해질 것입니다.

초기적 리스크와 운영 상 지속적인 유지 보수의 위험이 너무 높아서 현재 이 방법을 정당화하기에는 충분하지 않습니다. 우리는 앞으로 이 방법을 재고할 수 있습니다.