Gitの圧縮・HTTPバッファ設定はcloneを速くするか


GitHubとGitLabの公開リポジトリで、CIの高速化のために次の2設定を比較しました。

core.compression = 0
http.postBuffer = 1073741824

比較対象はHTTPSのshallow cloneと、その直後の全ファイルcheckoutで、個別のパターンと、併用するパターンで検証しました。

結論は効果がなく、両サービスで、設定を変えても受信packのサイズとSHA-256は一致、HTTPリクエスト数とPOST本文のサイズも一致しています。

この記事では、Gitの公式仕様、実測した結果を説明します

調査のきっかけ

CIのcheckoutを高速化する方法として、圧縮の無効化、HTTP POSTバッファの拡大を紹介する文章を読みました。主な説明は次の2点です。

  • 圧縮を無効にすると、CPU使用時間を減らし、帯域幅を多く使って転送できる。
  • HTTP POSTバッファを増やすと、大規模リポジトリの転送で往復回数を減らせる。

こういう設定に関しては、クライアントとサーバ、どこに設定するのか、どの方向の通信なのかが間違いやすいポイントです。以下説明していきます。

Gitが転送するデータ

object、loose object、pack

Gitはファイルの内容、ディレクトリ構造、コミットなどをobjectとして保存します。ファイル内容を表すobjectがblobです

objectの保存形式には、主に次の2つがあります。

保存形式内容
loose object1つのobjectを1つのファイルに保存する
pack複数のobjectを1つのpackファイルにまとめる

packには、object間の共通部分を利用する差分表現と、zlibによる圧縮を使用できます。core.compression=0 が指定するのはzlibの圧縮レベルです。差分表現をすべて無効にする設定ではない。

また、packを用意する際には、保存済みの圧縮データを再利用することがあります。毎回すべてのobjectを展開し、圧縮し直すとは限りません。

cloneとfetchの流れ

HTTPSの通常のGit転送では、クライアントが取得したいobjectなどを要求し、サーバが転送用のpackを用意します。

クライアント                         サーバ
    │                                 │
    ├── 取得要求 ──────────────────────>│
    │                          packを用意
    │<────────────────────── pack送信 ─┤
  受信・検証・保存                      │
    │                                 │

cloneは新しいローカルリポジトリを作ります。通常はobjectの取得後にcheckoutも実行し、選択したコミットのファイルを作業ディレクトリに書き出します。

fetchは既存のローカルリポジトリにobjectや参照を取得します。fetch単独では作業ディレクトリのファイルを更新しません。

checkout中にも、圧縮されたobjectの読み出しやファイルへの書き込みが発生します。git clone 全体の時間には、ネットワーク転送以外の処理も含まれます。

pushの流れ

pushでは、クライアントがサーバに必要なobjectを送ります。

クライアント                         サーバ
    │                                 │
  packを用意                           │
    ├── pack送信 ─────────────────────>│
    │                          受信・検証・保存
    │<────────────────────── 処理結果 ─┤

cloneとpushでは、転送用packを用意する側が逆になります。

git config は実行した側の設定を変更する

次のコマンドをCI runnerで実行すると、runner上のユーザー設定が変わります。

git config --global core.compression 0

--global の範囲は、その環境のユーザーです。GitHubやGitLabのサーバに設定を送る操作ではありません。

自前のGitサーバでも、サーバ上のGitプロセスが読む設定を変更すれば、そのプロセスに作用します。適用される設定ファイルや実行ユーザーは、サーバの運用方法によって異なります。

GitHub.comやGitLab.comの利用者は、クライアントの git config からホスティングサービスの圧縮設定を変更できません。

一時的な比較では、git -c を使うと設定ファイルを書き換えずに実行できます。

git -c core.compression=0 clone https://example.com/group/repo.git

この例では、起動したGitと、そのコマンドから起動するGitの子プロセスに設定が渡ります。

core.compression=0 が変更するもの

圧縮レベルの既定値

Gitの公式仕様では、core.compression は圧縮レベルの既定値です。0 は圧縮なし、-1 はzlibの既定値、1〜9 は速度とサイズの異なる圧縮レベルを表します。

この値は、次の設定の既定値として使われます

  • core.looseCompression:loose objectを書き込む際の圧縮レベル。
  • pack.compression:pack内のobjectを書き込む際の圧縮レベル。

それぞれを明示していれば、明示した値が優先されます。圧縮関連の設定をすべて省略した場合、loose objectの既定値はレベル1、packの既定値はzlibの既定値です。

クライアントと圧縮の関係

クライアント上では、次のような処理に関係します。

  • git add などで新しいloose objectを書き込む。
  • git gc や git repack でpackを作る。
  • pushで転送するpackを作る。
  • 受信packを展開し、loose objectとして書き直す。

