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
ongoing @samihiltunen devops enablement 2023-05-30

Gitaly에서의 트랜잭션 관리

요약

Gitaly는 Git 리포지터리를 저장하는 데이터베이스 시스템입니다. 이 청사진은 다음을 도입하여 ACID 속성을 보장하는 Gitaly의 트랜잭션 관리를 다룹니다.

목표는 동시 액세스와 중단된 쓰기와 관련된 신뢰성을 향상시키는 것입니다. 트랜잭션 관리는 동시성 및 실패 관련 이상과의 처리를 다루기 때문에 Gitaly에 기여하기가 쉬워집니다.

이는 Gitaly Cluster를 위한 탈중앙화된 Raft 기반 아키텍처를 구현하는 첫 번째 단계입니다.

동기

Gitaly의 트랜잭션 관리가 부족합니다. Gitaly는 데이터베이스와 유사한 소프트웨어로부터 기대되는 일반적인 보장을 제공하지 않습니다. 데이터베이스는 일반적으로 다음과 같은 ACID 속성을 보장합니다.

  • 원자성: 트랜잭션에서의 모든 변경 사항이 완전히 일어나거나 전혀 일어나지 않습니다.
  • 일관성: 모든 변경 사항은 데이터를 일관된 상태로 유지합니다.
  • 격리성: 동시 트랜잭션이 시스템에서 유일한 트랜잭션인 것처럼 실행됩니다.
  • 지속성: 트랜잭션의 변경 사항은 시스템 충돌 후에도 지속되고 존속합니다.

Gitaly는 리포지터리에 트랜잭션적으로 접근하지 않고 이러한 속성들을 무수히 방식으로 위반합니다. 예를 들어:

  • 원자성:
    • 참조는 개별적으로 Git으로 업데이트됩니다. 작업이 중단되면 일부 참조는 업데이트되고 일부는 그렇지 않을 수 있습니다.
    • 객체는 리포지터리에 기록되지만 참조되지 않을 수 있습니다.
    • 커스텀 후크는 이전 디렉터리를 한쪽으로 옮기고 새로운 디렉터리를 자리에 놓음으로써 업데이트됩니다. 이 작업이 중간에 실패하면 리포지터리의 기존 후크는 제거되지만 새로운 것들은 기록되지 않을 수 있습니다.
  • 일관성:
    • Gitaly는 감시 디렉터리에서 객체를 주 리포지터리로 이동합니다. 이 과정에서 객체 간의 의존성을 고려하지 않습니다. 이 작업이 중간에 중단되고 의존성을 갖지 않은 객체 후에 참조되면 리포지터리가 손상될 수 있습니다.
    • 시스템 충돌은 디스크에 유효하지 않은 잠금을 남길 수 있어 추가적인 쓰기를 방지합니다.
  • 격리성:
    • 어떠한 작업이 리포지터리가 동시에 삭제되기 때문에 실패할 수 있습니다.
    • 참조 및 객체 데이터베이스 내용은 다른 작업이 그것들을 읽는 동안 수정될 수 있습니다.
    • 동시 쓰기 작업이 데이터를 수정함에 따라 백업이 일관되지 않게 될 수 있습니다. 백업은 심지어 서버에는 결쳇지 않은 상태를 포함할 수 있습니다. 이는 사용자 정의 후크가 백업되는 동안 업데이트되는 경우 발생할 수 있습니다.
    • 동시에 사용자 정의 후크를 수정하고 실행하면 사용자 정의 후크가 실행되지 않을 수 있습니다. 이는 이전 후크가 제거되고 새로운 것이 자리에 올 때 실행이 발생하는 경우에 발생할 수 있습니다.
  • 지속성: Gitaly에서 여러 fsync가 누락되었다는 것이 최근에 발견되었습니다.

ACID 속성을 준수하지 않으면 다음과 같은 결과를 낳을 수 있습니다.

  • 일관되지 않은 읽기.
  • 서버에 존재하지 않은 상태를 포함하는 일관되지 않은 백업.
  • 리포지터리 손상.
  • 충돌 후 신뢰되지 않는 작성.
  • 유효하지 않은 잠금으로 인한 가용성 부족.

