Slide 1

Slide 1 text

大きなGitリポジトリをJenkinsで扱うコツ 〜Copy on Writeを使ったディスク節約術〜 前田 薫 品質本部品質管理部SWET第二グループ 株式会社ディー・エヌ・エー © DeNA Co.,Ltd.

Slide 2

Slide 2 text

2 自己紹介 前田 薫 日本にLightning Talksを持ち込み、最初にしゃべった、 というのを自慢にしています。 30年以上T-Codeという漢字直接入力を使っています。 DeNA SWETグループ @mad-p @mad_p

Slide 3

Slide 3 text

3 目次 はじめに リポジトリ肥大化に対する一般的な対応方法 複数のクローンを持つ場合に有効な対応方法 事例紹介: データの性質を分析して対応方法を考える 〜macOSのcopy-on-write活用〜 〜macOSのcopy-on-write活用〜 1 2 3 4 まとめ 5

Slide 4

Slide 4 text

4 1. はじめに

Slide 5

Slide 5 text

5 Gitリポジトリは大きくなる コミットを重ねることで ● 歴史が長くなる ● ファイル数が増加 1 = コミット = ファイル(ブロブ)

Slide 6

Slide 6 text

6 リポジトリが大きくなると問題が多い クローンすると ● 過去のコミット履歴を手元にダウンロード ● .git/ 以下に保存 ● チェックアウトでワーキングツリーを作成 リポジトリが大きくなると ● ダウンロード時間が長くなる ● .gitに必要なディスク容量が大きくなる 2

Slide 7

Slide 7 text

7 2. リポジトリ肥大化に対する 一般的な対応方法

Slide 8

Slide 8 text

8 リポジトリ肥大化に対する一般的な対応方法 DeNA Testing Blogの記事で解説した内容をかんたんにおさらい 大きなGitリポジトリをクローンするときの工夫を図解します https://swet.dena.com/entry/2021/07/12/120000 1

Slide 9

Slide 9 text

9 リポジトリ肥大化に対する一般的な対応方法 大きく2つの対応方法 ● リポジトリが大きくならないように設計する ○ LFSを活用 ● クローン時・チェックアウト時に必要なデータだけを取得する ○ パーシャルクローン ○ シャロークローン ○ スパースチェックアウト 2

Slide 10

Slide 10 text

10 リポジトリが大きくならないように設計する LFSを活用する ● 大きなファイルはGit LFSに保存 ● バイナリファイルがおすすめ ● リポジトリ作成時に設定 するのがよい ○ 歴史ができてからの移行は 不可能ではないが大変 ● 設定方法 ○ git lfs track "*.zip" ○ .gitattributesをコミット 3

Slide 11

Slide 11 text

11 クローン時に必要なデータだけを取得する パーシャルクローン(ブロブレスクローン) ● 作業に必要なブロブだけをダウンロード ● 指定方法 ○ git clone --filter=blob:none \ [email protected]:org/repo 4 = 必要になったら取得するぞという情報

Slide 12

Slide 12 text

12 クローン時に必要なデータだけを取得する シャロークローン ● 最新の歴史だけ取得 ● 指定方法 ○ git clone --depth=1 \ [email protected]:org/repo.git 5 = 取得されない部分

Slide 13

Slide 13 text

13 チェックアウト時に必要なデータだけを取得する スパースチェックアウト ● 一部のフォルダやファイルだけを チェックアウト ● 設定方法 6 git clone --sparse --no-checkout \ [email protected]:org/repo cd repo git sparse-checkout add \ want_folder1 want_folder2 git checkout

Slide 14

Slide 14 text

パーシャルクローンと スパースチェックアウトを両方使う ● 歴史の長さ、ファイル数、両軸で 節約できるのでおすすめ パーシャルクローン、シャロークローン、 スパースチェックアウトは、 クローン後のフォルダに設定が保存される ● ローカルにのみ適用 14 パーシャルクローン + スパースチェックアウト 7

Slide 15

Slide 15 text

15 3. 複数のクローンを持つ場合に 有効な対応方法

