概述

问题

游戏项目中都会有大量的二进制美术资源存储。如果使用 Git,则仓库中会大量存储二进制文件。

Git 仓库过大导致出现了严重的性能问题。

Git 并不推荐管理超过 1GiB 的项目,性能会下降很严重。

Git 默认将文件存储在目录中,但是当数量过多时会将多余文件压缩到单独的 pack 文件中。由于二进制文件本身压缩与解压是没有意义的,只会浪费时间与性能。 现在在拉取时偶尔出现的 git repack 会导致机器 CPU 100% 半小时甚至一小时,极大影响工作。因此需要将二进制文件隔离出去。

Git 迁移到 Git LFS 测试 - 狂飙 并未在项目中实践,而此文是实践之后的总结。

解决方案

Git LFS 是 Git 官方支持的方案,在各个托管服务商中有较好的支持,而且经过发布到现在已三年时间,已变得成熟可用。

  1. 降低压缩包体积大小,移除无需压缩的文件。
  2. 降低首次克隆大小,只需下载当前版本所用二进制文件。

状态

仓库共有 40800+ 提交,30000+ 文件。

  • 当前状态:裸仓库 33.74 GiB。
  • 转换完的状态:裸仓库 11GiB 其中 pack 1.5GiB lfs 9.9GiB

处于压缩状态的大部分是文本文件,而且大小从原来的 33.74 GiB 降到了 1.5 GiB;而二进制文件维持原样保存在 lfs 目录中,也只有 9.9 GiB。

环境

列表

截止至 2018/05/15 以下软件均为最新版本

  • Git 2.17.0
  • Git LFS 2.4.0
  • GitLab CE 10.7.3 (自建)
  • macOS 10.13.4
  • macOS Sourcetree 2.7.3
  • Windows 10
  • Windows Sourcetree 2.5.5.0

说明

Git 及 Git LFS 都使用最新版本,为了获得新版本的优化与更少的 Bug。

Git LFS 提速,通过将每签出一个文件启动一个 Git LFS 进程改为签出所有文件只使用一个 Git LFS 进程。 实测 macOS Linux 提速 80 倍,Windows 上提速 55-65 倍。 包含在 Git 2.11 Git LFS 1.5

Git LFS at Light Speed - Git Merge 2017 - YouTube

GitLab 10.0 可以在推送时检查未提交的 LFS 大文件

Prevent git push when LFS objects are missing (!13837) · Merge Requests · GitLab.org / GitLab Community Edition · GitLab

其次为了支持 Git LFS,Sourcetree 也需要更新到最新版本

  • Windows 下 Sourcetree 需要升级到最新版本 2.5.5.0
  • macOS 下 Sourcetree 需要升级到最新版本 2.7.3

准备工作

服务器

开启 Git LFS 支持,修改 GitLab 服务器配置文件 /etc/gitlab/gitlab.rb,因为二进制文件较大,默认存储位置需要选择磁盘空间充足的盘。

1
2
3
4
5
6
# Change to true to enable lfs
gitlab_rails['lfs_enabled'] = true
# Optionally, change the storage path location. Defaults to
# `#{gitlab_rails['shared_path']}/lfs-objects`. Which evaluates to
# `/var/opt/gitlab/gitlab-rails/shared/lfs-objects` by default.
gitlab_rails['lfs_storage_path'] = "/mnt/storage/lfs-objects"

GitLab Git LFS Administration - GitLab Documentation

客户端

macOS

推荐使用 HomeBrew 安装 Git 与 Git LFS

1
2
3
brew install git
brew install git-lfs
git lfs install

注意:Homebrew 如果较长时间未使用,建议使用官网命令进行重装

Homebrew — The missing package manager for macOS

Windows

直接在官方网站上下载 Git 就可以,自带 Git LFS

Git 官方网站

转换

找到大文件的扩展名

查找所有大于 10KiB 的文件扩展名

fish

1
find Assets -type f -size +10K | grep -o -E "\.[^\.]+\$" | sort | uniq -c | sort -rn

bash

1
find Assets -type f -size +10k | grep -o -E "\.[^\.]+$" | sort | uniq -c | sort -rn

linux - Bash command for finding file extensions with largest size - Super User

由于 Unity 中在 Editor SettingsSerialzation ModeForce Text,所以 Unity 生成的资源都是文本形式保存的,这些可以选择不转换为 LFS。

转换工具

主要有三种工具

git-lfs-migrate 会生成转换完成的仓库,在本文中选择使用这个工具。

