atama plus techblog
🐣

Cloneに90分かかる巨大リポジトリの軽量化を検討した話

に公開

こんにちは、atama plusでエンジニアをしているsugiです。

先日、32GBにまで肥大化した巨大Gitリポジトリの軽量化を検討する機会がありました。

検討の結果、「技術的には1GBまで削減できるが、運用上の問題を考慮して軽量化は行わない」という判断に至りました。

この記事では、なぜそのような判断に至ったのか、その経緯を共有します!

現状:Cloneに90分かかるリポジトリ

私が所属するチームでは、歴史的経緯により肥大化した巨大なリポジトリを抱えていました。

  • Git履歴(.gitフォルダ)約32GB
  • LFS実体込みのサイズ200GB超

このリポジトリを新規参画メンバーがgit cloneしようとするとLFSファイル抜きで約90分、LFSファイル込みだと4時間以上かかります。回線状況によってはタイムアウトで失敗することもあり、「環境構築だけで1日が終わる」という状況が発生していました。

対象のリポジトリについて

まずは前提として、弊社のプロダクト構造について少し説明させてください。

弊社(atama plus)では、学習塾向けに学習アプリを提供しています。
このアプリを通じて生徒一人ひとりに問題や講義動画を届けているのですが、それらの「教材コンテンツ(問題、解説、動画などのデータ)」の定義を一元管理しているのが、今回対象となるリポジトリです。

つまり、このリポジトリにはプロダクトコードではなく「アプリ上で表示されるコンテンツデータそのもの」が格納されているということになります。

リポジトリ肥大化の原因

通常、弊社のプロダクトコード(サーバーサイドやアプリ)を管理するリポジトリは、大きくても1~2GB程度です。 しかし、このリポジトリだけはそれらの30倍ものサイズに膨れ上がっていました。その理由について具体的に説明します。

弊社の教材コンテンツはJSONを人間が読み書きしやすく拡張したデータ形式であるHJSONという形式で定義されており、リポジトリ内のファイルの多くはこのHJSON形式になっています。

通常、テキストファイルであるHJSONが主体であれば、ここまでリポジトリが肥大化することはありません。
しかし実際に.git フォルダの内訳を見ると、このHJSONファイルが大部分(29GB/32GB)を占めています。

その原因は、以下のような形式で数MBサイズの画像データが文字列としてHJSON内に直接埋め込まれる仕様 になっていることです。

HJSONファイルの構造イメージ

lecture: {
    images: { 
        // ▼ 数MBサイズの画像データがBase64文字列として直接埋め込まれている
        data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
    }
        
}

このリポジトリでは、動画や音声ファイルはGit LFSで管理されています。
しかし画像データについては、コンテンツ制作ツールの仕様や運用効率を最優先し、LFS化せずにテキストデータとしてHJSON内に埋め込む設計が採用されていました。

これは、Git操作に不慣れなコンテンツ制作チーム(非エンジニア)が、複雑なファイル管理を意識せずスムーズに教材を作成できるようにするための設計でした。

その結果として、日々の業務で発生する数MB単位のテキスト差分が積み重なり、数年の運用を経てリポジトリ全体が肥大化することとなってしまいました。

リポジトリ構成のイメージ

content/
└── 数学/
    └── 数学1/
        ├── 因数分解.hjson      # 教材コンテンツ ← 画像データが埋め込まれていてサイズが大きい
        └── assets/
            └── lecture.mp4   # 動画ファイル(Git LFSで管理)

この状況を改善するため、「リポジトリ軽量化」の検討が立ち上がりました。

ゴールはシンプルです。
.gitフォルダを軽量化し、git cloneにかかる時間を短縮する」 ことを目指します。

検証:リポジトリの軽量化

HJSONファイル内の画像データ文字列がリポジトリ肥大化の原因であることを踏まえて、以下の2ステップで軽量化を試みました。

  • ステップ1:HJSON内の画像データ文字列をPNG画像化してLFS管理する
  • ステップ2:過去の履歴を削除する

ここからは各ステップごとに具体的に説明します。

ステップ1:HJSON内の画像データ文字列をPNG画像化してLFS管理する

まずは「現在のファイル」をスリム化するため、スクリプトを用いて以下の処理を行いました。

  1. HJSON内の画像データ文字列を抽出
  2. デコードして画像ファイル(.png)として書き出し
  3. 書き出された画像をGit LFSの管理下に置く

実際に本番環境の対応をする場合には単純な画像データの置き換えだけでなく他にも様々な修正が必要ですが、今回は検証のため簡易的な処理で近い状況を作っています。

この処理により、HJSONファイル自体のサイズはかなり小さくなりました。
しかし、リポジトリ全体のサイズは32GB → 20GBへの縮小にとどまりました。
これは、Gitが過去の履歴すべてを保存するシステムであるため、「過去のコミットに含まれる巨大な画像データ文字列」は.gitフォルダの中に依然として残り続けているからです。

やはり、本丸である「過去の履歴」そのものを消し去る必要がありました。

ステップ2:過去の履歴を削除する

ステップ1で「現在のファイル」はスリム化できたのすが、さらなる軽量化には過去の履歴を削除することが必要です。ステップ2ではツールを使って履歴の削除を進めていきます。

1.履歴削除ツールの選定

Git履歴を削除するツールとしてはGit組み込みのgit filter-branchや、Java環境で動作するBFG Repo-Cleaner、Python環境で動作するgit-filter-repoなどが候補にありました。

