モノレポのパフォーマンスを改善する
モノレポは、サブプロジェクトを含むリポジトリです。単一アプリケーションには、相互に依存するプロジェクトが含まれていることがよくあります。たとえば、バックエンド、Webフロントエンド、iOSアプリケーション、Androidアプリケーションなどです。モノレポは一般的ですが、パフォーマンスのリスクを伴う場合があります。一般的な問題は次のとおりです:
- 大規模バイナリファイル。
- 履歴の長い多数のファイル。
- 多数の同時クローンとプッシュ。
- 垂直スケールの制限。
- ネットワーク帯域幅の制限。
- ディスク帯域幅の制限。
GitLab自体はGitベースです。そのGitストレージサービスであるGitalyは、モノレポに関連するパフォーマンス上の制約を経験します。私たちが学んだことは、独自のモノレポをより適切に管理するのに役立ちます。
- リポジトリの特性がパフォーマンスに与える影響。
- モノレポを最適化するためのいくつかのツールと手順。
モノレポ向けGitalyの最適化
Gitはオブジェクトをパックファイルに圧縮して、使用するスペースを減らします。クローン、フェッチ、またはプッシュすると、Gitはパックファイルを使用します。これらはディスクスペースとネットワーク帯域幅を削減しますが、パックファイルの作成には多くのCPUとメモリが必要です。
大規模なモノレポは、小規模なリポジトリよりも多くのコミット、ファイル、ブランチ、およびタグを含んでいます。オブジェクトが大きくなり、転送に時間がかかるようになると、パックファイルの作成はより高価になり、遅くなります。Gitでは、git-pack-objectsプロセスが最もリソースを大量に消費する操作です。これは、次の理由によります:
- コミット履歴とファイルを分析します。
- クライアントに送り返すファイルを決定します。
- パックファイルを作成します。
git cloneとgit fetchからのトラフィックは、サーバー上でgit-pack-objectsプロセスを開始します。GitLab CI/CDのような自動化された継続的インテグレーションシステムは、このトラフィックの多くを引き起こす可能性があります。大量の自動化されたCI/CDのトラフィックは、多数のクローンとフェッチリクエストを送信し、Gitalyサーバーに負担をかける可能性があります。
Gitalyサーバーへの負荷を軽減するには、これらの戦略を使用してください。
Gitalyのpack-objectsキャッシュを有効にする
Gitaly pack-objectsキャッシュを有効にすると、クローンとフェッチのサーバー負荷が軽減されます。
Gitクライアントがクローンまたはフェッチリクエストを送信すると、git-pack-objectsによって生成されたデータは再利用のためにキャッシュできます。モノレポが頻繁にクローンされる場合、Gitaly pack-objectsキャッシュを有効にすると、サーバー負荷が軽減されます。有効にすると、Gitalyは各クローンまたはフェッチ呼び出しの応答データを再生成する代わりに、インメモリキャッシュを維持します。
詳細については、Pack-objectsキャッシュを参照してください。
GitバンドルURIの設定
低レイテンシーのサードパーティストレージにGitバンドルを作成し、保存します(CDNなど)。Gitは最初にバンドルからパッケージをダウンロードし、次に残りのオブジェクトと参照をGitリモートからフェッチします。このアプローチにより、オブジェクトデータベースの起動が高速化され、Gitalyへの負荷が軽減されます。
- これにより、GitLabサーバーへのネットワーク接続が悪いユーザーのクローンとフェッチが高速化されます。
- CI/CDジョブを実行するサーバーの負荷を、バンドルを事前に読み込むことで軽減します。
詳細については、Bundle URIを参照してください。
Gitalyネゴシエーションのタイムアウトの設定
フェッチまたはリポジトリのアーカイブを試行すると、次の場合にfatal: the remote end hung up unexpectedlyエラーが発生する可能性があります:
- 大規模なリポジトリ。
- 多数のリポジトリを並行処理。
- 同じ大規模リポジトリを並行処理。
この問題を軽減するには、デフォルトのネゴシエーションタイムアウト値を増やしてください。
ハードウェアを適切にサイズ設定する
モノレポは通常、多くのユーザーを抱える大規模な組織向けです。モノレポをサポートするには、GitLab環境がGitLabテストプラットフォームおよびサポートチームによって提供されるリファレンスアーキテクチャのいずれかに一致する必要があります。これらのアーキテクチャは、パフォーマンスを維持しながらGitLabを大規模にデプロイするための推奨される方法です。
Git参照の数を減らす
Gitでは、参照は特定のコミットを指すブランチおよびタグ名です。Gitは参照を、リポジトリの.git/refsフォルダーにルーズファイルとして保存します。リポジトリ内のすべての参照を表示するには、git for-each-refを実行します。
リポジトリ内の参照の数が増えると、特定の参照を見つけるのに必要なシーク時間も長くなります。Gitが参照を解析するたびに、シーク時間が増加し、レイテンシーが増大します。
この問題を修正するために、Gitはpack-refsを使用して、そのリポジトリのすべての参照を含む単一の.git/packed-refsファイルを作成します。この方法は、refsに必要なストレージスペースを削減します。また、単一ファイルでのシークはディレクトリ内のすべてのファイルをシークするよりも高速であるため、シーク時間が短縮されます。
Gitは、新しく作成または更新された参照をルーズファイルで処理します。git pack-refsを実行するまで、これらはクリーンアップされて.git/packed-refsファイルに追加されません。Gitalyはハウスキーピング中にgit pack-refsを実行します。これは多くのリポジトリに役立ちますが、書き込み負荷の高いリポジトリには依然として次のパフォーマンス問題があります:
- 参照を作成または更新すると、新しいルーズファイルが作成されます。
- 参照を削除するには、既存の
packed-refsファイルを編集して既存の参照を削除する必要があります。
Gitは、フェッチまたはリポジトリをクローンするときに、すべての参照をイテレーションを行うします。サーバーは、各参照の内部グラフ構造をレビュー(「ウォーク」)し、不足しているオブジェクトを見つけてクライアントに送信します。イテレーションおよびウォークプロセスはCPUを大量に消費し、レイテンシーが増加します。このレイテンシーは、アクティビティが多いリポジトリでドミノ効果を引き起こす可能性があります。各操作が遅くなり、各操作が後続の操作を停止させます。
モノレポ内の多数の参照の影響を軽減するには:
古いブランチをクリーンアップするための自動化プロセスを作成します。
特定の参照をクライアントに表示する必要がない場合は、
transfer.hideRefs設定を使用して非表示にします。Gitalyはサーバー上のGit設定を無視するため、/etc/gitlab/gitlab.rbでGitaly設定自体を変更する必要があります:gitaly['configuration'] = { # ... git: { # ... config: [ # ... { key: "transfer.hideRefs", value: "refs/namespace_to_hide" }, ], }, }
Git 2.42.0以降では、オブジェクトグラフウォークを行う際に、異なるGit操作が非表示の参照をスキップできます。
リポジトリ最適化タスクのスケジュール
Gitリポジトリのオブジェクトデータベースにデータが格納される方法は、時間の経過とともに非効率になり、Git操作が遅くなります。最大実行時間を指定してGitalyで日次バックグラウンドタスクをスケジュールすることで、これらの項目をクリーンアップし、パフォーマンスを向上させることができます。
モノレポ向けCI/CDの最適化
GitLabをモノレポと共にスケールする状態に保つには、CI/CDジョブがリポジトリとどのように連携するかを最適化します。大規模で長いパイプラインは、モノレポの一般的な問題点です。モノレポのパイプライン設定で、変更の種類を検出するビルドルールを使用し、次のことを行います:
- 不要なジョブをスキップします。
- 関連するジョブのみを子パイプラインで実行します。
CI/CDでの同時クローン数を減らす
CI/CDパイプラインの並行処理を、スケジュールされたパイプラインをずらして異なる時間に実行することで軽減します。数分の違いでも役立ちます。
CI/CDの負荷は、パイプラインが特定の時間にスケジュールされるため、多くの場合並行処理されます。これらの時間帯には、リポジトリへのGitリクエストが急増し、CI/CDプロセスとユーザーのパフォーマンスに影響を与える可能性があります。
CI/CDプロセスでシャロークローンとフィルターを使用する
CI/CDシステムでのgit cloneおよびgit fetchの呼び出しでは、これらのオプションで転送されるデータ量を制限できます:
CI/CDでのシャロークローン
--depthフィルターは、いわゆる_シャロークローン_を作成します。GitLabとGitLab Runnerは、デフォルトでシャロークローンを実行します。
クローン深度は、GIT_DEPTHを使用してGitLab CI/CDパイプライン設定で構成できます。例:
variables:
GIT_DEPTH: 10
test:
script:
- ls -alCI/CDでの部分クローン
--filterオプションを使用すると、_部分クローン_を作成できます。この引数をgit-cloneに渡すには、GIT_CLONE_EXTRA_FLAGS変数を設定します。たとえば、blobの最大サイズを1MBに制限するには、次を追加します:
variables:
GIT_CLONE_EXTRA_FLAGS: --filter=blob:limit=1mパスとオブジェクトの種類をフィルターで除外する
特定のタイプまたは特定のパスのオブジェクトをフィルターで除外するには、git sparse-checkoutオプションを使用します。詳細については、ファイルパスでフィルターを参照してください。
CI/CD操作でgit fetchを使用する
リポジトリの実行コピーを維持できる場合は、CI/CDシステムでgit cloneの代わりにgit fetchを使用してください。git fetchはサーバーからの作業が少なくて済みます:
git cloneはリポジトリ全体を最初からリクエストします。git-pack-objectsはすべてのブランチとタグを処理して送信する必要があります。git fetchは、リポジトリに不足しているGit参照のみをリクエストします。git-pack-objectsは、Git参照の合計のうち、サブセットのみを処理します。この戦略は、転送される総データ量も削減します。
デフォルトでは、GitLabは大規模なリポジトリに推奨されるfetch Git戦略を使用します。
git cloneパスを設定する
モノレポがフォークベースのワークフローで使用される場合、GIT_CLONE_PATHを設定してリポジトリをクローンする場所を制御することを検討してください。
Gitはフォークを、別個のリポジトリとして別個のワークツリーで保存します。GitLab Runnerはワークツリーの使用を最適化できません。GitLab Runnerのexecutorは、指定されたプロジェクトに対してのみ設定および使用してください。プロセスをより効率的にするために、異なるプロジェクト間で共有しないでください。
GIT_CLONE_PATHは$CI_BUILDS_DIRで設定されたディレクトリ内にある必要があります。ディスクから任意のパスを選択することはできません。
CI/CDジョブでのgit cleanを無効にする
git cleanコマンドは、ワークツリーから追跡されていないファイルを削除します。大規模なリポジトリでは、大量のディスクI/Oを使用します。既存のマシンを再利用し、既存のワークツリーを再利用できる場合は、CI/CDジョブでこれを無効にすることを検討してください。たとえば、GIT_CLEAN_FLAGS: -ffdx -e .build/を使用すると、実行間でワークツリーからディレクトリを削除することを回避できます。これにより、増分ビルドが高速化される可能性があります。
CI/CDジョブでgit cleanを無効にするには、GIT_CLEAN_FLAGSをnoneに設定します。
デフォルトでは、GitLabは以下を保証します:
- 指定されたSHAにワークツリーがあります。
- リポジトリがクリーンです。
GIT_CLEAN_FLAGSが受け入れる正確なパラメータについては、git cleanのGitドキュメントを参照してください。使用可能なパラメータは、Gitのバージョンによって異なります。
git fetchの動作をフラグで変更する
git fetchの動作を変更して、CI/CDジョブが不要なデータを除外するようにします。プロジェクトに多数のタグが含まれており、CI/CDジョブでこれらが不要な場合は、GIT_FETCH_EXTRA_FLAGSを使用して--no-tagsを設定します。この設定により、フェッチをより高速かつコンパクトにできます。
リポジトリに多くのタグが含まれていなくても、--no-tagsがパフォーマンスを向上させる場合があります。詳細については、イシュー746およびGIT_FETCH_EXTRA_FLAGS Gitドキュメントを参照してください。
Runnerにロングポーリングを使用する
Runnerは、新しいCI/CDジョブのためにGitLabインスタンスを定期的にポーリングします。ポーリングの間隔は、次の両方に依存します:
check_interval設定。- Runner設定ファイルで構成されたRunnerの数。
サーバーが多数のRunnerを処理する場合、このポーリングは、キューイング時間の延長やCPU使用率の増加など、GitLabインスタンスのパフォーマンス問題を引き起こす可能性があります。ロングポーリングは、新しいジョブの準備が整うまで、Runnerからのジョブリクエストを保持します。
設定手順については、ロングポーリングを参照してください。
モノレポ向けGitの最適化
GitLabをモノレポと共にスケールする状態に保つには、リポジトリ自体を最適化します。
開発でのシャロークローンを避ける
開発でのシャロークローンは避けてください。シャロークローンは、変更をプッシュするのに必要な時間を大幅に増やします。シャロークローンはCI/CDジョブとうまく連携します。これは、リポジトリのコンテンツがチェックアウト後に変更されないためです。
ローカル開発では、代わりに部分クローンを使用して、次のことを行います:
- blobをフィルターで除外する(
git clone --filter=blob:noneを使用)。 - ツリーをフィルターで除外する(
git clone --filter=tree:0を使用)。
詳細については、クローンサイズの削減を参照してください。
リポジトリをプロファイルして問題を見つける
大規模なリポジトリは、一般的にGitでパフォーマンスの問題を経験します。git-sizerプロジェクトはリポジトリをプロファイルし、潜在的な問題を理解するのに役立ちます。これは、パフォーマンス問題を防ぐための軽減戦略を開発するのに役立ちます。リポジトリを分析するには、すべてのGit参照が存在することを確認するために、完全なGitミラーまたはベアクローンが必要です。
git-sizerを使用してリポジトリをプロファイルするには:
git-sizerをインストールします。git-sizerと互換性のあるベアGit形式でリポジトリをクローンするには、このコマンドを実行します:git clone --mirror <git_repo_url>Gitリポジトリのディレクトリで、すべての統計情報とともに
git-sizerを実行します:git-sizer -v
処理後、git-sizerの出力はこの例のようになります。各行には、リポジトリのその側面に対するLevel of concernが含まれます。懸念レベルが高いほど、より多くのアスタリスクで表示されます。極めて懸念レベルが高い項目には、感嘆符が表示されます。この例では、いくつかの項目に高い懸念レベルがあります:
Processing blobs: 1652370
Processing trees: 3396199
Processing commits: 722647
Matching commits to trees: 722647
Processing annotated tags: 534
Processing references: 539
| Name | Value | Level of concern |
| ---------------------------- | --------- | ------------------------------ |
| Overall repository size | | |
| * Commits | | |
| * Count | 723 k | * |
| * Total size | 525 MiB | ** |
| * Trees | | |
| * Count | 3.40 M | ** |
| * Total size | 9.00 GiB | **** |
| * Total tree entries | 264 M | ***** |
| * Blobs | | |
| * Count | 1.65 M | * |
| * Total size | 55.8 GiB | ***** |
| * Annotated tags | | |
| * Count | 534 | |
| * References | | |
| * Count | 539 | |
| | | |
| Biggest objects | | |
| * Commits | | |
| * Maximum size [1] | 72.7 KiB | * |
| * Maximum parents [2] | 66 | ****** |
| * Trees | | |
| * Maximum entries [3] | 1.68 k | * |
| * Blobs | | |
| * Maximum size [4] | 13.5 MiB | * |
| | | |
| History structure | | |
| * Maximum history depth | 136 k | |
| * Maximum tag depth [5] | 1 | |
| | | |
| Biggest checkouts | | |
| * Number of directories [6] | 4.38 k | ** |
| * Maximum path depth [7] | 13 | * |
| * Maximum path length [8] | 134 B | * |
| * Number of files [9] | 62.3 k | * |
| * Total size of files [9] | 747 MiB | |
| * Number of symlinks [10] | 40 | |
| * Number of submodules | 0 | |大規模バイナリファイルにGit LFSを使用する
バイナリファイル(パッケージ、オーディオ、ビデオ、グラフィックスなど)をGit Large File Storage(LFS)オブジェクトとして保存します。
ユーザーがファイルをGitにコミットすると、Gitはblob object typeを使用してそのコンテンツを保存および管理します。Gitは大規模なバイナリデータを効率的に処理しないため、大規模なblobはGitにとって問題となります。git-sizerが10MBを超えるblobを報告する場合、通常、リポジトリには大規模なバイナリファイルがあります。大規模なバイナリファイルは、サーバーとクライアントの両方に問題を引き起こします:
- サーバーの場合: テキストベースのソースコードとは異なり、バイナリデータは多くの場合、すでに圧縮されています。Gitはバイナリデータをこれ以上圧縮できないため、大規模なパックファイルにつながります。大規模なパックファイルは、作成と送信により多くのCPU、メモリ、帯域幅を必要とします。
- クライアントの場合: Gitはblobコンテンツをパックファイル(通常は
.git/objects/pack/内)と通常のファイル(ワークツリー内)の両方に保存しますが、バイナリファイルはテキストベースのソースコードよりもはるかに多くのスペースを必要とします。
Git LFSは、オブジェクトストレージなどの外部にオブジェクトを保存します。Gitリポジトリには、バイナリファイル自体ではなく、オブジェクトの場所へのポインターが含まれています。これにより、リポジトリのパフォーマンスを向上させることができます。詳細については、Git LFSドキュメントを参照してください。