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. As with all projects, the items mentioned on this page are subject to change or delay. The development, release, and timing of any products, features, or functionality remain at the sole discretion of GitLab Inc.
Status Authors Coach DRIs Owning Stage Created
proposed @qmnguyen0711 devops enablement 2023-06-15

Gitaly - 순수 HTTP/2 서버에서 업로드 팩 트래픽 처리

요약

HTTP/SSH를 사용하는 모든 Git 데이터 전송 작업은 Gitaly에서 upload-pack RPC로 처리됩니다.

이러한 RPC는 Sidechannel이라는 독특한 응용 프로그램 계층 프로토콜을 사용하여 클라이언트가 Gitaly gRPC 서버를 다이얼하는 동안 핸드셰이킹 프로세스를 수행합니다. 이 프로토콜은 gRPC 연결을 일반적으로 처리하면서 동시에 클라이언트로 대량의 데이터를 전송할 수 있는 out-of-band 연결을 허용합니다.

순수 gRPC 스트리밍 호출 대신 이러한 프로토콜을 사용하면 성능이 크게 향상되지만, 이 프로토콜은 비전통적이고 혼란스럽고 복잡하며 Gitaly의 다음 아키텍처로 통합하거나 통합시키기 어려운 문제가 있습니다. 이에 따라 이 설계안에서는 모든 upload-pack 트래픽을 처리하는 새로운 “지루한(boring)” 기술 솔루션을 제안합니다.

동기

이 부분에서는 Git 데이터 전송 작업 방법을 탐구하여 Sidechannel 최적화의 역할에 특별히 주목하고 그 이점과 단점을 논의합니다. 우리의 주요 목표는 시스템을 완전히 다시 작성할 필요가 없고 다른 시스템의 다른 부분 및 RPC에 영향을 미치지 않는 솔루션을 찾는 것입니다.

Git 데이터 전송 작업 방법

Git 데이터 전송은 Git 서버가 제공할 수 있는 중요한 서비스 중 하나입니다. Linux 커널 개발을 위해 초기에 개발된 Git의 기본 기능입니다. Git이 인기를 얻으면서 분산 시스템으로 인식되었지만 GitHub 또는 GitLab과 같은 중앙 집중식 Git 서비스의 등장으로 사용 패턴이 변화하였습니다. 결과적으로 호스팅된 Git 서버에서의 Git 데이터 전송은 어려워졌습니다.

Git은 pack-파일을 통해 데이터를 여러 프로토콜을 통해 전송하는데, 특히 HTTP 및 SSH를 이용합니다. 자세한 내용은 pack-protocolhttp-protocol을 참조하십시오.

요약하면 일반적인 흐름은 다음과 같습니다.

  1. 참조 발견: 서버가 클라이언트에게 참조를 광고합니다.
  2. Packfile 협상: 클라이언트가 서버와의 전송에 필요한 packfile을 “haves”, “wants” 등의 참조 목록을 보내면서 협상합니다.
  3. Packfile 데이터 전송: 서버는 요청된 데이터를 구성하고 Pack 프로토콜을 사용하여 클라이언트에게 데이터를 다시 보냅니다.

이러한 단계에는 자세한 내용 및 최적화가 있을 수 있습니다. Git 서버는 참조 발견 및 Packfile 협상에 문제가 발생한 적이 없습니다. 프로세스에서 가장 요구되는 측면은 Packfile 데이터의 전송으로, 실제 데이터 교환이 발생하는 부분입니다.

GitLab에서 Gitaly은 모든 Git 작업을 관리합니다. 그러나 외부 소스에서 액세스할 수 없으며 Workhorse 및 GitLab Shell이 모든 외부 통신을 처리합니다. Gitaly은 SmartHTTPSSH와 같은 특정한 RPC를 통해 gRPC 서버를 제공합니다. 또한 GitLab Rails와 기타 서비스를 위해 다양한 다른 RPC를 제공합니다. 다음 다이어그램은 SSH를 사용한 복제를 설명합니다.