격리 부족으로 인해 어떤 기능들은 현실적으로 불가능합니다. 주로 데이터 확인을 위한 온라인 체크섬이나 온라인 백업과 같은 장시간 실행되는 읽기 작업이 이에 해당합니다. 동시에 수정 중인 데이터로 인해 이러한 작업은 잘못된 결과가 나타날 수 있습니다.

이 디렉터리은 철저하지 않습니다. 그동안의 다양한 상황들로 인해 철저한 디렉터리을 만드는 것은 유익하지 않습니다. 그러나 이러한 문제들을 체계적으로 해결할 필요성은 명확합니다.

해결책

해결책은 ACID 속성을 보장하는 Gitaly의 트랜잭션 관리를 구현하는 것입니다. 이를 통해 트랜잭션 관련 로직을 단일 컴포넌트로 중앙화합니다.

사용자 데이터에 액세스하는 모든 작업은 트랜잭션에서 실행되며 트랜잭션 관리자는 트랜잭션이 커밋되는 동안 변경의 지속성과 원자성을 보장함으로써 개발이 용이해집니다.

목표

제안

아래의 설계는 우리가 달성하고자 하는 최종 단계입니다. Gitaly의 진행 중인 구현은 몇 가지 측면에서 다르며 작업이 진행됨에 따라 최종 결과에 점차 가까워질 것입니다.

파티셔닝

Gitaly의 사용자 데이터는 리포지터리에 저장됩니다. 이러한 리포지터리들은 서로 독립적으로 액세스됩니다.

각 리포지터리는 단일 스토리지에 존재합니다. Gitaly는 (storage_name, relative_path)의 복합 키로 리포지터리를 식별합니다. 스토리지 이름은 고유합니다. 두 스토리지가 동일한 상대 경로의 리포지터리를 포함할 수 있습니다. Gitaly는 이 두 개의 다른 리포지터리로 간주합니다.

트랜잭션 속성을 보장하는 데 필요한 동기화는 성능에 영향을 미칩니다. 영향을 줄이기 위해 트랜잭션은 Gitaly 노드에 저장된 데이터의 부분만 처리합니다.

첫 번째 경계는 스토리지입니다. 스토리지는 서로 독립적이며 별도의 리포지터리를 보관합니다. 트랜잭션은 스토리지를 거쳐서 전파되지 않습니다.

스토리지는 파티션으로 더 나눠집니다:

  • 트랜잭션 속성은 파티션 내에서 유지됩니다. 트랜잭션은 파티션을 거쳐 전파되지 않습니다.
  • 각 파티션은 일부 데이터를 저장하고 해당 데이터에 대한 액세스에 트랜잭션 보장을 제공합니다. 데이터는 주로 리포지터리일 것입니다. 파티션은 미래에 새 클러스터 아키텍처에서 클러스터 메타데이터를 저장하기 위해 사용될 키-값 데이터도 저장할 수 있습니다.
  • 파티션은 Raft와의 복제 단위가 될 것입니다.

리포지터리:

  • 스토리지 내에서 서로 의존할 수 있습니다. 객체 풀과 그것들을 빌려오는 리포지터리의 경우입니다. 이들 작업은 풀의 변경이 빌려오는 리포지터리의 객체 데이터베이스에 영향을 미칠 수 있기 때문에 동기화되어야 합니다.
  • 객체 풀에서 빌려오지 않는 리포지터리는 서로 독립적입니다. 이들도 독립적으로 액세스됩니다.
  • 서로 의존하면 같은 파티션에 들어갑니다. 대체로 객체 풀과 그것들을 빌려오는 리포지터리를 말합니다. 대부분의 리포지터리는 자체 파티션을 갖게 될 것입니다.

논리적 데이터 계층은 다음과 같습니다:

graph subgraph "Gitaly 노드" G[프로세스] --> S1[스토리지 1] G[프로세스] --> S2[스토리지 2] S1 --> P1[파티션 1] S1 --> P2[파티션 2] S2 --> P3[파티션 3] S2 --> P4[파티션 4] P1 --> R1[객체 풀] P1 --> R2[멤버 리포지터리 1] P1 --> R3[멤버 리포지터리 2] R2 --> R1 R3 --> R1 P2 --> R4[리포지터리 3] P3 --> R5[리포지터리 4] P4 --> R6[리포지터리 5] P4 --> R7[리포지터리 6] end

트랜잭션 관리

트랜잭션 속성은 파티션 내에서 보장됩니다. 여기서 설명하는 모든 것은 단일 파티션의 범위 내에 있습니다.

각 파티션은 파티션 내의 데이터에 작용하는 트랜잭션을 관리하는 트랜잭션 관리자를 갖게 될 것입니다. 트랜잭션 관리에서 사용되는 상위 개념들은 아래에 다루었습니다.

직렬화할 수 있는 스냅숏 격리

트랜잭션 이전에 Gitaly는 동시 작업을 서로 분리하지 않았습니다. 동시에 실행 중인 쓰기로 인해 동일한 데이터를 읽는 읽기 작업은 중간 상태를 읽을 수 있었습니다. 동일한 데이터를 여러 번 읽는 것은 동시 작업이 그 데이터를 수정했을 경우에 서로 다른 결과를 낼 수 있었습니다. 다른 이상 현상들도 가능했습니다.

트랜잭션 관리자는 트랜잭션에 대해 직렬화할 수 있는 스냅숏 격리 (SSI)를 제공합니다. 각 트랜잭션이 시작될 때 읽기 스냅숏이 할당됩니다. 이 읽기 스냅숏은 리포지터리의 최신 커밋된 데이터를 포함합니다. 데이터는 커밋되는 어떠한 동시 변경도 반영할 수 없는 상태를 유지합니다.

비차단 동시성을 위해 멀티버전 동시성 제어 (MVCC)가 사용됩니다. MVCC는 항상 업데이트를 새로운 위치에 기록하고 이전 버전을 그대로 둠으로써 작동합니다. 여러 버전이 유지되므로 읽기는 업데이트로부터 분리되어 오래된 버전을 계속해서 읽을 수 있습니다. 이전 버전은 더 이상 사용 중인 트랜잭션이 없을 시 가비지 컬렉트됩니다.

스냅숏은 다음의 사용자 데이터에 대해서 작동합니다:

  • 참조
  • 객체
  • 사용자 정의 후크

Git는 스냅숏 격리를 구현하기 위한 도구를 제공하지 않습니다. 따라서 리포지터리 스냅숏은 임시 디렉터리에 리포지터리의 디렉터리 구조를 복사하고 리포지터리의 내용을 하드 링크하는 파일 시스템

#### 직렬 가능성

직렬 가능성은 강력한 정확성 보증입니다. 동시 트랜잭션의 결과가 그것들의 순차 실행과 동일함을 보증합니다. 직렬 가능성을 보장함으로써 트랜잭션 사용자들의 삶을 쉽게 만들어줍니다. 그들은 시스템의 유일한 사용자인 것처럼 변경을 수행할 수 있으며 어떤 동시 활동이 있더라도 결과가 올바르다고 믿을 수 있습니다.

트랜잭션 관리자는 낙관적인 잠금을 통해 직렬 가능성을 제공합니다.

각 읽기와 쓰기는 리포지터리의 스냅샷에서 작동합니다. Git이 획득한 잠금은 서로 다른 스냅샷 리포지터리를 대상으로 하고 있으며, 이를 통해 모든 트랜잭션이 공유 리소스를 사용하지 않고 동시에 진행되어 변경 사항을 스테이징할 수 있습니다.