Slide 16

Slide 16 text

16 複数クローンを持つ場合とは ● 同一のリポジトリをひとつのマシン上の複数の フォルダにクローンする場合 ● Jenkins agentの場合によく発生 ○ ジョブ別にワークスペースを作成 ○ 次回のジョブ実行に備えて 残しておく ○ 同じリポジトリを使う 複数のジョブが存在 1

Slide 17

Slide 17 text

17 複数のクローンを持つ場合に有効な対応方法 ローカルにダウンロードしたデータを複数のクローンで共有する ● クローン済み.gitをリファレンスとして参照するクローン ● LFSローカルストレージの共有 2

Slide 18

Slide 18 text

18 クローン済み.gitをリファレンスとして参照するクローン クローン済みの.gitを使ってクローン ● 追加で必要なもののみ新規にダウンロード ● Bのgit操作ではAとBの両方のデータを使う ● git clone \ --reference-if-able=/path/to/A \ [email protected]:org/repo ./B ● 後からリファレンスを加える ことも可能(付録参照) 3

Slide 19

Slide 19 text

19 組織的なリファレンスクローンの活用事例 よく使うリポジトリをミラーする ● 定期的にgit fetch ジョブ内でクローンする際 リファレンスとしてミラーを指定 ● サブモジュールでも利用 ● ジョブによって使う リポジトリは異なる 運用が難しく、おすすめはしません 4

Slide 20

Slide 20 text

20 LFSローカルストレージの共有 LFSからダウンロードしたオブジェクトは ローカルストレージに保存される ● ローカルストレージを共有すると ひとつにまとめることが可能 ● 設定方法 ● git config --local \ lfs.storage \ /path/to/shared/storage 5

Slide 21

Slide 21 text

21 4. 事例紹介: データの性質を分析して対応方法を考える 〜macOSのcopy-on-write活用〜

Slide 22

Slide 22 text

22 事例: あるモバイルゲームタイトルのJenkins agent あるモバイルゲームタイトル ● リリースから数年 ● 毎月のイベントごとにアプリ更新とキャラクターの追加 ○ 月次リリースに対応してブランチ管理 ● 巨大リポジトリトップ4(.gitサイズ) ○ アセット(22G)、アセットバンドル(LFSあり16G)、 アセットソース(15G)、アプリ(3.4G) 運用しているJenkins agentマシン群(15台規模) ● mac, win, linux混成agentクラスタ ● macノードの慢性的なディスク容量不足 1

Slide 23

Slide 23 text

23 ここまでで実現できた節約 ここまでの方法で節約できたこと ● リファレンスを活用したクローン ○ .gitの容量/ダウンロード時間の節約 ○ 容量×回数で計820GB → 60GB ○ 元がシャローもあるので節約は実質半分くらい ● LFSローカルストレージの共有化 ○ .git/lfsの容量/ダウンロード時間の節約 ○ 90GB → 20GB 2

Slide 24

Slide 24 text

24 次の問題: チェックアウトしたファイルの容量が大きい ジョブ内で参照するファイル ● Jenkinsでは次回実行に備えディレクトリを残しておく どんなデータが容量を使っているか調査してわかったこと ● アセットリポジトリを扱うジョブが20個くらいある ○ 特にキャラクターのモデル/モーションデータが大きい ○ スパースチェックアウトで減らせない部分が多い ● サウンドデータのリポジトリも大きいが、ジョブは少ない ● マージ時のコンフリクト検出のためにチェックアウトしているものもあり ○ read-onlyの使い方とは限らない → モデル/モーションデータを対象として節約方法を検討 3

Slide 25

Slide 25 text

同じ内容のファイルツリーが 多数保持されている ● 1組が4万個25GBくらい ● これが20組くらい ● macだけでいいから なんとかならないか 25 容量を使っているのはどんなデータか 4

Slide 26

Slide 26 text