sequenceDiagram participant User as User participant UserGit as git fetch participant SSHClient as User's SSH Client participant SSHD as GitLab SSHD participant GitLabShell as gitlab-shell participant GitalyServer as Gitaly participant GitalyGit as git upload-pack User ->> UserGit: git fetch 실행 UserGit ->> SSHClient: SSH 클라이언트 생성 Note over User,SSHClient: 사용자의 로컬 머신 상 SSHClient ->> SSHD: SSH 세션 Note over SSHClient,SSHD: 인터넷 상의 세션 SSHD ->> GitLabShell: gitlab-shell 생성 GitLabShell ->> GitalyServer: gRPC SSHUploadPack GitalyServer ->> GitalyGit: git upload-pack 생성

출처: 이 문서에서 무심코 복사함

Sidechannel을 사용한 Git 데이터 전송 최적화

과거에는 gRPC를 사용하여 대량의 데이터를 전송할 때 많은 성능 문제에 직면했습니다. Epic 463에서 이 최적화 작업을 추적했습니다. 상황은 더 있지만 간단히 말하면 두 가지 주요 문제가 있습니다.

  • gRPC의 설계는 상대적으로 크기가 작은 대량의 메시지를 전달하기에 이상적입니다. 그러나 중간 크기의 리포지토리를 복제하는 경우 기가바이트 단위의 데이터를 전송할 수 있습니다. 대규모 데이터 처리에 대해 Protobuf는 여러 소스에서 확인했듯이 심각한 오버헤드를 추가하고 인코딩 및 디코딩 중에 상당한 CPU 사용량이 필요합니다. 또한 protobuf는 메시지를 부분적으로 읽거나 쓸 수 없어 서버가 동시에 여러 요청을 받을 때 메모리 사용량이 급증합니다.
  • 둘째, grpc-go 구현도 유사한 목적으로 최적화됩니다. gRPC 프로토콜은 HTTP/2위에 구축되어 있습니다. gRPC 서버가 데이터를 전송할 때 데이터를 HTTP/2 데이터 프레임으로 래핑합니다. grpc-go 구현은 비동기 제어 버퍼를 유지합니다. 새로운 메모리를 할당하고 데이터를 복사하여 제어 버퍼에 추가합니다(client, server). 따라서 사용자 정의 코덱으로 protobuf 문제를 해결하더라도 grpc-go는 여전히 해결되지 않은 문제입니다. 메모리 재사용에 대한 상담(상에서)은 아직 미해결 상태입니다. 풀 메모리 추가 시도는 일반적인 사용 패턴과 충돌했기 때문에 되돌렸습니다.

우리는 Sidechannel이라는 프로토콜을 개발했는데, 이를 통해 grpc-go 구현을 우회하여 클라이언트와 원시 이진 데이터를 통신할 수 있습니다. Sidechannel에 대한 자세한 내용은 이 문서를 참조하십시오. 간단히 말하면 Sidechannel은 다음과 같이 작동합니다.

  • gRPC 서버가 클라이언트 TCP 연결 수용 중의 핸드셰이킹 프로세스에서 Yamux를 사용하여 응용 프로그램 계층에서 TCP 연결을 멀티플렉싱합니다. 이를 통해 gRPC 서버는 Yamux 스트림이라고하는 가상 멀티플렉싱 연결에서 작동할 수 있습니다.
  • 서버에서 데이터를 전송해야 할 때 추가적인 Yamux 스트림을 설정합니다. 해당 채널을 통해 데이터가 전송됩니다.