트랜잭션을 커밋할 때, 트랜잭션 관리자는 업데이트되거나 읽히는 리소스가 해당 트랜잭션보다 늦게 커밋된 중복 트랜잭션에 의해 변경되었는지 확인합니다. 만약 그런 경우가 있다면, 후속 트랜잭션은 직렬화 위반으로 거부됩니다. 만약 충돌이 없다면, 트랜잭션은 로그에 추가됩니다. 트랜잭션이 로깅되면, 성공적으로 커밋됩니다. 트랜잭션이 로깅되면 로깅된 내용을 사용하여 리포지터리에 최종적으로 적용됩니다. 이러한 잠금 메커니즘은 커밋까지 모든 트랜잭션이 블록되지 않고 진행되도록 허용합니다. 이는 모든 리소스의 쓰기 충돌을 식별하기에 충분합니다.

실제 직렬 가능성을 위해서는 수행된 읽기를 추적해야 합니다. 이것은 한 트랜잭션이 동시 트랜잭션에 의해 업데이트된 다른 값을 기반으로 그것의 업데이트를 시도하는 쓰기 편향을 방지하기 위한 것입니다. Git은 명령어의 일부로 읽은 참조를 추적할 수 있는 방법을 제공하지 않습니다. 우리가 명령의 일부로 읽은 참조를 추적할 일반적인 방법이 없기 때문에, 쓰기 편향이 허용됩니다.

고정 잠금은 트랜잭션에서 명시적으로 획득될 수 있습니다. 이러한 것은 사용될 경우 쓰기 편향을 방지할 수 있도록 트랜잭션 관리자에게 힌트를 제공합니다.

#### Write-ahead 로그

트랜잭션 이전에 쓰기는 디스크의 대상 데이터를 직접 업데이트했습니다. 만약 쓰기가 수행 중에 중단된다면 문제가 발생합니다.

예를 들어, 다음과 같은 쓰기가 주어졌다면:

- `ref-a new-oid old-oid`
- `ref-b new-oid old-oid`

프로세스가 `ref-a`를 업데이트한 후에 `ref-b`를 아직 업데이트하지 않은 상태에서 충돌이 발생한다면, 이 상태는 부분적으로 적용된 트랜잭션을 포함하게 됩니다. 이는 원자성을 위반합니다.

트랜잭션 관리자는 원자성과 내구성을 제공하기 위해 쓰기-전 로그를 사용합니다. 트랜잭션의 변경 사항은 로그에 커밋되기 전에 먼저 쓰기-전 로그에 기록됩니다. 만약 충돌이 발생하면, 트랜잭션은 로그로부터 복구되어 완료됩니다.

파티션의 모든 쓰기는 쓰기-전 로그를 통해 이루어집니다. 한 번 트랜잭션이 로깅되면, 그것은 다음과 같은 곳으로부터 적용됩니다:

- Git 리포지터리. 리포지터리의 현재 상태는 로그된 트랜잭션으로부터 구성됩니다.
- 리포지터리의 모든 파티션에서 공유되는 내장된 데이터베이스. 쓰기-전 로깅 관련된 부연적인 상태는 여기에 유지됩니다.

대부분의 쓰기는 로그 항목 내에서 완전히 자체 포함됩니다. 새로운 객체를 포함하는 참조 업데이트는 포함되지 않습니다. 새로운 객체는 팩 파일에 로깅됩니다. 팩 파일에 있는 객체들은 리포지터리의 기존 객체에 의존할 수 있습니다. 이것은 두 가지 이유로 문제가 됩니다:

- 팩 파일이 적용을 기다리는 동안 이러한 의존성은 가비지 컬렉션될 수 있습니다.
- 각 트랜잭션이 새로운 객체의 연결성을 그것의 스냅샷과 비교하며 검증할 때 실제 리포지터리의 객체 데이터베이스 안의 의존성이 가비지 컬렉션될 수 있습니다.

