Upgrade to Pro — share decks privately, control downloads, hide ads and more …

大きなGitリポジトリをJenkinsで扱うコツ 〜Copy on Writeを使ったディスク節約術〜

DeNA_Tech
September 21, 2022

大きなGitリポジトリをJenkinsで扱うコツ 〜Copy on Writeを使ったディスク節約術〜

CEDEC2022で 品質本部品質管理部SWET第二グループ 前田 薫 が発表した資料です。
大きなGitリポジトリをクローンするときの通信量・ディスク容量を節約するための、リポジトリの設定やgitコマンドのオプションの工夫を紹介します。また、macOSにあるcopy-on-writeの機能を利用して、同一内容のコピーをまとめることでディスクを400GB以上節約できた事例も紹介します。考えかたや注意点などもあわせて解説します。

DeNA_Tech

September 21, 2022
Tweet

More Decks by DeNA_Tech

Other Decks in Technology

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

  4. 4
    1. はじめに

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  42. 42
    これが
    21

    View Slide

  43. 43
    こうなった!
    22

    View Slide

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

    View Slide

  45. 45
    5. まとめ

    View Slide

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

    View Slide

  47. © DeNA Co.,Ltd. 47

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  54. 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

    View Slide

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

    View Slide

  56. 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

    View Slide

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

    View Slide

  58. © DeNA Co.,Ltd. 58

    View Slide