sequenceDiagram participant Workhorse participant Client handshaker participant Server handshaker participant Gitaly Note over Workhorse,Client handshaker: Workhorse 프로세스 Note over Gitaly,Server handshaker: Gitaly 프로세스 Workhorse ->> Client handshaker: - Client handshaker ->> Server handshaker: 1. 다이얼 Note over Server handshaker: Yamux 세션 생성 Server handshaker ->> Client handshaker: - Client handshaker ->> Workhorse: Yamux 스트림 1 Note over Workhorse: Yamux 세션 유지 par Yamux 스트림 1 Workhorse ->>+ Gitaly: 2. PostUploadWithSidechannel Note over Gitaly: 요청 유효성 검사 Gitaly ->> Workhorse: 3. Sidechannel 열기 end Note over Workhorse: Yamux 스트림 2 생성 par Yamux 스트림 2 Workhorse ->> Gitaly: 4. Packfile 협상 및 기타 동작 Workhorse ->> Gitaly: 5. Half-closed 시그널 Gitaly ->> Workhorse: 6. Packfile 데이터 Note over Workhorse: Yamux 스트림 2 닫기 end par Yamux 스트림 1 계속 Gitaly ->>- Workhorse: 7. PostUploadPackWithSidechannelResponse end

Sidechannel은 현재까지 원래 문제를 해결하였습니다. Git 전송 이용률이 크게 개선되고 CPU 및 메모리 사용량이 크게 줄어든 것을 확인했습니다. 물론 이는 어떤 타협을 동반합니다.

  • 이것은 아주 똑똑한 트릭입니다. grpc-go는 인증 목적으로만 사용해야 하는 핸드셰이크 훅을 제공합니다. 연결 수정에 사용해서는 안 됩니다.
  • Sidechannel은 연결 수준에서 작동하므로 해당 연결을 사용하는 모든 RPC는 멀티플렉싱을 설정해야 하며 목표가되는 업로드 팩이 아니어도 그렇습니다.
  • Yamux는 응용 프로그램 수준 멀티플렉서입니다. 바이너리 프레임 및 흐름 제어 등 HTTP/2에서 큰 영향을 받았습니다. 우리는 그것을 HTTP/2의 축소판으로 칭할 수 있습니다. Yamux 위에서 gRPC를 실행할 때 두 가지 프레임 프로토콜이 중첩됩니다.
  • Sidechannel의 상세한 구현은 복잡합니다. 지난 말한 핸드셰이킹 외에도 서버가 다이얼 백을 할 때 클라이언트는 핸들러에 그것을 전달하기 전에 보이지 않는 gRPC 서버를 시작해야 합니다. 또한 pktline에서 영감받은 대칭 프레임 프로토콜을 구현합니다. 이 프로토콜은 업로드 팩 RPC용이며 Yamux의 “half-closed” 능력 부족을 극복합니다.

이러한 모든 복잡성은 Gitaly의 유지 보수 및 미래 발전에 부담을 주게 됩니다. 미래 Gitaly Raft 기반 아키텍처를 전망할 때 Sidechannel은 문제가 됩니다. 라우팅 전략 및 구현에 대한 이해는 Sidechannel과의 호환성을 고려해야 합니다. Sidechannel은 응용 프로그램 계층 프로토콜이므로 대부분의 클라이언트 측 라우팅 라이브러리는 그것과 잘 작동하지 않습니다. 또한 Sidechannel에 의한 성능은 유지되어야 합니다. 결국 Sidechannel을 적합한 위치에 맞추는 방법을 찾을 수 있지만 선택지는 상당히 제한되어 다른 똑똑한 해킹으로 이어질 가능성이 높습니다.

Sidechannel을 성능 특성을 유지한채로 간단하고 널리 사용되는 기술로 대체하는 것이 유용할 것으로 판단됩니다. 모든 문제를 해결하는 잠재적인 솔루션 중 하나는 모든 upload-pack RPC를 순수 HTTP/2 서버로 보내는 것입니다.

목표

  • Sidechannel에 대한 업로드 팩 RPC의 대체물을 찾아야 합니다. 쉽고 복잡하지 않으며 널리 채택되는 대안이어야 합니다.
  • 구현은 인기있는 라이브러리, 프레임워크 또는 Go의 표준 라이브러리의 지원되는 API 및 사용 사례를 사용해야 합니다. 더 이상 전송 레이어에서 해킹하지 않아도 됩니다.
  • 새로운 솔루션은 Praefect와 호환되어야 하며, 미래의 라우팅 메커니즘, 로드 밸런서 및 프록시에 사용하기 쉬워야 합니다.
  • Sidechannel과 동일한 성능 및 낮은 리소스 사용량을 가져야 합니다.
  • 점진적인 롤아웃을 허용하고 클라이언트와 완전한 하위 호환성을 가져야 합니다.