이러한 문제들은 로그 엔트리가 가비지 수집될 때까지 팩 파일의 의존성에 대한 내부 참조를 쓰는 것으로 해결될 수 있습니다. 이러한 내부 참조는 로그 엔트리가 가지를 별지면 지울 때 비워질 수 있습니다. 더 많은 정보는 [GitLab의 Git 포크의 문제 154](https://gitlab.com/gitlab-org/git/-/issues/154)를 참조하세요.

### 통합

Gitaly에는 150개가 넘는 RPC가 포함되어 있습니다. 이들 모두를 수정하지 않고도 트랜잭션 관리를 연결할 수 있습니다. 이는 각 핸들러 앞에 트랜잭션을 열고 커밋하는 gRPC 인터셉터를 연결하여 달성할 수 있습니다. 이 인터셉터는 다음을 수행합니다:

1. 트랜잭션을 시작합니다.
1. 요청 안의 리포지터리를 트랜잭션의 스냅샷 리포지터리를 가리키도록 다시 작성합니다.
1. 수정된 리포지터리를 사용하여 RPC 핸들러를 호출합니다.
1. 핸들러가 성공적으로 반환되면 트랜잭션을 커밋하거나 롤백합니다.

핸들러 내의 기존 코드는 이미 요청에서 리포지터리에 대한 액세스 방법을 알고 있습니다. 우리가 리포지터리를 스냅샷을 가리키게 다시 작성함으로써, 그들은 자동으로 스냅샷 격리를 받게됩니다. 왜냐하면 그들의 작업은 스냅샷을 대상으로하기 때문입니다.

`SetCustomHooks`와 같이 Git이외의 쓰기를 수행하는 RPC는 참조 트랜잭션 후크처럼 그들의 쓰기에 대한 방법을 갖고 있지 않기 때문에 적응해야합니다. 그러나 이들은 작은 소수에 불과하며, 명시적으로는 다음과 같습니다:

- 사용자 정의 후크 업데이트.
- 리포지터리 생성.
- 리포지터리 삭제.

이러한 것들을 통합하는 것을 지원하기 위해, 우리는 트랜잭션 안에 데이터를 포함하는 도우미 함수를 제공할 것입니다. 우리는 트랜잭션을 요청 컨텍스트를 통해 파이프라인화할 것입니다.

트랜잭션 관리를 통합하는 가장 큰 걱정은 트랜잭션 논리에 존재하지 않으면서 리포지터리에 쓰기를 하는 위치를 놓치는 것입니다. 우리가 요청의 리포지터리를 스냅샷 리포지터리를 가리키게하고 있기 때문에, 이는 문제가 아닙니다. RPC 핸들러들은 리포지터리의 실제 위치를 알지 못하기 때문에 실수로 거기에 쓰기를 할 수 없습니다. 그들이 수행하는 스냅샷 리포지터리로의 모든 쓰기는 트랜잭션에 포함되지 않는다면 폐기될 것입니다. 이것은 테스트를 실패하게 만들고 우리에게 문제를 알릴 것입니다.

Gitaly에 존재할 수 있는 어떤 위치들은 실제 리포지터리의 상대적 경로를 가지는 것이 유익할 수 있습니다. 예를 들어, 상대적 경로를 캐시하는 캐시(예: 팩 오브젝트 캐시)가 그러한 위치 중 하나일 수 있습니다. 각 트랜잭션이 자신만의 스냅샷 리포지터리와 따라서 다른 상대적 경로를 가짐으로써 문제를 일으킬 것입니다. 필요하다면, 실제 상대적 경로는 요청 컨텍스트를 통해 파이프라인화될 수 있을 것입니다. 스냅샷은 상대적 경로를 안정적으로 유지하면서 여러 읽기 전용 트랜잭션 간에 공유될 수 있습니다. 이것은 최소한 일부 케이스에서 캐시가 데이터가 변경될 때마다 만료되어야 하는 경우에도 작동할 것입니다.

프리펙트와의 하위 호환성 유지를 위해 트랜잭션 관리자는 트랜잭션을 커밋할 때 프리펙트에게 투표를 할 것입니다. 참조 트랜잭션 후크는 그곳에 변경 사항이 실제로 커밋되지는 않지만 트랜잭션에만 포함된다는 사실 때문에 그것을 할 필요가 없을 것입니다.

하우스키퍼링은 트랜잭션 처리와 통합되어야 합니다. 임시 파일을 제거하거나 낡은 잠금들을 제거하는 과정과 관련된 대부분의 정리 관련 하우스키퍼링 작업은 이제 더 이상 필요하지 않습니다. Git이 오류로 남긴 모든 쓰레기는 스냅샷 안에 포함되어 있으며 트랜잭션이 완료될 때 제거됩니다.

이것은 참조와 객체 리팩킹, 객체 가비지 수집, 다양한 인덱스를 구축하는 등의 일부 작업을 남깁니다. 이러한 작업은 모두 트랜잭션 안에서 수행될 수 있습니다. 새로운 팩들은 예를 들어 스냅샷을 통해 계산될 수 있습니다. 커밋할 때, 트랜잭션 관리자는 그들의 변경 사항이 동시에 커밋된 다른 트랜잭션과 충돌하는지 확인할 수 있습니다. 예를 들어, 스냅샷에서 가비지 수집된 객체가 다른 트랜잭션에서 동시에 참조될 수 있습니다. 만약 충돌이 있다면, 트랜잭션 관리자는 충돌을 해결하거나 충돌이 있으면 트랜잭션을 중지하고 하우스키퍼링 업무를 재시도할 것입니다.

트랜잭션 관리자는 리포지터리에 있는 팩 파일과 루즈 참조의 수를 추적하고 필요할 때 리팩킹을 트리거할 것입니다.

위의 내용은 Gitaly의 기존 코드와 거의 투명하게 통합될 수 있습니다. 우리는 데이터를 설정하면 작은 부분에만 조건 로직을 전염시켜서 마이그레이션 기간을 코드 기반 전체에 최소한의 조건 로직으로 관리할 수있습니다.

### 성능 고려 사항

가장 눈에 띄는 걱정은 리포지터리의 스냅샷에 대한 비용입니다. 우리는 요청이 처리되기 전에 리포지터리의 디렉터리 구조를 복사하고 파일을 하드 링크하는 것으로 생각되겠지만, 이것은 처음에 들려야 할 정도로 심각한 문제가 아닐 수 있습니다. 왜냐하면:

- 스냅샷은 본질적으로 디렉터리 항목만 생성합니다. 이것들은 빠른 시스콜입니다. 리포지터리 내 파일 수가 늘어날수록 스냅샷에 생성해야 할 디렉터리 항목과 링크의 수가 늘어납니다. 이것은 객체와 참조를 리팩킹하여 리포지터리를 좋은 상태로 유지함으로써 완화될 수 있습니다. Reftables는 미래에 루즈 참조의 수를 줄이는 데 도움이 될 것입니다. 쓰기-전 로그는 루즈 객체가 미래에 문제가 없을 팩 파일로만 리포지터리로 기록합니다.
- 이들은 메모리 내 작업입니다. 페이지 캐시를 타겟팅하며 fsync할 필요가 없습니다.
- 스냅샷은 읽기 전용 트랜잭션 간에 공유될 수 있습니다. 그들은 스냅샷에서 어떤 변경도 하지 않으므로, 쓰기는 상대적으로 드물게 발생합니다.
- 격리 수준은 성능을 위해 트랜잭션 단위로 설정할 수 있습니다. 단일 블롭을 가지고오는 RPC가 스냅샷 격리가 필요하지 않습니다.

쓰기를 직렬화하는 것은

## 트랜잭션의 생명주기

다음 다이어그램은 일부 참조를 업데이트하는 쓰기 트랜잭션의 흐름을 모델링합니다. 다이어그램은 트랜잭션이 처리되는 방식의 주요 지점을 보여줍니다.

- 각 트랜잭션은 리포지터리의 스냅샷을 가지고 있습니다.
- RPC 핸들러들은 리포지터리 자체에서 작동하지 않습니다.
- 스냅샷에서 수행된 변경 사항은 트랜잭션에서 캡처됩니다.
- 변경 사항은 RPC가 성공적으로 반환된 후에 커밋됩니다.
- 트랜잭션은 로그에서 리포지터리에 비동기적으로 적용됩니다.

트랜잭션의 시작과 커밋은 다른 트랜잭션을 차단할 수 있습니다. 오픈된 트랜잭션은 차단하지 않고 동시에 진행됩니다.

1. 스냅샷을 생성하는 중에 리포지터리에 공유 잠금이 획득됩니다. 동시에 여러 스냅샷을 촬영할 수 있지만 변경 사항은 리포지터리에 기록될 수 없습니다.
1. 트랜잭션은 커밋 호출까지 차단하지 않고 동시에 실행되며, 시리얼라이즈 가능성 검사가 이루어집니다.
1. 로그 적용은 리포지터리에 전용 잠금을 획득하며, 스냅샷팅을 차단합니다.

```mermaid
sequenceDiagram
    autonumber
    gRPC Server->>+트랜잭션 미들웨어: 요청
    트랜잭션 미들웨어->>+트랜잭션 매니저: 시작
    트랜잭션 매니저->>+트랜잭션: 오픈된 트랜잭션
    참가자 리포지터리
    중요 공유 잠금 on 리포지터리
        트랜잭션->>+스냅샷: 스냅샷 생성
    end
    트랜잭션->>트랜잭션 매니저: 트랜잭션 시작됨
    트랜잭션 매니저->>트랜잭션 미들웨어: 시작됨
    트랜잭션 미들웨어->>+RPC 핸들러: 재작성된 요청
    RPC 핸들러->>+git update-ref: 참조 업데이트
    git update-ref->>스냅샷: 준비
    스냅샷->>git update-ref: 준비됨
    git update-ref->>스냅샷: 커밋
    스냅샷->>git update-ref: 커밋됨
    git update-ref->>+참조 트랜잭션 후크: 호출
    참조 트랜잭션 후크->>트랜잭션: 업데이트 캡처
    트랜잭션->>참조 트랜잭션 후크: OK
    참조 트랜잭션 후크->>-git update-ref: OK
    git update-ref->>-RPC 핸들러: 참조 업데이트됨
    RPC 핸들러->>-트랜잭션 미들웨어: 성공
    트랜잭션 미들웨어->>트랜잭션: 커밋
    트랜잭션->>트랜잭션 매니저: 커밋
    중요 시리얼라이즈 가능성 검사
        트랜잭션 매니저->>트랜잭션 매니저: 트랜잭션 확인
    end
    트랜잭션 매니저->>리포지터리: 로그 트랜잭션
    리포지터리->>트랜잭션 매니저: 트랜잭션 로그됨
    트랜잭션 매니저->>트랜잭션: 커밋됨
    트랜잭션->>스냅샷: 스냅샷 제거
    스냅샷 비활성화
    트랜잭션->>-트랜잭션 미들웨어: 커밋됨
    트랜잭션 미들웨어->>-gRPC Server: 성공
    중요 리포지터리에 단독 잠금
        트랜잭션 매니저->>-리포지터리: 트랜잭션 적용
    end

향후 기회

거래를 클라이언트에 노출

내부적으로 트랜잭션을 처리하는 경우, 다음 단계는 이를 클라이언트에 노출하는 것입니다. 예를 들어, Rails는 단일 트랜잭션에서 여러 작업을 실행할 수 있습니다. 이로써 클라이언트에게 ACID 보증이 확장되어 여러 문제가 해결될 수 있습니다:

  • 클라이언트는 트랜잭션을 원자적으로 커밋할 수 있습니다. 수행한 모든 변경 사항이 적용되거나 아무 것도 적용되지 않습니다.
  • 시리얼라이즈 가능성 보증을 통해 작업은 경쟁 조건에 대해 자동적으로 보호됩니다.

Gitaly 유지 관리자들에게 클라이언트에게 거래를 확장하면 API 표면을 줄일 수 있는 기회를 제공합니다. Gitaly에는 동일한 작업을 수행하는 여러 RPC가 있습니다. 예를 들어 참조 업데이트는 여러 RPC에서 이루어집니다. 이로 인해 복잡성이 증가합니다. 클라이언트가 트랜잭션을 시작하고 변경 사항을 스테이징하며 커밋할 수 있다면, 보다 적은 수의 더 세분화된 RPC를 얻을 수 있습니다. 예를 들어, UserCommitFiles은 다음과 같이 더 세분화된 명령어로 모델링될 수 있습니다:

  • 시작
  • WriteBlob
  • WriteTree
  • WriteCommit
  • UpdateReference
  • 커밋

클라이언트는 단일 목적의 RPC를 사용하여 더 복잡한 작업을 구성할 수 있기 때문에 API가 구성 가능합니다. 이로 인해 각 작업이 다중 RPC 호출을 요구하여 왕복 지연이 증가하는 우려가 있습니다. 이는 명령 일괄 처리를 허용하는 API를 제공함으로써 완화할 수 있습니다.

다른 데이터베이스는 명시적인 트랜잭션과 쿼리 언어를 통해 이러한 기능을 제공합니다.

WAL 아카이빙을 사용한 지속적인 백업

증분 백업은 현재 이전 백업과 리포지터리의 현재 상태 사이의 변경 사항을 항상 계산해야 하기 때문에 성가신 문제가 됩니다. 파티션의 모든 쓰기는 전진 로그를 통해 이루어지기 때문에, 리포지터리를 점진적으로 백업하기 위해 전진 로그 항목을 스트리밍할 수 있습니다. 더 많은 정보는 리포지터리 백업를 참조하세요.

Raft 복제

트랜잭션은 단일 파티션에서 시리얼라이징을 제공합니다. 파티션의 전진 로그는 Raft와 같은 합의 알고리즘을 사용하여 복제될 수 있습니다. Raft는 로그 항목 커밋에 대한 선형 일관성을 보장하며, 트랜잭션 매니저는 이들을 로깅하기 전에 트랜잭션의 시리얼라이징을 보증하기 때문에 복제본 간에 모든 작업은 시리얼라이징을 보증합니다. 더 많은 정보는 epic 8903를 참조하세요.

대안적인 해결책

트랜잭션 관리에 대한 대안이 제시되지 않았습니다. 병렬성 및 쓰기 중단과 관련된 현재 상태의 버그 하나하나 해결은 확장 가능하지 않습니다.

리프테이블과 스냅샷 분리 격리

리프테이블에 대한 스냅샷 분리의 초기 설계는 Git의 새로운 참조 백엔드인 리프테이블에 의존했습니다. 리프테이블은 몇 년간 진행 중인 작업이지만 실제로 Git에 도입될 타임라인은 명확하지 않습니다. 여기 제안된 해결책과 비교하여 몇 가지 단점이 있습니다:

  • 리프테이블은 스냅샷에서 참조만 다룹니다. 여기서 제안된 스냅샷 설계는 리포지터리의 전체를 다루며 가장 중요한 객체 데이터베이스 콘텐츠를 다룹니다.
  • 리프테이블은 각 Git 호출이 올바른 버전의 리프테이블을 읽도록 연결돼야 하므로 많은 통합이 필요합니다. 여기서의 파일 시스템 기반 스냅샷 설계는 기존의 Git 호출에 변경을 요구하지 않습니다.
  • 여기 설계는 리포지터리의 완전한 스냅샷을 제공하여 단일 트랜잭션에서 다중 RPC를 실행할 수 있게 합니다. 왜냐하면 트랜잭션의 상태가 트랜잭션 중에 디스크에 저장되기 때문에 각 RPC는 트랜잭션의 이전 쓰기를 읽을 수 있지만 다른 트랜잭션으로부터 격리됩니다. 특히 객체 격리를 다루는 경우에 필요합니다. 이는 리프테이블과 어떻게 구현될지에 대해 명확하지 않습니다.
  • 스냅샷은 서로 독립적입니다. 이는 각 트랜잭션이 다른 트랜잭션에 의해 차단되지 않고 변경 사항을 스테이징할 수 있도록 하여 동기화를 줄입니다. 이는 성능 향상을 위해 낙관적 락을 가능케 합니다.

리프테이블은 더 효율적인 참조 백엔드로서 여전히 유용하지만, 스냅샷 분리 격리에 필요하지는 않습니다.