Gitaly開発ガイドライン

GitalyはGitLab Rails、Workhorse、GitLab Shellで使用される高レベルのGit RPCサービスです。

ディープダイブ

2019年5月、Bob Van LanduytはGitalyプロジェクトに関するDeep Dive(GitLabチームメンバー限定:https://gitlab.com/gitlab-org/create-stage/-/issues/1 )を開催しました。このディープダイブでは、Ruby開発者としてGitalyプロジェクトに貢献する方法や、将来Gitalyプロジェクトで働く可能性のある人たちとドメイン固有の知識を共有しました。

このページは 録画はYouTubeで、スライドはGoogleスライドと PDFでご覧いただけます。

このディープダイブで扱った内容はすべて GitLab 11.11 時点でのもので、具体的な内容は変更されているかもしれませんが、それでも入門編としては十分なものです。

初心者向けガイド

GitalyリポジトリのGitaly貢献のための初心者ガイドを読むことから始めましょう。Gitalyのセットアップ方法、Gitalyのさまざまなコンポーネントとその機能、テストスイートの実行方法について説明されています。

Git の新機能の開発

Gitのデータを読み書きするには、Gitalyにリクエストする必要があります。つまり、lib/gitlab/git でまだ利用できないデータが必要な新機能を開発する場合は、Gitaly に変更を加えなければなりません。

gitlab リポジトリのどこにも、ディスクアクセス経由で Git リポジトリに触れるような新しいコード(たとえば、Rugged、gitrm -rf )があってはなりません。Gitリポジトリに直接アクセスする必要があるものはすべてGitalyで実装し、RPCを介して公開しなければなりません

Gitalyで新機能を開発する場合、その新機能を使おうとするGitLabへの変更を別のマージリクエストにし、Gitalyのマージ直後にマージする方が簡単なことがよくあります。こうすることで、マージされる前に変更をテストすることができます。

  • 変更したバージョンのGitalyでGitLabのテストを実行する手順は以下をご覧ください。
  • GDK でgdk install を実行し、gdk restart を使って GDK を再起動すると、ローカルで変更した Gitaly バージョンを開発に使うことができます。

Gitalyの問題でテスト・スイートが失敗する場合、最初のステップとして、実行してみてください:

rm -rf tmp/tests/gitaly

RSpecテスト中、Gitalyインスタンスはgitlab/log/gitaly-test.log にログを書き込みます。

レガシーRuggedコード

GitalyはすべてのGitアクセスを処理できますが、GitLabの顧客の多くはまだNFSの上でGitalyを実行しています。Git呼び出しのためのレガシーなRugged実装は、N+1 Gitaly呼び出しやその他の理由により、Gitaly RPCよりも高速かもしれません。詳細はイシューをご覧ください。

GitLabがこれらの非効率性のほとんどを取り除くか、Gitデータに対するNFSの使用が廃止されるまで、最もよく使われるRPCのいくつかのRugged実装は機能フラグによって有効にすることができます:

  • rugged_find_commit
  • rugged_get_tree_entries
  • rugged_tree_entry
  • rugged_commit_is_ancestor
  • rugged_commit_tree_entry
  • rugged_list_commits_by_oid

便利な Rake タスクを使えば、これらのフラグを一括して有効にも無効にもできます。有効にするには

bundle exec rake gitlab:features:enable_rugged

無効にするには

bundle exec rake gitlab:features:disable_rugged

このコードのほとんどはlib/gitlab/git/rugged_impl ディレクトリにあります。

note
Gitalyチームと明確に議論しない限り、Ruggedに関連するコードを追加したり修正したりする必要はありません。このコードはGitLab.comやNFSを使わない他のGitLabインスタンスでは動作しません。

TooManyInvocationsError エラー

開発中やテスト中に、Gitlab::GitalyClient::TooManyInvocationsError エラーが発生することがあります。GitalyClient では、1回のRailsリクエストまたはSidekiq実行でGitalyが30回以上呼び出された場合にこのエラーを発生させることで、潜在的なn+1問題に対するブロックを試みています。

一時的な対策として、GITALY_DISABLE_REQUEST_LIMITS=1 をエクスポートしてエラーを抑制してください。これにより、開発者環境でのn+1検出が無効になります。

GitLab 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. GITALY_SERVER_VERSIONNETファイルで指定したバージョンで固定されたtmp/tests/gitaly 、Gitalyのローカルクローンを使用します。GITALY_SERVER_VERSIONこの 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.toml.exampleからconfig.toml を、config.praefect.toml.exampleからpraefect.config.toml をコピーすることができます。コピー後、正しいパスを指すように編集してください。

rm -rf tmp/tests/gitaly
ln -s /path/to/gitaly tmp/tests/gitaly

テストを実行する前に、必ずmake を Gitaly の内部ディレクトリで実行してください。そうしないと、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 protobuf仕様のガイドに従ってください。次に

  1. Gitalyのtools/protogem ディレクトリでbundle install
  2. Gitalyのルート・ディレクトリからRPCクライアントgemをビルドします:

    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のGemfilegitaly 行を以下のように変更します:

    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 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. どの設定ファイルが使用されているか確認してください: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>/.マージは2つのヒストリをマージします。このref名前空間は、その下の以下のrefを使用してマージに関する情報を追跡します:
    • 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への参照。