現在、Gitは最も人気のあるバージョン管理ツールとして広く使用されています。 しかし、Gitはバイナリファイルを管理するのが苦手と言われています。
その理由の一つとして、 Gitはテキストファイルは圧縮して効率的にリポジトリに格納できますが、 バイナリファイルはほとんど圧縮できないため、 バイナリファイルをリポジトリに格納すると、 リポジトリが肥大化しやすいことがあります。
SVNのような中央集権型バージョン管理ツールとは違い、Gitは分散型バージョン管理ツールのため、 Gitではすべてのユーザーがローカルにリポジトリ全体のコピーを持ちます。 そのため、リポジトリが肥大化すると、リポジトリの clone が遅くなることやユーザーのローカルディスクを圧迫などの問題が発生します。
Git LFSを利用することで、このような問題を改善することができると考えて検証を行いましたが、 期待した結果は得られませんでした。
動作確認環境
この記事の内容は GitLab.com で動作確認をしています。 動作確認を行ったバージョンは以下のとおりです。 バージョンによっては、動作や表示に違いが発生する場合があるのでご注意ください。
GitLab.com のバージョンは、ログインしている状態でヘルプページにて確認できます。
- サーバー
- GitLab Enterprise Edition 13.2.0-pre 0651c298bb3
- クライアント
- git 2.27.0
- git-lfs 2.11.0
Git LFSとは
Git LFSについては git lfs
でGoogle検索すると、多くのページが見つかるので詳細は省きますが、簡単にまとめると次のようになります。
簡単にいうと「画像ファイルなどのgitで管理されるべきでないとされるバイナリファイルなどを、まとめてgit上で管理するため機能」です。
出典: Git LFSについて調べてみた
Git LFSの詳細については、出典元の記事がよくまとまっているので参照ください。
私も上記のような認識を持っていたので、大量の画像ファイルで肥大化したリポジトリをGit LFS形式に変換することで、 肥大化したリポジトリの問題を改善できるだろうと考えて検証を行いました。
検証方法
大量の画像ファイルで肥大化したリポジトリのサンプルとして GitLab公式サイト の リポジトリ を使用しました。 このリポジトリを git-lfs-migrate を使用してGit LFS形式に変換しました。 そして、変換前と変換後でリポジトリの容量がどのように変化したかを比較しました。
サンプルリポジトリを clone する
はじめに、GitLab公式サイトのリポジトリを clone します。 その際に --mirror
オプションを付けて、すべての参照(ブランチやタグなどのこと)を取得しています。
$ git clone --mirror git@gitlab.com:gitlab-com/www-gitlab-com.git
続いて、clone したリポジトリの調査を行います。 まずは、リポジトリに移動します。
$ cd www-gitlab-com.git
続いて、次のコマンドでリポジトリの容量を確認します。
$ du -h -d 1 .
出力結果は次の通りでした。
5.2G ./objects
4.0K ./info
56K ./hooks
0B ./refs
5.3G .
リポジトリにコミットしたファイルは objects
ディレクト以下に格納されています。 objects
の中身の詳細は .git/objects
でGoogle検索すると、多くのページが見つかるので詳細は省きますが、興味のある方は こちらのページ を読むのが良いと思います。
続いて、次のコマンドでリポジトリで多くの容量を消費しているファイルを調査します。
$ git lfs migrate info --everything
上記のコマンドはリポジトリのすべての参照の履歴をたどって、容量の大きいファイル形式の上位5位を表示します。
出力結果は次の通りでした。
*.yml 12 GB 42858/42884 files(s) 100%
*.jpg 2.2 GB 7463/7473 files(s) 100%
*.md 2.0 GB 92954/92980 files(s) 100%
*.png 1.9 GB 16167/16192 files(s) 100%
*.gif 468 MB 288/288 files(s) 100%
ファイルサイズは圧縮前の状態での合算ですが、YAMLファイルとマークダウンファイルのサイズの大きさには目を疑いました。
リポジトリをGit LFS形式に変換する
上記の結果から、画像ファイルの *.jpg *.png *.gif
をGit LFS形式に変換することでリポジトリを軽量化できると考えて、 次のコマンドを実行しました。
$ git lfs migrate import --everything --include="*.jpg,*.png,*.gif"
注: このコマンドは このIssue のエラーで終了しました。 このエラーを回避するために、最新の git-lfs/gitojb
を使用して、 git-lfs
を自前でビルドして使用しましたが詳細は省きます。
上記のコマンドは、リポジトリの履歴を書き換えて、画像ファイルをGit LFSのポインターファイルに置換します。 そのため、コマンドを実行すると、リポジトリ内には履歴を辿れなくなった大量のゴミオブジェクトが発生します。
これらのゴミオブジェクトを直ちにクリーンアップするために下記のコマンドを実行しました。
$ git reflog expire --expire=now --all && git gc --prune=now --aggressive
変換前と変換後のリポジトリ容量を比較する
続いて、Git LFSに変換後のリポジトリの容量を確認するために次のコマンドを実行します。
$ du -h -d 1 .
コマンドの出力結果は次の通りでした。
4.8G ./objects
26M ./info
72K ./hooks
0B ./refs
4.2G ./lfs
9.0G .
ここで注目すべきは、変換前は存在しなかった、lfs
というディレクトです。 これは、Git LFSのポインターファイルが指し示すファイルの実体を格納ディレクトリです。
通常、このディレクトリに格納されているファイルは、git checkout で必要になったタイミングでサーバーからクライアントにダウンロードされます。
変換前と変換後の objects
と lfs
ディレクトリの容量を比較すると次のようになります。
ディレクトリ | 変換前 | 変換後 |
---|---|---|
objects | 5.2G | 4.8G |
lfs | - | 4.2G |
変換前と変換後のディレクトリ容量の比較
lfs
内のファイルは git checkout で必要になったタイミングでサーバーからクライアントにダウンロードされるので、 変換後のリポジトリをサーバーから clone した場合に必要なディスク容量は 4.8G 〜 9.0G の間のいずれかの値になると推測されます。
すなわち、リポジトリをGit LFS形式に変換することにより、最良のケースでは 0.4G の容量を節約できますが、最悪のケースでは 3.8G の容量が余分に必要となります。 大量の小さな画像ファイルをGit LFSで管理しても、objects
ディレクトリの削減効果が少ない割に lfs
ディレクトリは肥大化しやすいと推測されます。
例えば、追加した画像ファイルの多くを変更も削除もせずにずっと残しているようなリポジトリの場合では、 最新のブランチを checkout すると最悪のケースに近い状態になります。 (ウェブサイトを Git でバージョン管理している場合は、これに近い状態になりやすいように思います。)
さらに悪いことに、変換したリポジトリを GitLab.com に push した所、12時間以上経過しても push が終わらないため、あきらめて強制終了しました。 これは、Git LFSで管理しているファイルを push した場合、大量の Git LFS Batch API リクエストが発生するためと考えられます。
仮にLFSに変換後のリポジトリの push が完了したとして、そのリポジトリを clone すると同様に大量のHTTPリクエストが発生するため、 LFSに変換前のリポジトリの clone よりも遅くなる可能性が高いと考えられます。
まとめ
サイズが数百キロバイト程度の画像ファイルを Git LFS で管理することは、メリットよりもデメリットの方が大きくなる可能性が高いのでやめたほうがよいでしょう。
特に、画像ファイルが少ないはじめのうちは問題がないようでも、 画像ファイルが増えるにつれて clone や checkout の速度が遅くなるなどの問題が発生する可能性があるので注意が必要です。
反対にGit LFSで管理した方が良いファイルとして、次のようなものが挙げられます。
- 頻繁に更新されるバイナリファイル
- 音楽ファイル、動画ファイル、グラフィックデータなどの巨大なバイナリファイル
Git LFS は用量、用法を守って正しく使いましょう。
カバーイメージの提供: Thomas Bennie on Unsplash