비목표

  • gRPC에 투자한 모든 것을 다시 구현하지 않습니다. 즉, 인증, 가시성, 동시성 제한, 메타데이터 전파 등은 유지하거나 복제하고 싶지 않습니다. 두 시스템 간에 기능을 유지 관리하고 싶지 않습니다.
  • 다른 RPC를 수정하거나 클라이언트에서 그들을 사용하는 방식을 변경하지 않습니다.
  • 모든 RPC를 새로운 HTTP/2 서버로 마이그레이션하지 않습니다.

제안

이 설계안의 큰 부분은 역사적인 맥락과 우리가 왜 진행해야 하는지에 대해 설명합니다. 제안된 솔루션은 간단합니다. Gitaly은 모든 업로드 팩 RPC를 처리하기 위해 순수한 HTTP2 서버를 노출합니다. 다른 RPC는 그대로 유지되며 기존의 gRPC 서버에서 처리됩니다.

flowchart LR Workhorse-- 다른 RPC --> Port3000{{":3000"}} Port3000 --o gRPCServer("gRPC 서버") Workhorse-- PostUploadPack --> Port3001{{":3001"}} Port3001 --o HTTP2Server("HTTP/2 서버") GitLabShell("GitLab 셸")-- SSHUploadPack --> Port3001 GitLabRails("GitLab 레일즈") -- 다른 RPC --> Port3000 subgraph Gitaly Port3000 gRPCServer Port3001 HTTP2Server end

앞서 언급했듯이 Sidechannel은 Yamux 다중화 프로토콜을 활용하는데, 이는 HTTP/2의 간단화된 버전으로 볼 수 있습니다. HTTP/2를 사용하면 핵심 기능인 다중화, 이진 프레임 프로토콜 및 플로우 컨트롤 등이 변경되지 않습니다. 따라서 Workhorse와 같은 클라이언트는 사용자 정의 인코딩 및 디코딩 레이어와 같은 것을 필요로 하지 않고 동일한 TCP 연결을 통해 대량의 이진 데이터를 효율적으로 교환할 수 있습니다. 실제로 여기에는 HTTP/2를 위한 실행 의도가 있었으며 원칙적으로 Sidechannel만큼의 성능 수준을 제공할 수 있습니다. 더불어, 이 교체로 인해 gRPC 위의 Yamux를 통한 TCP의 오버헤드가 제거됩니다.

또한 Gitaly은 공식적으로 지원되는 API를 통해 고급 HTTP/2 기능에 액세스할 수 있습니다. HTTP/2는 Go의 표준 라이브러리에서 공식적으로 지원되는 것이며, 외장 로드 밸런서와 프록시와 시스템이 호환되며 다양한 라이브러리에서도 지원됩니다.

마지막으로 UploadPack RPC 및 기타 일반 RPC는 다음 섹션에 설명된 기술을 사용하여 동일한 포트에서 공존할 수 있습니다. 그러나 그들 중 한 번에 모두를 마이그레이션하는 것은 성능과 기능 측면에서 위험합니다. 예상치 못한 결과가 발생할 수 있습니다. 따라서 GitLab.com에서 점진적으로 마이그레이션하는 것이 더 현명할 것입니다. 기타 RPC는 신중한 고려 후에 마이그레이션할 수 있습니다. Self-managed 인스턴스는 기존의 단일 포트를 수정하지 않고 사용할 수 있으므로 이 변경은 사용자에게 투명합니다.

다음 섹션에서는 구체적인 구현 세부 정보와 해당 접근 방식의 장단점에 대해 설명합니다.

설계 및 구현 세부 정보

설계