圧縮処理が実行時間の大きな割合を占める場合、圧縮レベルを下げることでCPU時間を減らせる可能性があります。ただ保存サイズや送信量が増える可能性もあります。

設定を変更しても、保存済みのobjectが自動的に再圧縮されるわけではありません。Gitの公式仕様にも、既存objectの圧縮レベルは自動では変わらないと記載されています。

cloneでは受信packの圧縮率を指定しない

cloneの転送用packを用意するのはサーバである。クライアントの core.compression=0 は、サーバが使う圧縮レベルを指定しません。

クライアントが受信packをそのまま保存する経路では、受信したobjectの圧縮レベルも変わりません。objectを読み出す際の解凍は引き続き必要になります。

受信packをloose objectへ展開して保存する経路では、クライアントで再圧縮が発生します。git unpack-objects は、このようにpack内のobjectをloose形式で保存するコマンドです。この保存処理ならローカルの圧縮設定が関係ある可能性があります

したがって、圧縮の設定では次の2つを分けて考える必要があります。

処理クライアントの圧縮設定との関係
サーバがclone用packを用意するクライアント設定では圧縮率を指定しない
クライアントがobjectやpackを新たに書くローカルの圧縮処理に関係ある可能性あり

「クライアントで圧縮を無効にすると、cloneの転送が無圧縮になる」という説明は、設定の作用とは異なります

サーバ側で変更する場合

サーバ側の圧縮設定を変更すると、サーバで新たに圧縮する処理に影響が出ます。CPU処理時間、転送量、クライアントの解凍処理時間を含めて評価する必要があるでしょう。

既存packの再利用も結果に影響します。サーバ側で圧縮を無効にした場合でも、すべての転送objectが無圧縮になるとは限りません。

今回の調査では、GitHubとGitLabのサーバ設定は変更していませんし、そもそも変更できません。

http.postBuffer が変更するもの

HTTP POSTの送信バッファ

http.postBuffer は、Gitのsmart HTTP転送でリモートにPOSTするデータを保持するバッファの閾値です。既定値は1 MiB、つまり1,048,576バイトです。

比較に使った1,073,741,824バイトは1 GiBです。

git -c http.postBuffer=1073741824 clone https://example.com/group/repo.git

cloneでも、取得要求をサーバに送るためにPOSTを使います。POSTの応答として大量のpackデータを受信する場合もあるでしょう。

この2つのサイズは異なります。

クライアント → サーバ:取得要求のPOST本文
サーバ → クライアント:応答に含まれるpackデータ

http.postBuffer の対象はクライアント=>サーバの送信方向です。受信するpackを丸ごと保持するダウンロード用バッファではありません。

バッファを超えたデータも送信できる

HTTP/1.1では、本文の総サイズをあらかじめ確定させずに送る方法としてchunked transfer encodingがあります。本文をチャンクに区切り、1つのHTTPリクエストとして送ります。

Gitの公式仕様では、POSTデータがバッファの上限を超える場合、HTTP/1.1のchunked転送を使用すると説明されています。なお、HTTP/2ではchunked transfer encodingを使用せず、DATAフレームで本文を転送します。

よってチャンクごとにリクエストが必要になるわけではありません。

拡大を検討する条件

Gitの公式FAQは、chunked転送を正しく扱えないサーバや中継プロキシがある場合に、バッファ拡大を検討する説明になっています。通常は既定値を維持する方針です。

大きなpushでは送信するpackが1 MiBを超えやすいでしょう。バッファの上限を超えても転送できないわけではありません。

cloneのリクエストが大きくなる場合

多数のobject IDなどを含む取得リクエストでは、cloneやfetchのPOST本文も大きくなることがあります

受信packが100 MiBや数GiBになることだけでは、POST本文も同じサイズになるとは限りません。今回の測定では、約137 MiBのpackを受信したGitLabへのPOST本文は375バイトと201バイトでした。

大きなリポジトリと大きなpushの違い

pushの送信量は、送信先がまだ持っていないobjectを転送するために必要なpackのサイズで決まります。リポジトリ全体の容量だけでは決まりません。

操作送信量を左右する要因
大きなリポジトリで数行を変更新たに必要なobjectが少なければ、小さなpushになりやすい
全ファイルを修正objectの数やサイズ、差分圧縮の効き方によって変わる
大きなバイナリを差し替え差分圧縮が効きにくいデータでは送信量が増えやすい
空のリモートへの初回push履歴を含む多数のobjectを送る場合がある
多数のコミットをまとめてpushサーバにない履歴とobjectの量によって変わる

