CEDEC2022で 品質本部品質管理部SWET第二グループ 前田 薫 が発表した資料です。 大きなGitリポジトリをクローンするときの通信量・ディスク容量を節約するための、リポジトリの設定やgitコマンドのオプションの工夫を紹介します。また、macOSにあるcopy-on-writeの機能を利用して、同一内容のコピーをまとめることでディスクを400GB以上節約できた事例も紹介します。考えかたや注意点などもあわせて解説します。
大きなGitリポジトリをJenkinsで扱うコツ〜Copy on Writeを使ったディスク節約術〜前田 薫品質本部品質管理部SWET第二グループ株式会社ディー・エヌ・エー© DeNA Co.,Ltd.
View Slide
2自己紹介前田 薫日本にLightning Talksを持ち込み、最初にしゃべった、というのを自慢にしています。30年以上T-Codeという漢字直接入力を使っています。DeNASWETグループ@mad-p@mad_p
3目次はじめにリポジトリ肥大化に対する一般的な対応方法複数のクローンを持つ場合に有効な対応方法事例紹介: データの性質を分析して対応方法を考える〜macOSのcopy-on-write活用〜〜macOSのcopy-on-write活用〜1234まとめ5
41. はじめに
5Gitリポジトリは大きくなるコミットを重ねることで● 歴史が長くなる● ファイル数が増加1= コミット= ファイル(ブロブ)
6リポジトリが大きくなると問題が多いクローンすると● 過去のコミット履歴を手元にダウンロード● .git/ 以下に保存● チェックアウトでワーキングツリーを作成リポジトリが大きくなると● ダウンロード時間が長くなる● .gitに必要なディスク容量が大きくなる2
72. リポジトリ肥大化に対する一般的な対応方法
8リポジトリ肥大化に対する一般的な対応方法DeNA Testing Blogの記事で解説した内容をかんたんにおさらい大きなGitリポジトリをクローンするときの工夫を図解しますhttps://swet.dena.com/entry/2021/07/12/1200001
9リポジトリ肥大化に対する一般的な対応方法大きく2つの対応方法● リポジトリが大きくならないように設計する○ LFSを活用● クローン時・チェックアウト時に必要なデータだけを取得する○ パーシャルクローン○ シャロークローン○ スパースチェックアウト2
10リポジトリが大きくならないように設計するLFSを活用する● 大きなファイルはGit LFSに保存● バイナリファイルがおすすめ● リポジトリ作成時に設定するのがよい○ 歴史ができてからの移行は不可能ではないが大変● 設定方法○ git lfs track "*.zip"○ .gitattributesをコミット3
11クローン時に必要なデータだけを取得するパーシャルクローン(ブロブレスクローン)● 作業に必要なブロブだけをダウンロード● 指定方法○ git clone --filter=blob:none \[email protected]:org/repo4= 必要になったら取得するぞという情報
12クローン時に必要なデータだけを取得するシャロークローン● 最新の歴史だけ取得● 指定方法○ git clone --depth=1 \[email protected]:org/repo.git5= 取得されない部分
13チェックアウト時に必要なデータだけを取得するスパースチェックアウト● 一部のフォルダやファイルだけをチェックアウト● 設定方法6git clone --sparse --no-checkout \[email protected]:org/repocd repogit sparse-checkout add \want_folder1 want_folder2git checkout
パーシャルクローンとスパースチェックアウトを両方使う● 歴史の長さ、ファイル数、両軸で節約できるのでおすすめパーシャルクローン、シャロークローン、スパースチェックアウトは、クローン後のフォルダに設定が保存される● ローカルにのみ適用14パーシャルクローン + スパースチェックアウト7
153. 複数のクローンを持つ場合に有効な対応方法
16複数クローンを持つ場合とは● 同一のリポジトリをひとつのマシン上の複数のフォルダにクローンする場合● Jenkins agentの場合によく発生○ ジョブ別にワークスペースを作成○ 次回のジョブ実行に備えて残しておく○ 同じリポジトリを使う複数のジョブが存在1
17複数のクローンを持つ場合に有効な対応方法ローカルにダウンロードしたデータを複数のクローンで共有する● クローン済み.gitをリファレンスとして参照するクローン● LFSローカルストレージの共有2
18クローン済み.gitをリファレンスとして参照するクローンクローン済みの.gitを使ってクローン● 追加で必要なもののみ新規にダウンロード● Bのgit操作ではAとBの両方のデータを使う● git clone \--reference-if-able=/path/to/A \[email protected]:org/repo ./B● 後からリファレンスを加えることも可能(付録参照)3
19組織的なリファレンスクローンの活用事例よく使うリポジトリをミラーする● 定期的にgit fetchジョブ内でクローンする際リファレンスとしてミラーを指定● サブモジュールでも利用● ジョブによって使うリポジトリは異なる運用が難しく、おすすめはしません4
20LFSローカルストレージの共有LFSからダウンロードしたオブジェクトはローカルストレージに保存される● ローカルストレージを共有するとひとつにまとめることが可能● 設定方法● git config --local \lfs.storage \/path/to/shared/storage5
214. 事例紹介:データの性質を分析して対応方法を考える〜macOSのcopy-on-write活用〜
22事例: あるモバイルゲームタイトルのJenkins agentあるモバイルゲームタイトル● リリースから数年● 毎月のイベントごとにアプリ更新とキャラクターの追加○ 月次リリースに対応してブランチ管理● 巨大リポジトリトップ4(.gitサイズ)○ アセット(22G)、アセットバンドル(LFSあり16G)、アセットソース(15G)、アプリ(3.4G)運用しているJenkins agentマシン群(15台規模)● mac, win, linux混成agentクラスタ● macノードの慢性的なディスク容量不足1
23ここまでで実現できた節約ここまでの方法で節約できたこと● リファレンスを活用したクローン○ .gitの容量/ダウンロード時間の節約○ 容量×回数で計820GB → 60GB○ 元がシャローもあるので節約は実質半分くらい● LFSローカルストレージの共有化○ .git/lfsの容量/ダウンロード時間の節約○ 90GB → 20GB2
24次の問題: チェックアウトしたファイルの容量が大きいジョブ内で参照するファイル● Jenkinsでは次回実行に備えディレクトリを残しておくどんなデータが容量を使っているか調査してわかったこと● アセットリポジトリを扱うジョブが20個くらいある○ 特にキャラクターのモデル/モーションデータが大きい○ スパースチェックアウトで減らせない部分が多い● サウンドデータのリポジトリも大きいが、ジョブは少ない● マージ時のコンフリクト検出のためにチェックアウトしているものもあり○ read-onlyの使い方とは限らない→ モデル/モーションデータを対象として節約方法を検討3
同じ内容のファイルツリーが多数保持されている● 1組が4万個25GBくらい● これが20組くらい● macだけでいいからなんとかならないか25容量を使っているのはどんなデータか4
26同一内容のファイルを共有して節約できないか同一内容をまとめてひとつにしたい● macOSのAPFSには同一内容のファイルのディスク領域だけを共有する手段がある● copy-on-write動作をするのでread-onlyと限らない場合でも有効● この図のようになれば理想5
27データの性質: ファイル追加はあるがファイル更新は少ないモデル/モーションデータは● 開発が進むにつれて新規キャラ/モーションが追加される● 既存モデル/モーションの改修は少ない(更新頻度が少ない)● 月次リリースに対応してブランチ管理されている● ブランチを切りかえても内容が変わらないファイルが多いジョブでチェックアウトしているデータは● ブランチをパラメータとするジョブが多く、ブランチ切りかえで上書きされる● どのファイルが上書きされるかを予見することは難しい6
28UNIXに伝統的にあるファイルのリンク同一内容のファイルをまとめてひとつにするための、古典的なテクニック● ハードリンク● シンボリックリンクいずれも、read-onlyのデータで有効に使える手法利用にあたっては、以下の点に注意する必要あり● 書きかえたときに、共有されている他のファイルに変更が反映されるか● Gitにコミットするとどうなるか7
29UNIXのファイルとディスクブロックの対応ファイルシステム内では、inode(アイノード)という構造で管理8
30ハードリンクひとつのinodeに複数のファイル名をつける● ln fileA fileB● 書きかえは両方の名前に反映される● Gitには別のファイルとしてコミットされる(チェックアウトすると別の実体に)9
31シンボリックリンクファイル名をポインタとして使って、別のファイルの中身を参照できる● ln -s fileA fileB● 書きかえは両方の名前に反映される● Gitにはシンボリックリンクとしてコミットされる10
32macOS APFSのcopy-on-writeファイル名、inodeは別実体、inodeから指すディスク領域のみを共有● cpコマンドに-cオプションをつけるとこの方法でコピーされる○ cp -c fileA fileB● 書きかえは片方にのみ反映される● Gitには別ファイルとしてコミットされる○ チェックアウトすると別の実体に11
33macOS APFSのcopy-on-write部分的に書きかえると、書き込みが発生した部分のみコピーして書きかえる(copy-on-write動作)今回の用途に適している12
34copy-on-writeを利用してファイルをまとめるcopy-on-writeでディスクの節約ができそうか?● 同一内容のファイルはgit checkoutで上書きされるか?○ → 一度まとめれば、ブランチを切りかえてもその状態が維持できるのか?copy-on-writeを使いやすい対象とは?● 同一ファイルかどうか判定しやすい○ ディレクトリツリーを対応させて、同じファイル名のものを候補○ リファレンス用にミラーしているディレクトリと比較できる13
35予備実験、効果見積り予備実験● ブランチ間でチェックアウトしてみて、更新のないファイルが上書きされるか調査○ ls -liでinode番号とタイムスタンプを表示して調べる○ 上書きされないことがわかった効果見積り● 同一内容のファイルがどの程度あるか調査● ツリーをつきあわせて、cmpコマンドでファイル内容を比較● 4万ファイル×25組=100万ファイルの97%程度、523GB中516GBを節約できそう● この調査に10時間かかった。一度に全部やると24時間以上必要になり難しそうcopy-on-writeのリスクを検討 (付録参照)14
36試験運用日次ジョブを作成してディスク領域を共有● 20個のツリーのうち、1日に2つずつ置きかえる○ 1日に2時間程度の処理時間がかかる● ミラーのツリーを「お手本」として、ファイル名が同じものの内容を比較● 同一かつ未共有なら、cp -cでディスク領域共有したファイルと置きかえる● 統計情報を出力。dry-runモードも用意2つのファイル間で、すでにディスク領域共有済かどうかを調べるツール● https://github.com/dyorgio/apfs-clone-checker (MITライセンス)15
37試験運用結果1か月程度運用してみた結果● 40%程度のディスク領域を共有できた○ 96万ファイル中39万ファイル○ 486GB中201GB○ (dry-runモードの出力)● 見積りの半分程度しか効果が出ていないのはなぜだろう?16
38トラブルシューティング (1/3)効果が出ない理由として考えられるもの(予想)● ジョブスクリプトで、予備的に全クリアしている?● 別ブランチのチェックアウトで大量に書きかえられている?1分に1回、ls -lUTrを使って、どのファイルが書きかえられたか見張る● 書きかえた瞬間をつかまえ、ディレクトリ名を特定● Jenkinsログのタイムスタンプを手がかりに、その瞬間に実行されていた処理を特定17
39トラブルシューティング (2/3)わかったことと対策● 「予備的な全クリア」はなかった● 「ブランチを削除するツール」が悪さをしていた○ 削除したいブランチをチェックアウトしているとエラーになる。回避策として「とりあえずmasterをチェックアウト」していた。■ masterが古く、大量のファイルを更新することになっていた○ git checkout --detach HEADを使うことで解決● アプリビルド用ジョブで、同梱アセット以外を消していた○ これらのジョブを処理対象から外すことで解決18
40トラブルシューティング (3/3)わかったことと対策● cp -cでファイルを置きかえた直後にget reset --hardすると、ファイル内容が同一でも上書きされてしまうことがわかった○ 一度git statusしておけば、git reset --hardでも上書きされない○ ファイル置きかえが終わった後、git statusするように変更その他の工夫● ツリーが処理済みであるかを素早く予想する○ 初期から存在するキャラのファイルだけ共有化済みか調べてみる○ 未処理のツリーを優先して処理できる19
41最終結果● 88%のディスク領域を共有できた○ 86万ファイル中77万ファイル○ 464GB中412GB● あきらめたジョブもあるので分母も減少20導入前 試験運用 改善後macノード4台の空き容量の変化空き容量200GB程度500GB程度
42これが21
43こうなった!22
44事例紹介まとめ容量の大きいデータをmacOSのcopy-on-write機能でディスク領域共有● 容量を使っているデータを特定● データ性質を調査: 更新が少ない、ブランチ切りかえで部分的に入れかえ● copy-on-write利用可能性の調査、効果見積り● 実装● 試験運用● 効果が出ていない原因をトラブルシューティング、対策を実施● 本運用● 効果を確認23
455. まとめ
46まとめ● Gitリポジトリが大きくなるとどんな問題があるか解説● 大きなリポジトリを扱う場合の工夫を紹介○ リポジトリ作成時、クローン時、チェックアウト時○ 同一リポジトリを複数クローンしている場合● macOSのcopy-on-write機能を利用してディスク容量を節約した事例調査・予備実験、実装、トラブルシューティング、結果を紹介Gitリポジトリとデータの特性を理解して、大きくなりがちなリポジトリとうまくつき合っていきましょう
© DeNA Co.,Ltd. 47
48付録: 想定質問と答え、補足など
49copy-on-writeにデメリット・リスクはある?duなどのファイル容量調査ツールでは、別のinodeは別のディスク領域を使っていると計算される● 「duで測定したディレクトリごとのディスク容量の合計」と「dfで見たディスク容量」が一致しなくなる● copy-on-write活用によってどれくらいの節約になったか、効果測定が難しい○ 2つのファイルが共有しているかは、p.36で紹介した apfs-clone-checker で調べられる領域共有が解除されるような操作によって急にディスク領域が必要になる● ディスクの残り容量が十分にあると思っていたら、急にディスクフルになってしまう可能性がなくはない● バックアップは正常に取れるが、リストアしようとしたらディスクに入り切らないという可能性もある1
50git reset --hard で上書きされてしまう理由● git statusは内部的にキャッシュを作成● ワーキングツリー内のトラックしている各ファイルについて、sha1、inode番号などを記録● git checkoutでは、ワーキングツリー内のファイルとキャッシュが違っている場合、sha1を計算し直してくれるようだ○ 一致すれば上書きしない(望ましい動作)● git reset --hardではinode番号が違うとsha1の再計算をせずに上書きする○ cp -cで作成したものは内容は同一でもinode番号は変わっている○ このため、有意をいわさず上書きされてしまうようだ(望まれない動作)2
51パーシャルクローンを活用しないのはなぜ?2つの理由があります● Gitにパーシャルクローンが実装される以前に、リファレンス活用の仕組みを整えてしまった○ リファレンスを使っていれば、パーシャルクローンは必要ない● パーシャルクローンを活用するには、Jenkinsの各ジョブに設定を追加する必要があり、作成済ジョブを見直して設定追加していくのは手間がかかる3
52スパースチェックアウトは活用しないのか?作成済のジョブに対してスパースチェックアウトの設定を順次行っています● ジョブで必要なファイル集合がすぐわかるジョブから順にとりかかっている● 分析が大変そうなものはどうしても後回しになってしまう4
53クローンしたディレクトリに後からリファレンスをつける方法通常どおりにクローンしたフォルダに対して、後からリファレンスを加えることもできます● .git/objects/info/alternates というファイルを作成し、リファレンスの.git/objects のフルパスを書き込む○ サブモジュールの場合は.git/modules//objects/info/alternates● alternates 作成後、以下のコマンドを実行すると、リファレンスに存在するオブジェクト(重複分)を削除できる○ git repack -a -d -l5
54リファレンスクローンのミラー運用が難しいとは?フォルダBで必要なオブジェクトがA側から消えるとB側でエラーになる● A側でgcなどで消えないようgc.pruneExpire=neverを設定● たまにAがこわれる場合もある○ Aを作り直し、Aを見ているB(複数)を作り直し6fatal: bad object HEADfatal: bad object fatal: unable to read
55git lfs dedupcopy-on-writeをサポートしている環境では、git lfs dedupが使えます● ローカルストレージとワーキングツリー内のファイルが同一の場合、cp -cしたもので置きかえてくれる○ ★印の2つでディスク領域を共有● 各ワーキングツリー内でgit lfs dedup を実行● 手元のマシンでは20GB程度の節約7
56クローン済みディレクトリ(ミラー)をcp -caしたらよいのでは?特にローカルマシン上ではよい方法です!Jenkinsの場合は以下のような点に注意すれば運用できると思います● すでにクローン済みかどうか判定して、初回だけ処理を変える○ git cloneする代わりにcp -caでミラーから複製する(ディスク共有済みになる)○ その後にgit fetchして最新化するとよい● cp -caした後のgit fetchで追加ダウンロードしたオブジェクトは共有されない○ cf. リファレンスの場合はミラー側でfetchした分も共有される● JenkinsのSCMを利用する場合にはcp -caを利用できないので注意○ Jenkinsfile取得のためのクローン、Jenkinsfile内に記述したcheckoutステップなど○ cf. リファレンスの設定は可能8
57macOS以外では使えないのか?Linuxのbtrfsには同様の機能があるようです。https://btrfs.wiki.kernel.org/index.php/DeduplicationZFSにもあるそうですが、ライセンスの問題があってLinuxで使うのは難しそうです。WindowsのNTFSはボリュームの単位でしか「以前のバージョン」を保存できないため、個別のファイル単位でのディスクブロック共有はできなさそうです。9
© DeNA Co.,Ltd. 58