26 同一内容のファイルを共有して節約できないか 同一内容をまとめてひとつにしたい ● macOSのAPFSには 同一内容のファイルのディスク 領域だけを共有する手段がある ● copy-on-write動作をするので read-onlyと限らない場合でも有効 ● この図のようになれば理想 5

Slide 27

Slide 27 text

27 データの性質: ファイル追加はあるがファイル更新は少ない モデル/モーションデータは ● 開発が進むにつれて新規キャラ/モーションが追加される ● 既存モデル/モーションの改修は少ない(更新頻度が少ない) ● 月次リリースに対応してブランチ管理されている ● ブランチを切りかえても内容が変わらないファイルが多い ジョブでチェックアウトしているデータは ● ブランチをパラメータとするジョブが多く、ブランチ切りかえで上書きされる ● どのファイルが上書きされるかを予見することは難しい 6

Slide 28

Slide 28 text

28 UNIXに伝統的にあるファイルのリンク 同一内容のファイルをまとめてひとつにするための、古典的なテクニック ● ハードリンク ● シンボリックリンク いずれも、read-onlyのデータで有効に使える手法 利用にあたっては、以下の点に注意する必要あり ● 書きかえたときに、共有されている他のファイルに変更が反映されるか ● Gitにコミットするとどうなるか 7

Slide 29

Slide 29 text

29 UNIXのファイルとディスクブロックの対応 ファイルシステム内では、inode(アイノード)という構造で管理 8

Slide 30

Slide 30 text

30 ハードリンク ひとつのinodeに複数のファイル名をつける ● ln fileA fileB ● 書きかえは両方の名前に 反映される ● Gitには別のファイルとして コミットされる (チェックアウトすると別の実体に) 9

Slide 31

Slide 31 text

31 シンボリックリンク ファイル名をポインタとして使って、別のファイルの中身を参照できる ● ln -s fileA fileB ● 書きかえは両方の名前に 反映される ● Gitにはシンボリックリンク としてコミットされる 10

Slide 32

Slide 32 text

32 macOS APFSのcopy-on-write ファイル名、inodeは別実体、inodeから指すディスク領域のみを共有 ● cpコマンドに-cオプションをつけるとこの方法でコピーされる ○ cp -c fileA fileB ● 書きかえは片方にのみ 反映される ● Gitには別ファイルとして コミットされる ○ チェックアウトすると 別の実体に 11

Slide 33

Slide 33 text

33 macOS APFSのcopy-on-write 部分的に書きかえると、 書き込みが発生した 部分のみコピーして 書きかえる (copy-on-write動作) 今回の用途に適している 12

Slide 34

Slide 34 text

34 copy-on-writeを利用してファイルをまとめる copy-on-writeでディスクの節約ができそうか? ● 同一内容のファイルはgit checkoutで上書きされるか? ○ → 一度まとめれば、ブランチを切りかえてもその状態が維持できるのか? copy-on-writeを使いやすい対象とは? ● 同一ファイルかどうか判定しやすい ○ ディレクトリツリーを対応させて、同じファイル名のものを候補 ○ リファレンス用にミラーしているディレクトリと比較できる 13

Slide 35

Slide 35 text

35 予備実験、効果見積り 予備実験 ● ブランチ間でチェックアウトしてみて、更新のないファイルが上書きされるか調査 ○ ls -liでinode番号とタイムスタンプを表示して調べる ○ 上書きされないことがわかった 効果見積り ● 同一内容のファイルがどの程度あるか調査 ● ツリーをつきあわせて、cmpコマンドでファイル内容を比較 ● 4万ファイル×25組=100万ファイルの97%程度、523GB中516GBを節約できそう ● この調査に10時間かかった。一度に全部やると24時間以上必要になり難しそう copy-on-writeのリスクを検討 (付録参照) 14

Slide 36

Slide 36 text

36 試験運用 日次ジョブを作成してディスク領域を共有 ● 20個のツリーのうち、1日に2つずつ置きかえる ○ 1日に2時間程度の処理時間がかかる ● ミラーのツリーを「お手本」として、ファイル名が同じものの内容を比較 ● 同一かつ未共有なら、cp -cでディスク領域共有したファイルと置きかえる ● 統計情報を出力。dry-runモードも用意 2つのファイル間で、すでにディスク領域共有済かどうかを調べるツール ● https://github.com/dyorgio/apfs-clone-checker (MITライセンス) 15

