Gitaly 개발 지침

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

심층 분석

2019년 5월, Bob Van Landuyt는 Gitaly 프로젝트에 대한 심층 분석을 진행했습니다. 이는 Ruby 개발자로서 이에 기여하는 방법과 해당 코드베이스에 향후 작업할 수 있는 누구에게나 도메인 특화 지식을 공유하는 것을 포함했습니다.

Youtube에서 녹화본을 확인할 수 있으며, Google 슬라이드PDF에서 슬라이드를 찾을 수 있습니다.

이 심층 분석에서 다룬 내용은 GitLab 11.11 기준으로 정확했지만, 특정 세부 사항은 변경되었을 수 있지만 여전히 좋은 소개 자료로 사용될 것으로 예상됩니다.

초심자 가이드

먼저 Gitaly 리포지터리의 Gitaly 기여 초심자 가이드를 읽어보세요. Gitaly 설정 방법, Gitaly의 여러 컴포넌트 및 기능, 그리고 테스트 스위트를 실행하는 방법에 대해 설명합니다.

새로운 Git 기능 개발

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

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

Gitaly에서 새로운 기능을 개발하기 위해 일반적으로 해당 기능을 사용할 GitLab에서 변경 사항을 별도로 Merge Request으로 만드는 것이 좋습니다. 이렇게 하면 변경 사항을 Merge하기 전에 변경 내용을 테스트할 수 있습니다.

  • 지원: 아래에서 로컬 수정 버전의 Gitaly로 GitLab 테스트하는 지침을 참조하세요.
  • Gitaly를 로컬에서 수정한 버전으로 설치하려면 GDK에서 gdk install을 실행하고 gdk restart로 GDK를 재시작하세요.

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 감지를 비활성화합니다.

CE 또는 EE 리포지터리에서 문제를 보고하세요. 라벨 ~Gitaly, ~performance, ~"technical debt"를 포함하세요. 문제에는 TooManyInvocationsError의 전체 스택 트레이스와 오류 메시지뿐만 아니라 가능한 경우 알려진 실패 테스트를 포함해야 합니다.

n+1 문제의 원인을 분리하세요. 대개 배열의 각 요소에 대해 Gitaly가 호출되는 반복문입니다. 문제를 분리할 수 없는 경우 Gitaly 팀의 구성원에게 도움을 요청하세요.

원인을 찾은 후 다음과 같이 allow_n_plus_1_calls 블록으로 래핑하세요:

# n+1: link to n+1 issue
Gitlab::GitalyClient.allow_n_plus_1_calls do
  # original code
  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의 로컬 클론을 사용합니다. GITALY_SERVER_VERSION 파일은 브랜치 및 SHA도 지원하여 리포지터리의 사용자 정의 커밋을 사용할 수 있습니다.

note
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 /path/to/gitaly tmp/tests/gitaly

테스트를 실행하기 전에 로컬 Gitaly 디렉터리에서 make를 실행하세요. 그렇지 않으면 Gitaly가 부팅하지 못할 수 있습니다.

테스트 실행 사이에 로컬 Gitaly를 수정하면 테스트를 다시 실행하기 전에 매뉴얼으로 make를 다시 실행해야 합니다.

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

변경 사항이 별도 포크에 있는 등 사용자 정의 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 리포지터리를 사용하려면 GitLab 포크가 항상 사용자 자체 Gitaly 포크를 사용하도록하려면 CI/CD 변수GITALY_REPO_URL을 설정하세요.

로컬 수정 버전의 Gitaly 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. Rails의 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: "FindAllTags의 go vs ruby 구현 수 카운터",
      },
      []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_로 접두사를 붙이도록 주의하십시오.

note
GitLab에서 설정되지 않은 경우, 피처 플래그는 콘솔에서 거짓으로 읽히며 Gitaly에서 기본값을 사용합니다. 기본값은 GitLab 버전에 따라 다릅니다.

GDK로 테스트하기

플래그가 올바르게 설정되어 Gitaly에 전달되는지 확인하려면 GDK를 사용하여 통합을 확인할 수 있습니다.

  1. 플래그 상태를 확인할 수 있어야 합니다. 이를 확인하려면 다음을 수행하고 활성화시켜야합니다:
    1. GDK 루트 디렉터리로 이동합니다.
    2. Gitaly에 적합한 브랜치를 체크아웃했는지 확인합니다.
    3. make gitaly-setup으로 다시 컴파일하고 gdk restart gitaly로 서비스를 재시작합니다.
    4. 설정이 실행 중인지 확인합니다: gdk status | grep praefect.
    5. 사용 중인 구성 파일을 확인합니다: 설정 파일에서 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 Dedicated 참조

이러한 GitLab Dedicated 참조는 GitLab(을 통한 Gitaly)에서 전용으로 사용됩니다:

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