その中でも、GitHub公式ドキュメントでも推奨されていることや使用方法のわかりやすさなどからgit-filter-repoを採用することに決めました。

2.「必要な履歴」を守るための工夫

git-filter-repo は非常に強力ですが、単純に「サイズが大きいファイルを削除」という指定をすると、「現在(HEADで)使われているファイル」ごと消し去ってしまう恐れがあります。これでは現在のビルドが壊れてしまいます。

そこで、「現在使われているファイルは保護し、過去の不要な履歴だけを削除する」ために、以下の手順で削除対象リストを作成しました。

  • A. 全オブジェクトをリストアップ:リポジトリの全期間に存在するHJSONファイルのオブジェクトID(ハッシュ値)をすべて抽出します。

  • B. 最新コミット(HEAD)のオブジェクトをリストアップ:現在チェックアウトされている最新のHJSONファイルのオブジェクトIDを抽出します。

  • C. 差分を抽出:「A(全履歴)」から「B(最新)」を引き算し、「今はもう使われていない過去バージョンのファイルID」だけをリスト化したdelete_hjson_objects_id.txtを作成しました。

3.履歴削除の実行

作成したリストをgit-filter-repoに渡して、以下のコマンドを実行して不要な履歴だけを削除しました。

# 作成したIDリストを渡して削除を実行
git-filter-repo --strip-blobs-with-ids delete_hjson_objects_id.txt --force

この履歴削除によって、リポジトリサイズは20GB → 1GBに大きく削減され、
git cloneにかかる時間もLFS抜きの場合で90分 → 2分に大幅に短縮することに成功しました。

これで技術的には「リポジトリを軽量化する」という目的を達成できることが実証されました。

課題:GitHub上の開発資産を喪失するリスク

ここまでで技術的な目処は立ちましたが、運用面を詳細に調査した結果、GitHubの仕組みに起因する重大な副作用があることが判明しました。

履歴削除による副作用

Gitの履歴を削除すると、GitHub上のPull Requestのレビューコメントや変更差分などの重要な開発資産が参照できなくなる可能性があるということがわかりました。
なぜそのようなことが起きるのか、それはGitの「コミットハッシュ」の仕組みに原因があります。

Gitのコミットハッシュはそのコミットの内容だけでなく、親コミットのハッシュを含めて計算されます。
そのため過去のデータを少しでも変更すると、それ以降の全てのコミットハッシュが連鎖的に新しい値に書き換わります。

GitHubの公式ドキュメントgit-filter-repoのマニュアルによるとGitHub等のホスティングサービスにおいて、Pull Requestのレビューコメントや議論のログは、特定のコミットハッシュに紐付いており、ハッシュが変更されると前述のPull Requestのレビューコメントや変更差分などが参照できなくなるといった問題が発生するリスクがあるようでした。

2つの実施プランの検討

この副作用を踏まえ、リポジトリ軽量化の実施プランを2つ考えました。

プラン1:新リポジトリへの移行

  • 現在のリポジトリは「アーカイブ」として残し、軽量化したデータを元に「新しいリポジトリ」を立ち上げる案です。
    • メリット:現環境を触らないため安全で、過去の資産も旧リポジトリに残せます。git-filter-repoマニュアルでも基本的には新リポジトリへの移行が推奨されています。
    • デメリット:リポジトリURLが変わるため、CI/CDや連携ツールの再設定が必要になり、試算するとかなりの工数がかかりそうでした。

プラン2:現リポジトリの改修

  • 現在のリポジトリで履歴削除を行い、そのまま使用する案です。
    • メリット:リポジトリが変わらないため、設定の一部を流用できます。
    • デメリット:全開発者に環境の再構築を強いることになります。そして何より、前述の通りGitHub上の過去の開発資産が失われるリスクについては、私の調査範囲では確実な回避策が見つかりませんでした。

プラン1で「インフラ再構築の工数」を取るか、プラン2で「過去の開発資産を失うリスク」をとるか。どちらも大きな痛みを伴います。

結論:「なにもしない」という選択

最終的に、この検証では今回のリポジトリ軽量化は「実施しない」という判断をしました。

判断理由:投資対効果が合わない

「新規参画時のgit clone時間」を短縮するためだけに支払うコスト(数ヶ月規模の移行工数、または開発資産を失うリスク)が大きすぎると判断しました。頻度の低い初期環境構築のために、チーム全体の継続的な開発効率や過去資産を犠牲にはできません。

代替案:Partial Clone

また、過去の履歴を削除しなくてもGitの標準機能で「環境構築の高速化」は実現可能であるという代替案の存在に気づいたというのも大きいです。

  • Partial Clone:ファイルの実体をダウンロードせず、メタデータのみを先に取得する。
  • Sparse Checkout:作業に必要なフォルダだけをローカルに展開する。

これらを組み合わせることで、32GBのリポジトリであっても必要なデータのみをオンデマンドで取得する運用が可能となり、clone含めた初期環境構築が数分で完了することが確認できました。

まとめ

正直なところ、私は「肥大化したリポジトリ=悪」という思い込みから、「リポジトリをきれいにすること」自体を目的にして調査を進めてしまっていました。

しかし、本来の目的は「開発効率を上げること」です。
大きなコストをかけてリポジトリを掃除するよりも、現状のままPartial Clone等の運用でカバーする方がチーム全体の利益になると判断しました。

今回の検証で技術的に「できる」ことでも「やるべき」とは限らないという学びを得ることができました。
この記録が同じように巨大リポジトリに苦しむ誰かの参考になれば幸いです。

atama plus techblog
atama plus techblog

Discussion