Slide 37

Slide 37 text

37 試験運用結果 1か月程度運用してみた結果 ● 40%程度のディスク領域を共有できた ○ 96万ファイル中39万ファイル ○ 486GB中201GB ○ (dry-runモードの出力) ● 見積りの半分程度しか効果が出ていないのはなぜだろう? 16

Slide 38

Slide 38 text

38 トラブルシューティング (1/3) 効果が出ない理由として考えられるもの(予想) ● ジョブスクリプトで、予備的に全クリアしている? ● 別ブランチのチェックアウトで大量に書きかえられている? 1分に1回、ls -lUTrを使って、どのファイルが書きかえられたか見張る ● 書きかえた瞬間をつかまえ、ディレクトリ名を特定 ● Jenkinsログのタイムスタンプを手がかりに、 その瞬間に実行されていた処理を特定 17

Slide 39

Slide 39 text

39 トラブルシューティング (2/3) わかったことと対策 ● 「予備的な全クリア」はなかった ● 「ブランチを削除するツール」が悪さをしていた ○ 削除したいブランチをチェックアウトしているとエラーになる。 回避策として「とりあえずmasterをチェックアウト」していた。 ■ masterが古く、大量のファイルを更新することになっていた ○ git checkout --detach HEADを使うことで解決 ● アプリビルド用ジョブで、同梱アセット以外を消していた ○ これらのジョブを処理対象から外すことで解決 18

Slide 40

Slide 40 text

40 トラブルシューティング (3/3) わかったことと対策 ● cp -cでファイルを置きかえた直後にget reset --hardすると、 ファイル内容が同一でも上書きされてしまうことがわかった ○ 一度git statusしておけば、git reset --hardでも上書きされない ○ ファイル置きかえが終わった後、git statusするように変更 その他の工夫 ● ツリーが処理済みであるかを素早く予想する ○ 初期から存在するキャラのファイルだけ共有化済みか調べてみる ○ 未処理のツリーを優先して処理できる 19

Slide 41

Slide 41 text

41 最終結果 ● 88%のディスク領域を 共有できた ○ 86万ファイル中 77万ファイル ○ 464GB中412GB ● あきらめたジョブも あるので分母も減少 20 導入前 試験運用 改善後 macノード4台の空き容量の変化 空き容量 200GB程度 500GB程度

Slide 42

Slide 42 text

42 これが 21

Slide 43

Slide 43 text

43 こうなった! 22

Slide 44

Slide 44 text

44 事例紹介まとめ 容量の大きいデータをmacOSのcopy-on-write機能でディスク領域共有 ● 容量を使っているデータを特定 ● データ性質を調査: 更新が少ない、ブランチ切りかえで部分的に入れかえ ● copy-on-write利用可能性の調査、効果見積り ● 実装 ● 試験運用 ● 効果が出ていない原因をトラブルシューティング、対策を実施 ● 本運用 ● 効果を確認 23

Slide 45

Slide 45 text

45 5. まとめ

Slide 46

Slide 46 text

46 まとめ ● Gitリポジトリが大きくなるとどんな問題があるか解説 ● 大きなリポジトリを扱う場合の工夫を紹介 ○ リポジトリ作成時、クローン時、チェックアウト時 ○ 同一リポジトリを複数クローンしている場合 ● macOSのcopy-on-write機能を利用してディスク容量を節約した事例 調査・予備実験、実装、トラブルシューティング、結果を紹介 Gitリポジトリとデータの特性を理解して、大きくなりがちなリポジトリと うまくつき合っていきましょう

Slide 47

Slide 47 text

© DeNA Co.,Ltd. 47

Slide 48

Slide 48 text

48 付録: 想定質問と答え、補足など

Slide 49

Slide 49 text