今回の実測はcloneだけを対象にしています。push、初回push、大規模な変更の送信は測定していません。

設定と操作の対応表

操作core.compression=0http.postBuffer の拡大
クライアントのcloneローカルで再圧縮が発生するとき。サーバの圧縮率ではない基本関係無し
クライアントのfetchcloneと同様。多数のIDなどを含むリクエストでは関係ある可能性はある
クライアントのpush転送用packを作る圧縮処理。ただ既存データの再利用もある大きなPOSTなら。通常は既定値で送信できる
ローカルのadd・gc・repack新たなobject・packの圧縮にHTTP転送がなければ関係なし
サーバがclone・fetchを処理サーバで読む圧縮設定が関係。クライアント設定は反映されないクライアントからのPOSTを受けるためのバッファ設定ではない
サーバがpushを受信objectを書き直す際の圧縮に関係し得るPOST本文の受信上限を指定する設定ではない

GitHubとGitLabで測定した条件

対象リポジトリ

項目GitHubGitLab
リポジトリpytorch/pytorchgitlab-org/gitlab
固定タグv2.9.0v18.0.0-ee
checkoutしたコミット0fabc3ba448504fd9e5236e
git ls-files -z で数えたエントリ19,41787,073
保存されたpackのサイズ58,668,712バイト、約55.95 MiB143,907,468バイト、約137.24 MiB

git ls-files の件数は、追跡対象のエントリ数である。submoduleのエントリも含み得る。submodule内のファイル数や、再帰的な取得量を表す値ではありません。

packのサイズは、取得後の .git/objects/pack/*.pack を測った値である。TLS、HTTP、Gitプロトコルの付加情報を含む回線上の総転送量ではない。リポジトリの完全履歴の容量や、作業ディレクトリの容量とも異なります。

環境と計測方法

  • OS:macOS。
  • Git:git version 2.50.1 (Apple Git-155)。
  • 通信:公開リポジトリへのHTTPS。トレースでHTTP/2を確認した。
  • 取得方法:--depth=1 --single-branch --no-tags、固定タグ指定。
  • checkout:cloneの既定動作で全体をcheckoutした。partial cloneやsparse checkoutは使っていない。
  • submodule:再帰取得していない。
  • 設定:system設定を無効化し、global設定の読み出し先を /dev/null にした。
  • 比較設定:設定ファイルを変更せず、git -c で渡した。
  • 測定区間:cloneコマンドの起動から終了まで。ネットワーク転送、受信後の処理、checkoutを含む。
  • 時間計測:Pythonの time.perf_counter()。
  • HTTPトレース:GIT_TRACE_CURL と GIT_TRACE_CURL_NO_DATA=1。
  • 回数:4条件をGitHubでは3巡、GitLabでは5巡し、それぞれ15回、25回。毎回新しいディレクトリを利用した。

比較した条件

条件指定した設定
標準2設定とも指定しない
圧縮無効core.compression=0
POSTバッファ拡大http.postBuffer=1073741824
併用上記2設定をすべて指定

GitHubでの結果

単位は秒。各回の値を省略せずに示す。

条件1巡目2巡目3巡目中央値
標準8.0198.3548.2218.221
圧縮無効8.05536.7608.0578.057
POSTバッファ拡大7.9017.9257.7977.901
併用8.1738.2538.2538.253

一貫した改善傾向は確認できなかった。圧縮無効時に36.760秒のものがあったが原因は調べていない。

すべての条件で、次の通信内容が一致した。

  • GET:1回。
  • POST:2回。
  • POST本文の Content-Length:351バイト、201バイト。
  • 保存されたpack:58,668,712バイト。
  • packのSHA-256:全15回で一致。

GitLabでの結果

単位は秒。

条件1巡目2巡目3巡目4巡目5巡目中央値
標準14.96813.92614.19214.92814.86214.862
圧縮無効14.04914.53314.36016.38114.81914.533
POSTバッファ拡大14.43213.68113.95515.09413.45513.955
併用14.70415.42016.58715.63415.14715.420

GitLabに関しても効果が出たとは言い難い。

すべての条件で、次の通信内容が一致した。

  • GET:1回。
  • POST:2回。
  • POST本文の Content-Length:375バイト、201バイト。
  • 保存されたpack:143,907,468バイト。
  • packのSHA-256:全25回で一致。

約137 MiBのpackを受信する際も、送信するPOST本文は既定の1 MiBバッファに収まっていた。

まとめ

クライアント側での設定はサーバ側での設定とはまた異なります。RDBでもこういうことがよくありますね。

ci-perf-lintではこのような設定を検証しながらCIの高速化がコマンド一つでできるようにしています。