GitLab Developers Guide to Working with Gitaly.

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

ディープ・ダイブ

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

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

このディープダイブで取り上げた内容はすべて GitLab 11.11 時点でのものであり、それ以降に具体的な内容が変更されている可能性はありますが、それでも入門書としては十分に役立つはずです。

初心者ガイド

まずはGitalyリポジトリのBeginner’s guide to Gitalycontributionを読んでみてください。 Gitalyのセットアップ方法、Gitalyのさまざまなコンポーネントとその機能、テストスイートの実行方法などが説明されています。

新しいGit機能の開発者

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

これはまだ明確に定義されていない新しいプロセスです。 もしGitの機能に貢献したくて行き詰まっているのであれば、Gitalyチームか@jacobvosmaer-gitlabに連絡してください。

新しい機能」とは、外部から呼び出されるメソッドやクラスを意味しますlib/gitlab/gitlib/gitlab/gitlib/gitlab/gitの内部から呼び出される新しいメソッドについては、以下の「既存の Git 機能の変更」を参照ください。

ディスクアクセス(Rugged,git,rm -rfなど)を介してGitリポジトリに触れる新しいコードは、lib/gitlab/git以外のどこにもあってはなりません。

Gitalyの新機能を追加する手順は以下の通りです:

  • 探索/プロトタイピング
  • で新しいGitaly RPCをデザインして作成します。gitaly-proto
  • の新バージョンをリリースしました。gitaly-proto
  • GoまたはRubyで、GitalyのRPCの実装とテストを書いてください。
  • Gitalyの新バージョンをリリース
  • 新しいGitaly RPCを呼び出すGitLab CE/EE, GitLab Workhorse, GitLab Shellのクライアントコードを記述します。

これらのステップはしばしば重複します。 テストや開発中に、Gitalyとgitaly-proto の未発表バージョンを使用することは可能です。

  • 未発表のプロトコルでサーバーサイドのコードを書く手順については、Gitalyリポジトリを参照してください。
  • 変更したバージョンのGitalyでGitLab CEテストを実行する手順は以下をご覧ください。
  • GDK でgdk install を実行し、gdk run (またはgdk run app) を再起動すると、ローカルで変更された Gitaly バージョンを開発者に使用させることができます。

gitaly-ruby

Rubyのコードを使ってGitalyのRPCを実装し、テストすることが可能です。gitaly-ruby。これにより、Goのコードを書くのが苦手な開発者でも貢献しやすくなるはずです。

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 ディレクトリにあります。

注意: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 テストではtmp/tests/gitaly で指定したバージョンに固定された Gitaly のローカルクローンを使いますGITALY_SERVER_VERSION。 この GITALY_SERVER_VERSIONファイルはブランチや SHA もサポートしており、https://gitlab.com/gitlab-org/gitalyのカスタムコミットを使うことができます。

注:Gitaly の自動デプロイの導入に伴い、GITALY_SERVER_VERSION のフォーマットが Omnibus の構文に合わせられました。=revisionはサポートされなくなり、ファイルの内容は Git の参照 (ブランチまたは SHA) として評価され、センバーにマッチした場合のみvが付加されます。

変更されたGitalyのバージョンに対してローカルでテストを実行したい場合、tmp/tests/gitaly をシンボリックリンクで置き換えることができます。rspecを実行するたびにGitalyを再インストールする必要がなくなるため、この方がはるかに高速です。

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

GitalyのカスタムリポジトリをCIで使うには、例えばGitLabのフォークに常に自分のGitalyフォークを使わせたい場合、GITALY_REPO_URLCI環境変数として設定します。

Gitaly RPCクライアントをローカルで修正したバージョンを使用します。

新しいエンドポイントを追加したり、既存のエンドポイントに新しいパラメータを追加するなど、RPCクライアントに変更を加える場合は、Gitalyprotoのガイドに従ってください。 変更を加えたブランチをプッシュした後 (new-feature-branchなど):

  1. Rails’Gemfilegitaly 行を次のように変更します:

    gem 'gitaly', git: 'https://gitlab.com/gitlab-org/gitaly.git', branch: 'new-feature-branch'
    
  2. bundle install を実行して、変更した RPC クライアントを使用します。


開発者のドキュメントに戻る

機能フラグによる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

  1. 機能フラグを設定してRailsコンソールでテストします:

    注意:フラグの名前とRailsコンソールで使用されるフラグの名前に注意してください。 両者には違いがあります (ダッシュがアンダースコアに置き換えられ、名前のプレフィックスが変更されています)。 すべてのフラグのプレフィックスは必ずgitaly_にしてください。
    Feature.enable('gitaly_go_find_all_tags')
    

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