49 copy-on-writeにデメリット・リスクはある? duなどのファイル容量調査ツールでは、別のinodeは別のディスク領域を使っていると計算される ● 「duで測定したディレクトリごとのディスク容量の合計」と 「dfで見たディスク容量」が一致しなくなる ● copy-on-write活用によってどれくらいの節約になったか、効果測定が難しい ○ 2つのファイルが共有しているかは、p.36で紹介した apfs-clone-checker で調べられる 領域共有が解除されるような操作によって急にディスク領域が必要になる ● ディスクの残り容量が十分にあると思っていたら、急にディスクフルになってしまう 可能性がなくはない ● バックアップは正常に取れるが、リストアしようとしたらディスクに入り切らないという 可能性もある 1

Slide 50

Slide 50 text

50 git reset --hard で上書きされてしまう理由 ● git statusは内部的にキャッシュを作成 ● ワーキングツリー内のトラックしている各ファイルについて、sha1、 inode番号などを記録 ● git checkoutでは、ワーキングツリー内のファイルとキャッシュが違っている 場合、sha1を計算し直してくれるようだ ○ 一致すれば上書きしない(望ましい動作) ● git reset --hardではinode番号が違うとsha1の再計算をせずに上書きする ○ cp -cで作成したものは内容は同一でもinode番号は変わっている ○ このため、有意をいわさず上書きされてしまうようだ(望まれない動作) 2

Slide 51

Slide 51 text

51 パーシャルクローンを活用しないのはなぜ? 2つの理由があります ● Gitにパーシャルクローンが実装される以前に、リファレンス活用の仕組みを 整えてしまった ○ リファレンスを使っていれば、パーシャルクローンは必要ない ● パーシャルクローンを活用するには、Jenkinsの各ジョブに設定を追加する 必要があり、作成済ジョブを見直して設定追加していくのは手間がかかる 3

Slide 52

Slide 52 text

52 スパースチェックアウトは活用しないのか? 作成済のジョブに対してスパースチェックアウトの設定を順次行っています ● ジョブで必要なファイル集合がすぐわかるジョブから順にとりかかっている ● 分析が大変そうなものはどうしても後回しになってしまう 4

Slide 53

Slide 53 text

53 クローンしたディレクトリに後からリファレンスをつける方法 通常どおりにクローンしたフォルダに対して、後からリファレンスを加えることも できます ● .git/objects/info/alternates というファイルを作成し、リファレンスの .git/objects のフルパスを書き込む ○ サブモジュールの場合は .git/modules//objects/info/alternates ● alternates 作成後、以下のコマンドを実行すると、 リファレンスに存在するオブジェクト(重複分)を削除できる ○ git repack -a -d -l 5

Slide 54

Slide 54 text

54 リファレンスクローンのミラー運用が難しいとは? フォルダBで必要なオブジェクトが A側から消えるとB側でエラーになる ● A側でgcなどで消えないよう gc.pruneExpire=neverを設定 ● たまにAがこわれる場合もある ○ Aを作り直し、Aを見ている B(複数)を作り直し 6 fatal: bad object HEAD fatal: bad object fatal: unable to read

Slide 55

Slide 55 text

55 git lfs dedup copy-on-writeをサポートしている環境では、git lfs dedupが使えます ● ローカルストレージと ワーキングツリー内の ファイルが同一の場合、 cp -cしたもので置きかえてくれる ○ ★印の2つでディスク領域を共有 ● 各ワーキングツリー内で git lfs dedup を実行 ● 手元のマシンでは20GB程度の節約 7

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

57 macOS以外では使えないのか? Linuxのbtrfsには同様の機能があるようです。 https://btrfs.wiki.kernel.org/index.php/Deduplication ZFSにもあるそうですが、ライセンスの問題があってLinuxで使うのは難しそうで す。 WindowsのNTFSはボリュームの単位でしか「以前のバージョン」を保存できないた め、個別のファイル単位でのディスクブロック共有はできなさそうです。 9

Slide 58

Slide 58 text

© DeNA Co.,Ltd. 58