Gitaly 개발 가이드

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

심층적인 탐구

2019년 5월, Bob Van Landuyt는 Gitaly 프로젝트에 대해 심층적으로 다룬 GitLab 팀 멤버만을 위한 Deep Dive(심층 탐구)를 진행했습니다. 루비 개발자로서의 기여 방법 및 향후에 이 코드베이스의 일부에서 작업할 수 있는 누구에게도 도메인 특화 지식을 공유했습니다.

유튜브 녹화Google 슬라이드(으)로 녹화 내용과 슬라이드를 확인할 수 있으며 PDF에서도 확인할 수 있습니다.

이 심층 탐구에서 다룬 모든 내용은 GitLab 11.11을 기준으로 정확했지만, 구체적인 세부 정보가 변경되었더라도 여전히 좋은 입문 자료로 사용될 것입니다.

초보자 가이드

우선 Gitaly 기여 초보자를 위한 가이드를 읽고 시작하세요. 이 문서에서는 Gitaly 설정 방법, Gitaly의 다양한 컴포넌트 및 역할, 그리고 테스트 스위트를 실행하는 방법에 대해 설명합니다.

새로운 Git 기능 개발

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

gitlab 리포지터리 어디에서든 직접적인 Git 리포지터리 액세스가 있는 새로운 코드는 없어야 합니다. Git 리포지터리에 직접 액세스가 필요한 모든 것은 반드시 Gitaly에서 구현하고 RPC를 통해 노출시켜야 합니다.

일반적으로 Gitaly에서 새로운 기능을 개발할 때는 Gitaly의 변경 사항을 즉시 Merge되도록 별도로 Merge Request을 통해 새로운 기능을 사용하려는 GitLab에 변경 사항을 만드는 것이 더 쉽습니다. 이렇게 하면 변경 사항이 Merge되기 전에 변경 사항을 테스트할 수 있습니다.

  • 지역 수정된 버전의 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 실패를 경험할 수 있습니다. Gitaly 호출이 단일 Rails 요청 또는 Sidekiq 실행에서 30번 이상 호출될 경우 이 오류가 발생합니다.

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

문제를 보고하려면 TooManyInvocationsError의 전체 스택 추적 및 오류 메시지가 포함된 이슈를 GitLab CE 또는 EE 리포지터리에 등록하세요. ~Gitaly, ~성능, ~기술적 부채 레이블을 포함하세요. 가능한 경우 알려진 실패하는 테스트도 포함하세요.

n+1 문제의 원인을 분리하세요. 일반적으로 이는 배열의 각 요소에 대해 Gitaly가 호출되는 루프입니다. 문제를 분리할 수 없는 경우 Gitaly 팀 멤버에게 도움을 요청하세요.

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

# n+1: link to n+1 issue
Gitlab::GitalyClient.allow_n_plus_1_calls do
  # 원본 코드
  commits.each { |commit| ... }
end

이 블록으로 래핑된 코드 경로는 n+1 감지에서 제외됩니다.

요청 횟수

커밋 및 기타 Git 데이터는 이제 Gitaly를 통해 검색됩니다. 이러한 검색은 데이터베이스와 마찬가지로 일괄 처리될 수 있습니다. 이는 클라이언트 및 Gitaly 자체의 성능을 향상시키며 따라서 사용자에게도 이점을 제공합니다. 성능을 안정적으로 유지하고 성능 감소를 방지하려면 Gitaly 호출을 세어서 호출 수를 테스트할 수 있습니다. 이를 위해 :request_store 플래그를 설정해야 합니다. ruby 'Gitaly 요청 횟수 테스트'에 대해 설명합니다 do '요청 리포지터리가 활성화된 경우', :request_store do it '정확하게 수행된 gitaly 요청을 계산합니다' 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_VERS

로컬 수정 버전의 Gitaly RPC 클라이언트 사용

RPC 클라이언트를 수정하는 경우, 새로운 엔드포인트를 추가하거나 기존 엔드포인트에 새로운 매개변수를 추가하는 등의 변경 사항을 가할 때는 Gitaly protobuf 사억서(Kor)을 따르세요. 그런 다음:

  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를 다시 실행하세요.


개발 문서로 돌아가기(Kor)

피처 플래그로 RPC 래핑

Gitaly에서 새로운 기능을 피처 플래그 뒤로 숨기려면 다음 단계를 따릅니다.

Gitaly

  1. 패키지 범위 피처 플래그 이름을 만드세요:

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

    if featureflag.IsEnabled(ctx, findAllTagsFeatureFlag) {
      // go implementation
    } else {
      // ruby implementation
    }
    
  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 implementation
    } else {
      findAllTagsRequests.WithLabelValues("ruby").Inc()
      // ruby implementation
    }
    
  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에서 설정되지 않은 경우, 피처 플래그는 콘솔에서 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. 사용 중인 구성 파일을 확인하세요: -config 플래그의 값으로 -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(Kor))를 사용합니다.

표준 Git 참조

이러한 표준 Git 참조는 Gitaly(을 통해 GitLab)에서 모든 Git 리포지터리에서 사용됩니다:

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

GitLab-특정 참조

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

  • refs/keep-around/<object-id>. 파이프라인 작업이나 Merge Request을 포함하는 커밋을 참조합니다. object-id는 파이프라인이 실행된 커밋을 가리킵니다.
  • refs/merge-requests/<merge-request-iid>/. Merge(Kor)은 두 가지 히스토리를 함께 Merge합니다. 이 참조 네임스페이스는 다음을 이용해 Merge 정보를 추적합니다:
    • head. Merge Request의 현재 HEAD.
    • merge. Merge Request에 대한 커밋. 각 Merge Request은 refs/keep-around 아래의 커밋 객체를 만듭니다.
    • Merge 기관이 활성화된 경우: train. Merge 기관용 커밋.
  • refs/pipelines/<pipeline-iid>. 파이프라인을 참조합니다. 일시적으로 파이프라인 커밋 객체 ID를 저장하는 데 사용됩니다.
  • refs/environments/<environment-slug>. 환경으로의 배포가 수행된 커밋을 참조합니다.
  • refs/heads/revert-<source-commit-short-object-id>. 변경사항 되돌리기(Kor) 시 만들어진 커밋의 객체 ID를 참조합니다.