GitHub - bozaro/git-lfs-migrate: Simple project for convert old repository for using git-lfs feature

BFG 与 git lfs migrate 都是在原有仓库上操作

rtyley/bfg-repo-cleaner: Removes large or troublesome blobs like git-filter-branch does, but faster. And written in Scala

git lfs migrate

转换前统计

转换前,当前裸仓库 33.74 GiB,40800+ 提交

1
2
3
4
5
6
7
$ git clone --mirror client.git
Cloning into bare repository 'client.git'...
remote: Counting objects: 618522, done.
remote: Compressing objects: 100% (112618/112618), done.
Receiving objects: 100% (618522/618522), 33.74 GiB | 33.66 MiB/s, done.
remote: Total 618522 (delta 505343), reused 618354 (delta 505242)
Resolving deltas: 100% (505343/505343), done.

用时 20m 47s

转换

转换命令,可以多开启线程进行操作,建议 16 线程以上,示例中开启了 64 线程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java -jar git-lfs-migrate.jar \
     -s client.git \
     -d client_converted.git \
     -g git@gitlab.com:example/client_converted.git \
    --write-threads 64 \
    "*.FBX"\
    "*.TGA"\
    "*.a"\
    "*.aar"\
    "*.bmp"\
    "*.bundle"\
    "*.bytes"\
    "*.chm"\
    "*.chw"\
    "*.cubemap"\
    "*.dds"\
    "*.dll"\
    "*.exe"\
    "*.fbx"\
    "*.gif"\
    "*.jpg"\
    "*.mdb"\
    "*.mel"\
    "*.mp3"\
    "*.otf"\
    "*.pdf"\
    "*.png"\
    "*.psd"\
    "*.swatch"\
    "*.tga"\
    "*.tif"\
    "*.ttf"\
    "*.wav"\
    "*.xlsx"\
    "*.zip"

转换用时 4h 19m 10s

GC 统计

对转换后的仓库执行 git gc

1
2
3
4
5
6
7
8
$ git gc
Counting objects: 619464, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (618794/618794), done.
Writing objects: 100% (619464/619464), done.
Total 619464 (delta 485448), reused 0 (delta 0)
Removing duplicate objects: 100% (256/256), done.
Checking connectivity: 619464, done.

用时 19m 44s

  • 压缩前 大小 4.6G 文件数量 619629
  • 压缩后 大小 1.5G 文件数量 7

注意:大小使用 du -sh . 统计,文件数量使用 find . -type f | wc -l 统计

推送

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ git push --mirror git@gitlab.com:example/client_converted.git
Counting objects: 618620, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (133190/133190), done.
Writing objects: 100% (618620/618620), 1.46 GiB | 19.56 MiB/s, done.
Total 618620 (delta 484760), reused 618620 (delta 484760)
remote: Resolving deltas: 100% (484760/484760), done.
remote: Checking connectivity: 618620, done.
To git@gitlab.com:example/client_converted.git
 * [new branch] master -> master

推送用时 3m 48s

转换期间推送的提交

尝试

由于需要尽量减少停机时间,需要将转换期间产生的新提交移动到转换后的仓库中。 尝试使用 cherry-pick 与 am 命令迁移,都出现二进制文件无法识别问题,即 Git 认为在 LFS 的仓库中二进制文件内容与生成的 binary diff 的源不同。 因为 LFS 仓库中只存二进制文件的指针,不存在文件内容了。

1
2
3
4
5
6
git remote add old old.git
git fetch old
git cherry-pick --strategy=recursive --strategy-option=theirs 1fdsa27..old/master

git format-patch --break-rewrites 1fdsa27..HEAD --stdout > transfer.patch
git am --ignore-space-change --ignore-whitespace --whitespace=nowarn --utf8 transfer.patch

可行方法

使用文件比较工具或 rsync 同步两个仓库,然后将变化提交。 需要特别注意旧仓库在拉取更新后一定要保证工作目录是干净的,否则会将错误的内容同步到新仓库中。

结果比较

转换前

  • 整个大小 52G 裸仓库 35G objects 35G
  • GitLab 统计 Storage used: 33.8 GB ( 33.8 GB repository, 0 Bytes build artifacts, 0 Bytes LFS )

转换后

  • 整个大小 24G 裸仓库 11G 其中 objects1.5G lfs 9.9G
  • GitLab 统计 Storage used: 94.2 GB ( 1.5 GB repository, 0 Bytes build artifacts, 92.7 GB LFS )

注意:大小统计使用 du -sh . 命令

检查钩子

提交时检查是否包含过大文件