요약하자면, Gitaly가 HTTP2 서버를 노출하도록 하는 것이 제안입니다. 처음에는 새로운 핸들러와 일련의 인터셉터를 구현해야 할 것으로 보입니다. 다행히도 gRPC 서버는 ServeHTTP를 제공하여 HTTP/2를 gRPC 방식으로 처리할 수 있습니다. 이것은 http.Handler 인터페이스를 구현하여 HTTP/2 서버에 플러그인할 수 있습니다. gRPC 프로토콜이 HTTP/2를 기반으로 구축되기 때문에 HTTP/2 서버는 요청을 해당 핸들러로 라우팅합니다. 리디렉션을 위해 헤더를 사용할 수 있습니다:

if r.ProtoMajor == 2 && strings.HasPrefix(
    r.Header.Get("Content-Type"), "application/grpc") {
    grpcServer.ServeHTTP(w, r)
} else {
    yourMux.ServeHTTP(w, r)
}

이 방법에는 다음과 같은 이점이 있습니다:

  • ServeHTTP는 Go의 HTTP/2 서버 구현을 사용하며, 이는 grpc-go의 HTTP/2 서버와 완전히 분리되어 있습니다. gRPC 및 HTTP/2 구현의 구현을 파고들어보면 두 서버의 내장 구현이 이전 섹션에서 언급된 메모리 할당 문제를 해결해야 한다고 생각됩니다.
  • ServeHTTP는 Go 표준 라이브러리의 http.Handler 인터페이스를 구현합니다. 따라서 gRPC 핸들러로 작성할 수 있으며 여기서는 원시 바이너리 데이터를 전송하고 오류 세부 정보를 포함한 상태 코드를 반환할 수 있습니다. 클라이언트는 무엇이 잘못되었을 때 데이터 전송 프로세스에서 문제가 발생하면 Sidechannel에서 파이프 오류 대신 정확한 이유를 받을 수 있습니다.
  • 대부분, 혹은 모든 인터셉터를 수정하지 않고 재사용할 수 있습니다. channelz 및 클라이언트 측 로드 밸런싱과 같은 기타 내장 도구도 제대로 작동합니다. 로깅 및 메트릭과 같은 관측 도구도 잘 작동합니다.
  • 업로드 팩 요청은 한 연결에서 순수한 스트리밍 호출이 됩니다. 더 이상 다른 비동기 전송을 열 필요가 없습니다.
  • 클라이언트(Workhorse 및 GitLab Shell)는 계속해서 gRPC 클라이언트를 사용합니다. 이 접근 방식을 사용한 요청은 정상적인 gRPC 호출로 간주됩니다. 따라서 Praefect에서도 최소한의 수정으로 잘 작동해야 합니다.

물론 ServeHTTP를 사용하면 요청 및 응답이 Protobuf 구조체로 이루어져야 한다는 비용이 따릅니다. 사용자 정의 코덱을 사용하면 이러한 성능 손실을 극복할 수 있습니다. 솔루션은 이러한 성능 부담을 극복하면서도 핸들러를 HTTP 핸들러로 구현하는 것이 이상적이며, 이를 통해 연결의 원시 액세스가 가능해집니다. 그러나 이 솔루션은 gRPC 특정 구성 요소를 모두 다시 구현하는 것을 의미합니다. 결과적으로 제안된 솔루션은 이러한 아키텍처의 진화를 용이하게 하기 위한 합리적인 교환을 제공합니다.

sequenceDiagram participant Workhorse participant HTTP2 Server participant UploadPack Handler Note over HTTP2 Server, UploadPack Handler: Gitaly 프로세스 Workhorse ->> HTTP2 Server: 1. Dial HTTP2 Server ->> Workhorse: 연결 par PostUploadPackV3 Workhorse ->> UploadPack Handler: 2. PostUploadPackV3Request Note over UploadPack Handler: 요청 유효성 검사 UploadPack Handler ->> Workhorse: PostUploadPackV3Response (빈 값) Workhorse ->> UploadPack Handler: 3. Raw []byte 전송 Workhorse ->> Workhorse: 전송 닫기 UploadPack Handler ->> Workhorse: 4. Raw []byte 전송 UploadPack Handler ->> UploadPack Handler: 연결 닫기 end

