Gitaly 개발 지침

Gitaly은 GitLab Rails, Workhorse 및 GitLab Shell에서 사용되는 고수준의 Git RPC 서비스입니다.

심층 탐구

5월 2019년, Bob Van Landuyt은 Gitaly 프로젝트에 대한 Deep Dive(모든 GitLab 팀 구성원: https://gitlab.com/gitlab-org/create-stage/-/issues/1)를 주최했습니다. 이 Deep Dive에는 Ruby 개발자로 참여하는 방법과 이 코드베이스의 향후 작업을 수행할 수 있는 사람들과 도메인 특화 지식이 공유되었습니다.

회의 녹화본은 YouTube에서, 슬라이드는 Google 슬라이드PDF에서 찾을 수 있습니다.

Gitaly용 Deep Dive에서 다룬 내용은 GitLab 11.11 기준으로 정확했으며, 특정 세부 정보가 변경되었더라도 여전히 좋은 소개 자료로 사용될 것입니다.

초보자 가이드

먼저 Gitaly 리포지토리의 Gitaly 기여 초보자 가이드를 읽어보세요. Gitaly 설정 방법, Gitaly의 다양한 구성 요소 및 역할, 그리고 테스트 슈트를 실행하는 방법에 대해 설명합니다.

새로운 Git 기능 개발

Git 데이터를 읽거나 쓰려면 Gitaly에 요청을 보내어야 합니다. 따라서, lib/gitlab/git의 변경 내용에 아직 사용할 수 없는 데이터가 필요한 새로운 기능을 개발 중이라면 Gitaly에 변경 사항을 반영해야 합니다.

gitlab 리포지토리에서 디스크 액세스를 통해 Git 리포지토리에 접근하는 새로운 코드가 있어서는 안 됩니다. Git 리포지토리에 직접 액세스해야 하는 모든 것은 반드시 Gitaly에 구현되어야 하며, RPC를 통해 노출되어야 합니다.

일반적으로 새로운 기능을 Gitaly에서 개발하는 것이 더 쉽습니다. 이렇게 하면 Gitaly MR 이후 즉시 별도의 MR을 엮을 수 있어 변경 내용을 병합하기 전에 변경 사항을 테스트할 수 있습니다.

  • 로컬 수정 버전의 Gitaly로 GitLab 테스트를 실행하는 방법은 아래를 참조하십시오.
  • GDK에서 gdk install을 실행하고 gdk restart를 사용하여 로컬 수정 버전의 Gitaly를 개발에 사용합니다.

Gitaly 관련 테스트 실패

Gitaly 문제로 인해 테스트 슈트가 실패하는 경우, 먼저 다음을 실행해 보십시오:

rm -rf tmp/tests/gitaly

RSpec 테스트 중 Gitaly 인스턴스는 gitlab/log/gitaly-test.log에 로그를 작성합니다.

TooManyInvocationsError 오류

개발 및 테스트 중에 Gitlab::GitalyClient::TooManyInvocationsError 오류를 경험할 수 있습니다. GitalyClient는 단일 Rails 요청이나 Sidekiq 실행에서 30회 이상 Gitaly가 호출될 경우 이 오류를 발생시켜 잠재적인 n+1 문제에 대비합니다.

임시 조치로 개발 환경에서 GITALY_DISABLE_REQUEST_LIMITS=1을 내보내어 이 오류를 억제할 수 있습니다. 이렇게 하면 개발 환경에서 n+1 탐지가 비활성화됩니다.

문제를 보고하려면 GitLab CE 또는 EE 리포지토리에 이슈를 등록하십시오. 레이블로 ~Gitaly, ~성능, ~기술 부채를 포함해야 합니다. 이슈에는 TooManyInvocationsError의 전체 스택 추적 및 오류 메시지뿐만 아니라 가능한 경우 알려진 실패하는 테스트도 포함해야 합니다.

n+1 문제의 원인을 분리하세요. 이는 대개 배열의 각 요소에 대해 Gitaly가 호출되는 루프입니다. 문제를 분리할 수 없다면, 도움이 필요하다면 Gitaly 팀의 구성원에 연락하세요.

원본이 발견된 후에는 다음과 같이 allow_n_plus_1_calls 블록으로 래핑하세요.

# n+1: n+1 문제에 대한 링크
Gitlab::GitalyClient.allow_n_plus_1_calls do
  # 원본 코드
  commits.each { |commit| ... }
end

이렇게 래핑된 코드 경로는 n+1 탐지에서 제외됩니다.

요청 횟수

커밋 및 기타 Git 데이터는 이제 Gitaly를 통해 검색됩니다. 이러한 검색은 데이터베이스와 마찬가지로 배치될 수 있습니다. 이는 클라이언트 및 Gitaly 자체의 성능을 향상시키고, 따라서 사용자들에게도 성능을 향상시킵니다. 성능을 안정적으로 유지하고 성능 회귀를 방지하기 위해 Gitaly 호출을 카운트하고 호출 횟수를 테스트할 수 있습니다. 이를 위해서는 :request_store 플래그가 설정되어 있어야 합니다.

describe 'Gitaly Request count tests' do
  context 'when the request store is activated', :request_store do
    it 'correctly counts the gitaly requests made' do
      expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(10)
    end
  end
end

Gitaly의 로컬 수정 버전으로 테스트 실행

일반적으로 GitLab CE/EE 테스트는 GITALY_SERVER_VERSION에서 지정된 버전으로 고정된 tmp/tests/gitaly의 로컬 클론을 사용합니다. GITALY_SERVER_VERSION 파일은 브랜치와 SHA를 지원하여 저장소에서 사용자 정의 커밋을 사용할 수 있습니다.

참고: Gitaly의 자동 배포가 소개되면서 GITALY_SERVER_VERSION의 형식이 Omnibus 구문과 일치하도록 조정되었습니다. 더 이상 =revision을 지원하지 않고 파일 내용을 Git 참조(브랜치 또는 SHA)로 평가합니다. 의미 있는 버전과 일치하는 경우에만 v를 접두사로 붙입니다.

로컬에서 수정된 버전의 Gitaly로 테스트를 실행하려면 tmp/tests/gitaly를 심볼릭 링크로 대체할 수 있습니다. 이렇게 하면 rspec를 실행할 때마다 Gitaly를 재설치하지 않아도 되므로 빠릅니다.

이 디렉토리에 config.tomlpraefect.config.toml 파일이 포함되어 있는지 확인하세요. config.tomlconfig.toml.example에서 복사할 수 있고, praefect.config.tomlconfig.praefect.toml.example에서 복사할 수 있습니다. 복사한 후 모두 올바른 경로를 가리키도록 편집해야 합니다.

rm -rf tmp/tests/gitaly
ln -s /경로/에서/gitaly tmp/tests/gitaly

테스트를 실행하기 전에 로컬 Gitaly 디렉토리에서 make 명령을 실행해야 합니다. 그렇지 않으면 Gitaly를 부팅할 수 없습니다.

테스트 실행간 로컬에서 Gitaly를 수정하면 각 테스트 실행 사이에 make 명령을 수동으로 다시 실행해야 합니다.

CI 테스트에서는 로컬로 수정된 Gitaly 버전을 사용하지 않습니다. CI에서 사용자 정의 Gitaly 버전을 사용하려면 이 섹션의 시작 부분에 설명된 대로 GITALY_SERVER_VERSION을 업데이트해야 합니다.

사용자의 변경 사항이 포함된 경우와 같이 Gitaly의 포크에서 다른 Gitaly 저장소를 사용하려면 테스트를 실행할 때 GITALY_REPO_URL 환경 변수를 지정할 수 있습니다:

GITALY_REPO_URL=https://gitlab.com/nick.thomas/gitaly bundle exec rspec spec/lib/gitlab/git/repository_spec.rb

사용자의 Gitaly 포크가 비공개인 경우 배포 토큰을 생성하고 URL에 지정할 수 있습니다:

GITALY_REPO_URL=https://gitlab+deploy-token-1000:token-here@gitlab.com/nick.thomas/gitaly bundle exec rspec spec/lib/gitlab/git/repository_spec.rb

CI/CD에서 사용자의 Gitaly 저장소를 사용하려면 GITALY_REPO_URLCI/CD 변수로 설정해야 합니다.

로컬로 수정된 Gitaly RPC 클라이언트 사용

RPC 클라이언트를 변경하려면 새 엔드포인트를 추가하거나 기존 엔드포인트에 새 매개변수를 추가하는 등 RPC 클라이언트를 변경하는 경우 Gitaly 프로토콜 버퍼 사양에 대한 지침을 따를 수 있습니다. 그 후:

  1. Gitaly의 tools/protogem 디렉토리에서 bundle install을 실행합니다.
  2. Gitaly의 루트 디렉토리에서 RPC 클라이언트 젬을 빌드합니다:

    BUILD_GEM_OPTIONS=--skip-verify-tag make build-proto-gem
    
  3. Gitaly의 _build 디렉토리에서 새로 생성된 .gem 파일을 해제하고 gemspec를 만듭니다:

    gem unpack gitaly.gem &&
    gem spec gitaly.gem > gitaly/gitaly.gemspec
    
  4. 레일즈의 Gemfile에서 gitaly 줄을 다음과 같이 수정합니다:

    gem 'gitaly', path: '../gitaly/_build'
    
  5. 수정된 RPC 클라이언트를 사용하려면 bundle install명령을 실행합니다.

새로운 변경 사항을 시도하려면 매번 단계 2-5를 다시 실행하면 됩니다.

기능 플래그로 RPC 래핑하기

Gitaly의 새로운 기능에 대한 기능 플래그를 적용하는 단계는 다음과 같습니다.

Gitaly

  1. 패키지 범위의 플래그 이름을 만듭니다:

    var findAllTagsFeatureFlag = "go-find-all-tags"
    
  2. featureflag 패키지를 사용하여 코드에서 스위치를 만듭니다:

    if featureflag.IsEnabled(ctx, findAllTagsFeatureFlag) {
      // go 구현
    } else {
      // 루비 구현
    }
    
  3. Prometheus 메트릭을 만듭니다:

    var findAllTagsRequests = prometheus.NewCounterVec(
      prometheus.CounterOpts{
        Name: "gitaly_find_all_tags_requests_total",
        Help: "Counter of go vs ruby implementation of FindAllTags",
      },
      []string{"implementation"},
    )
    
    func init() {
      prometheus.Register(findAllTagsRequests)
    }
    
    if featureflag.IsEnabled(ctx, findAllTagsFeatureFlag) {
      findAllTagsRequests.WithLabelValues("go").Inc()
      // go 구현
    } else {
      findAllTagsRequests.WithLabelValues("ruby").Inc()
      // 루비 구현
    }
    
  4. 테스트에서 헤더를 설정합니다:

    import (
      "google.golang.org/grpc/metadata"
    
      "gitlab.com/gitlab-org/gitaly/internal/featureflag"
    )
    
    //...
    
    md := metadata.New(map[string]string{featureflag.HeaderKey(findAllTagsFeatureFlag): "true"})
    ctx = metadata.NewOutgoingContext(context.Background(), md)
    
    c, err = client.FindAllTags(ctx, rpcRequest)
    require.NoError(t, err)
    

GitLab Rails

Rails 콘솔에서 기능 플래그를 설정하여 테스트합니다:

Feature.enable('gitaly_go_find_all_tags')

플래그의 이름과 Rails 콘솔에서 사용된 이름에 유의하십시오. 이 두 가지에는 차이가 있습니다 (대시가 언더스코어로 대체되고 이름이 변경됨). 모든 플래그에 gitaly_를 접두사로 붙이도록 주의하십시오.

참고: GitLab에서 플래그를 설정하지 않은 경우, 콘솔에서 기능 플래그는 false로 읽혀지며, Gitaly는 기본값을 사용합니다. 기본 값은 GitLab 버전에 따라 다릅니다.

GDK로 테스트하기

플래그가 올바르게 설정되었는지 확인하고 Gitaly에 제대로 전달되는지 확인하기 위해 GDK를 사용하여 통합을 체크할 수 있습니다:

  1. 플래그의 상태를 관찰할 수 있어야 합니다. 확인하려면 Prometheus 메트릭을 활성화해야 합니다.
    1. GDK 루트 디렉토리로 이동합니다.
    2. Gitaly에 적합한 브랜치를 확인했는지 확인합니다.
    3. make gitaly-setup로 다시 컴파일하고 gdk restart gitaly로 서비스를 다시 시작합니다.
    4. 설정이 실행 중인지 확인합니다: gdk status | grep praefect.
    5. 사용 중인 구성 파일을 확인합니다: cat ./services/praefect/run | grep praefect-config 플래그 값
    6. 구성 파일에서 prometheus_listen_addr을 주석 처리 해제한 다음 gdk restart gitaly을 실행합니다.
  2. 플래그가 아직 활성화되지 않았는지 확인하십시오:
    1. 프로젝트 생성, 커밋 제출 또는 히스토리 확인과 같은 필요한 작업을 수행합니다.
    2. 새로운 기능 플래그에 대한 현재 메트릭 목록이 있는지 확인합니다:

      curl --silent "http://localhost:9236/metrics" | grep go_find_all_tags
      
  3. 새로운 기능 플래그의 메트릭을 확인하고 증가한 후, 새로운 기능을 활성화할 수 있습니다:
    1. GDK 루트 디렉토리로 이동합니다.
    2. Rails 콘솔을 시작합니다:

      bundle install && bundle exec rails console
      
    3. 기능 플래그 목록을 확인합니다:

      Feature::Gitaly.server_feature_flags
      

      비활성화되어야 합니다. "gitaly-feature-go-find-all-tags"=>"false"로 표시됩니다.

    4. 활성화합니다:

      Feature.enable('gitaly_go_find_all_tags')
      
    5. Rails 콘솔을 종료하고 프로젝트 생성, 커밋 제출 또는 히스토리 확인과 같은 필요한 작업을 수행합니다.
    6. 해당 기능에 대한 메트릭을 관찰하여 기능이 활성화되었는지 확인합니다:

      curl --silent "http://localhost:9236/metrics" | grep go_find_all_tags
      

테스트에서 Praefect 사용

테스트에서 기본 Praefect는 메모리 기반 선출 전략을 사용합니다. 이 전략은 더 이상 사용되지 않으며 주로 단위 테스트 목적으로 유지됩니다.

더 현대적인 선출 전략은 PostgreSQL 데이터베이스와의 연결을 필요로 합니다. 이 동작은 테스트 실행 시 기본적으로 비활성화되어 있지만 환경에서 GITALY_PRAEFECT_WITH_DB=1을 설정하여 활성화할 수 있습니다.

이를 위해서는 PostgreSQL을 실행 중이어야 하고 데이터베이스를 생성해야 합니다. GDK를 사용하는 경우 다음과 같이 설정할 수 있습니다:

  1. 데이터베이스를 시작합니다: gdk start db
  2. GDK에서 환경을 불러옵니다: eval $(cd ../gitaly && gdk env)
  3. 데이터베이스를 생성합니다: createdb --encoding=UTF8 --locale=C --echo praefect_test

Gitaly에서 사용하는 Git 참조

Gitaly는 GitLab에 Git 서비스를 제공하기 위해 많은 Git 참조(refs)를 사용합니다.

표준 Git 참조

이러한 표준 Git 참조는 GitLab(을 통해 Gitaly)에서 모든 Git 저장소에서 사용됩니다:

  • refs/heads/. 브랜치에 사용됩니다. git branch 문서를 참조하세요.
  • refs/tags/. 태그에 사용됩니다. git tag 문서를 참조하세요.

GitLab 특정 참조

이 GitLab 특정 참조는 GitLab(을 통해 Gitaly)에서 독점적으로 사용됩니다:

  • refs/keep-around/<object-id>. 파이프라인 작업 또는 병합 요청을 가진 커밋을 참조합니다. object-id는 파이프라인이 실행된 커밋을 가리킵니다.
  • refs/merge-requests/<merge-request-iid>/. 병합은 두 개의 이력을 병합합니다. 이 참조 네임스페이스는 다음과 같은 하위 참조를 사용하여 병합에 대한 정보를 추적합니다:
    • head. 병합 요청의 현재 HEAD.
    • merge. 병합 요청에 대한 커밋. 모든 병합 요청은 refs/keep-around 아래에 커밋 객체를 생성합니다.
    • 병합 기차가 활성화된 경우: train. 병합 기차에 대한 커밋.
  • refs/pipelines/<pipeline-iid>. 파이프라인에 대한 참조. 일시적으로 파이프라인 커밋 객체 ID를 저장합니다.
  • refs/environments/<environment-slug>. 환경 배포가 수행된 커밋을 참조합니다.
  • refs/heads/revert-<source-commit-short-object-id>. 변경 사항을 되돌리는 경우에 생성된 커밋의 객체 ID를 참조합니다.