GitHub - Ninjaccount/git-big-lfs-hook: Git hook to prevent commit if a file is too big and not tracked by lfs.

同时需要增加服务器端钩子检查文件是否被 LFS 管理,不被管理的话拒绝推送

mgit-at/git-max-filesize: A pre-receive hook to enforce usage of git-lfs

迁移

所有人停止提交与推送。

克隆仓库

不再需要使用 git lfs clone 命令了

1
2
3
4
5
WARNING: 'git lfs clone' is deprecated and will not be updated
          with new flags from 'git clone'

'git clone' has been updated in upstream Git to have comparable
speeds to 'git lfs clone'.

可以直接使用 git clone

以下是两次克隆的结果,由于受网速影响,用时差异较大:

1
2
3
4
5
6
7
8
9
$ git clone git@gitlab.com:example/client_converted.git
Cloning into 'client_converted'...
remote: Counting objects: 618085, done.
remote: Compressing objects: 100% (133260/133260), done.
Receiving objects: 100% (618085/618085), 1.46 GiB | 48.10 MiB/s, done.
remote: Total 618085 (delta 484307), reused 617897 (delta 484155)
Resolving deltas: 100% (484307/484307), done.
Checking out files: 100% (30239/30239), done.
Filtering content: 100% (4917/4917), 9.80 GiB | 21.18 MiB/s, done.

用时 8m 55s

1
2
3
4
5
6
7
8
9
$ git clone git@gitlab.com:example/client_converted.git
Cloning into 'client_converted'...
remote: Counting objects: 618929, done.
remote: Compressing objects: 100% (133427/133427), done.
remote: Total 618929 (delta 484984), reused 618741 (delta 484832)
Receiving objects: 100% (618929/618929), 1.46 GiB | 51.48 MiB/s, done.
Resolving deltas: 100% (484984/484984), done.
Checking out files: 100% (30345/30345), done.
Filtering content: 100% (4932/4932), 9.86 GiB | 60.66 MiB/s, done.

用时 3m 44s

开启 Git LFS 支持

仓库正式从 Git 迁移到 Git LFS,需要重新克隆仓库,并初始化设置。

克隆的仓库需要启用 Git LFS

1
"C:\Program Files\Git\bin\git.exe" lfs install --force

使用 Sourcetree 首次打开仓库时会提示开启 Git LFS 支持并需要下载 Git LFS 模块,点击确定即可。

QA

如遇到 Sourcetree 打开其他仓库崩溃问题,可尝试重启电脑后卸载,再次重启后安装。

GitLab 中新仓库需要导入以前仓库的成员权限信息,直接在权限界面点击 Import 即可。

如果本地曾经使用 Git 配置过代理(做 Go 语言开发的时候通常会设置 Git 的代理以便拉取第三方依赖),需要去 ~/.gitconfig 中删除对应的代理,否则在拉取 LFS objects 会一直卡住(此问题遇到后查了一下午才解决)。

注意事项

GitLab 对 LFS object 的管理还没到位,虽然文档中说明已在 8.14 中加入清理功能,但截止至 2018/05/15 功能并未完成。

Prune unreferenced Git LFS objects (#30639) · Issues · GitLab.org / GitLab Community Edition · GitLab

Git LFS 默认使用 smudge clean 过滤器与 pre-push 本地钩子,如果使用了 pre-push 钩子,需要修改钩子调用 Git LFS 钩子。以下文章提到了此问题并给出了解决方案。

化繁为简的企业级 Git 管理实战(五):二进制大文件的版本控制 | HaHack

Download Zip 功能无法下载到存储在 Git LFS 中的对象

“Download ZIP” archiver does not resolve Git-LFS references (#14261) · Issues · GitLab.org / GitLab Community Edition · GitLab

游戏项目相关实践

关于在转换过程中遇到的问题以及对应的解决方案

Using Unity & Git-LFS – Hack and Paint

35 GiB 大小,包含 18000+ 次提交的仓库转换过程,公司的产品 War for the Overworld - Buy now!

Migrating your project to Git LFS

参考资料

介绍了不同平台下的注意事项

Git LFS 操作指南

文章中提到重新checkout的时候有概率发生lfs 404,但实际测试中未发现此问题,需要更多测试。

奇怪的Git实践 | Loading & Learning

此教程讲得很细,将每一步及其对应的输出都写了;而且 Sourcetree 的创始人也是 Git LFS 的主要贡献者。

Git LFS - large file storage | Atlassian Git Tutorial

大文件管理相关探索

How to manage big Git repositories

Handling Large Files with LFS