제안된 솔루션은 다음과 같습니다:

  • 입력 이진 데이터를 통과시키는 “원시 코덱”를 구현합니다.
  • “PostUploadPackV3” gRPC 호출을 새로 생성하고 다른 gRPC 핸들러와 비슷한 서버 핸들러를 구현합니다.
  • gRPC의 ServeHTTP를 호출하는 HTTP/2 서버를 구현합니다. 이 서버는 또 다른 gRPC 서버로 취급됩니다. 사실, Gitaly에서는 이제 다른 전송 채널을 통해 내부 및 외부적으로 최대 4개의 gRPC 서버를 시작합니다. 이 HTTP2 서버는 그 앞력과 Gitaly의 기존 프로세스 관리 및 우아한 종료 및 업그레이드를 사용할 수 있습니다.

이 솔루션을 시연하기 위해 Gitaly 및 Workhorse에 두 개의 POC 합병 요청을 구현했습니다.

POC MR은 비-Praefect 설정에서 테스트를 통과했습니다. 로컬 환경에서 조기 벤치마크는 이 접근 방식으로:

  • Sidechannel 접근방식보다 약간 빠릅니다.
  • 최대 CPU 소비를 10-20% 줄입니다.
  • 동일한 메모리 사용을 유지합니다.

POC는 완료되지 않았지만 “지루한” 기술을 사용하여 간소화하는 것을 보여주며, 성능 향상은 완전히 예상치 못한 것입니다.

기존의 Sidechannel 솔루션은 새로운 솔루션과 공존할 수 있습니다. 이것은 점진적 적용이 가능하게 만듭니다.

외부 트래픽 외에도 Gitaly 서버는 내부 요청을 처리합니다. 이들은 Gitaly에 의해 생성된 Git 프로세스로부터 나옵니다. 하나의 전형적인 예는 Packfile을 생성하기 위해 git-upload-pack(1)에서 트리거된 PackObjectsHookWithSidechannel RPC입니다. 제안된 솔루션은 이러한 내부 RPC에도 이점을 제공합니다.

고려 사항

제안된 솔루션의 주요 문제점은 별도의 포트를 통해 HTTP/2를 열어야 한다는 것입니다. ServeHTTP를 통해 gRPC 핸들러를 기존의 HTTP/2 서버에 통합할 수 있지만, 그 반대로는 지원하지 않습니다. 새로운 포트 노출은 제한적인 환경에서 방화벽, NAT 등 추가 설정이 필요합니다. 일반적인 설정에서는 인프라 구성 변경이 필요하지 않습니다. 위에서 언급한 대로, 이른바 이중 포트 상황은 이전에 이주할 때만 발생합니다. 완료되면 하나로 통합하고 Self-Managed 인스턴스에 배포할 수 있습니다. 사용자는 아무것도 변경할 필요가 없습니다.

이전에 언급한 대로 새롭게 도입된 HTTP/2 서버는 Gitaly 프로세스에 의해 관리됩니다. 기존의 gRPC 서버와 일관되게 시작, 다시 시작, 종료, 업그레이드 작업을 수행합니다. 이는 로드 밸런싱, 서비스 검색, 도메인 관리 및 기타 구성이 현재의 gRPC 서버에 대해 설정된 경우에만 원활하게 작동함을 의미합니다. 새로운 서버는 현재의 gRPC 서버의 대부분의 구성을 활용할 수 있으며, TLS 및 인증을 포함합니다. HTTP 서버가 바인딩될 주소는 유일한 필요한 새 구성입니다. 따라서 새로운 포트를 노출하는 것은 장애가 되지 않아야 합니다.

또 다른 고려 사항은 Workhorse와 GitLab Shell이 별도의 연결 풀을 유지해야 한다는 것입니다. 현재는 각 Gitaly 노드에 대해 하나의 연결을 유지합니다. 이 수는 두 개의 연결로 두 배가 될 것입니다. 이는 큰 문제가 되지 않아야 합니다. 결국, 트래픽은 HTTP/2 서버에서 대부분의 과중 작업이 처리되기 때문에 두 연결간에 분할됩니다. GitLab Rails는 UploadPack을 처리하지 않기 때문에 영향을 받지 않습니다.