Git LFS如何比Git更高效地追踪和存储二进制数据?

3
我知道git LFS会导致git在文本文件中存储一个“指针”字符串,然后git LFS会下载目标二进制文件。通过这种方式,远程git服务器上的git仓库变得更小。但是,git LFS仍然需要存储二进制文件,所以在本地存储(在执行git lfs pull之后)似乎没有什么不同,远程git LFS服务器数据加上远程git数据的总和仍然会相似。
我漏掉了什么?git LFS如何高效地跟踪二进制文件?

更新(在撰写这个问题后的额外学习之后):不要使用git lfs。我现在建议不要使用git lfs

另请参阅:

  1. 我的评论如下我接受的答案
  2. 我刚刚添加的自己的答案

我开始提出这个问题是因为我认为Git LFS很棒,很神奇,我想知道“怎么做”。然而,最终我意识到Git LFS是我日常工作流程问题的根源,我不应该再使用它,也不再推荐它。

总结:

正如我在这里所述

对于个人免费的 GitHub 帐户来说,它的限制太多了,而对于付费的企业帐户来说,git checkout 的时间从几秒变成了长达3个小时+,尤其对于远程工作者来说,这完全是浪费他们的时间。我经历了三年的痛苦。我写了一个脚本,在每晚执行一次git lfs fetch来缓解这个问题,但我的雇主拒绝为我购买更大的 SSD 以提供足够的空间来执行git lfs fetch --all,所以我仍然经常遇到多小时检出的问题。此外,要撤销 git lfs 集成到你的存储库中是不可能的,除非你删掉整个 GitHub 存储库并从头开始重新创建。
我刚刚发现git lfs的免费版本有如此严格的限制,以至于毫无用处,我正在删除我所有公开的免费仓库中的它。请参阅此答案(GitHub.com的仓库大小限制)并搜索"git lfs"部分。
在我看来,git lfs唯一的好处似乎是在克隆仓库时避免一次性下载大量数据。就是这样!对于内容总大小(git仓库+可能的git lfs仓库)小于约2 TB的任何仓库来说,这似乎是一个相当微不足道甚至无用的好处。使用git lfs所做的全部工作只是
1. 让我的git checkout变得非常慢(字面上是几个小时)(糟糕)。 2. 使我通常快速且离线的git命令,如git checkout,现在变成了在线且缓慢的git命令(糟糕)。 3. 还要作为另一个需要付费的GitHub服务(糟糕)。
如果你想使用git lfs来克服GitHub的100 MB文件大小限制,就像我一样,请不要这样做!你会很快就用完git lfs的空间,特别是如果有人克隆或分叉你的仓库,因为那会计入你的限制,而不是他们的限制!相反,可以使用"类似tar加split工具,或者只用split工具"(来源),将大文件分割成较小的部分,比如每个部分90 MB,然后将这些二进制文件块提交到你的常规git仓库中。
最后,GitHub上关于停止使用git lfs并完全释放空间的“解决方案”简直是疯狂得不可思议!你必须删除整个仓库!请参考这里的问答:如何删除由git-lfs跟踪的文件并释放存储配额? GitHub的官方文档也证实了这一点(重点已添加):
引用: 从Git LFS中删除文件后,Git LFS对象仍然存在于远程存储,并将继续计入您的Git LFS存储配额。 要从存储库中删除Git LFS对象,请删除并重新创建存储库。删除存储库时,还会删除任何相关的问题、星标和分叉。
我简直无法相信这被认为是一个“解决方案”。我真的希望他们正在努力寻找更好的修复方法。
对于考虑使用git lfs的雇主和公司,我有一个建议:

快速总结:不要使用 git lfs。相反,为您的员工购买更大的SSD。 如果 您最终决定使用 git lfs,还是要给您的员工买更大的SSD,这样他们可以在睡觉时运行一个脚本来执行 git lfs fetch --all ,每晚执行一次。

详细信息:

假设你是一家科技公司,拥有一个庞大的单一代码库,大小为50 GB,并且有4 TB的二进制文件和数据希望作为代码库的一部分。与其给他们不足的500 GB ~ 2 TB固态硬盘,然后再使用git lfs,这样在家庭互联网连接上进行git checkout时会花费数秒到数小时的时间,不如给你的员工更大的固态硬盘!一个典型的科技员工每天的成本超过1000美元(每周5个工作日 x 每年48个工作周 x 每天1000美元 = 240,000美元),这比他们的薪水+福利+间接成本还要少。因此,如果能节省他们几个小时的等待和麻烦,一块价值1000美元的8 TB固态硬盘完全值得购买!购买示例:
  1. 8TB Sabrent Rocket M.2 SSD,1100美元
  2. 8TB Inland M.2 SSD,900美元

现在他们希望有足够的空间来运行自动化的夜间脚本 git lfs fetch --all,以获取所有远程分支的LFS内容,以帮助缓解(但不能解决)这个问题,或者至少使用 git lfs fetch origin branch1 branch2 branch3 来获取他们最常用分支的哈希内容。

另请参阅

非常有见地的问答,也倾向于使用git lfs [即使是对于远程仓库]: 我需要在本地仓库中使用Git LFS吗? Git LFS有什么优势? 我的问答: 如何在git checkout失败后恢复git lfs post-checkout钩子? 我的回答: 如何缩小你的git存储库中的.git文件夹 我的问答: git lfs fetchgit lfs fetch --allgit lfs pull之间有什么区别?

2
请编辑您的回答,明确指出您只是在提到GitHub对git lfs的实现,而不是一般的git lfs。自己托管GitLab实例可能是解决这个问题的一个优雅的方案。 - Lavi Arzi
2
请编辑您的回答,以明确您只是指GitHub对git lfs的实现,而不是一般的git lfs。托管自己的GitLab实例可能是解决这个问题的一个优雅的解决方案。 - undefined
@LaviArzi,不,这甚至在正常的工作流程中也是一个问题。我在一个拥有1200名开发人员的组织中使用了3年的git lfs,这是一个大约200GB的单一仓库,其中有100GB存储在git lfs中。每一周,甚至每一天,只是执行git fetchgit checkout main,或者git checkout my_branch_from_yesterday等操作,光是进行checkout就需要长达3个小时,因为git lfs会在你执行git checkout时添加钩子来拉取git lfs数据。这是因为AI感知团队中的某个人会将大量相机数据或其他内容添加到git lfs中,而我的checkout操作会下载这些数据。 - Gabriel Staples
我宁愿拥有一个4TB的SSD,其中包含一个2TB的本地仓库,每晚进行更新,并且可以在30秒内完成git checkout操作,而不是只被分配了1TB的SSD,其中包含一个200GB的仓库和700GB的构建数据,每天需要花费3个小时来切换分支查看某些内容(通过通常无害的git checkout命令)。 - Gabriel Staples
1
对我来说,git lfs 的唯一好处似乎是在克隆存储库时避免一次性下载大量数据。更为严重的问题是,如果二进制文件经常发生变化,git lfs 可以避免存储库的大小增长过多。如果你只添加一些 blob 并且从未更改过,那么这并不是什么大问题。积累这些文件的变更历史才会引起大问题。 - dividebyzero
显示剩余2条评论
2个回答

11
当你克隆 Git 存储库时,你必须下载其整个历史记录的压缩副本。每个文件的每个版本都可以访问。
使用 Git LFS,文件数据不存储在存储库中,因此当你克隆存储库时,它不必下载存储在 LFS 中的文件的完整历史记录。仅会从 LFS 服务器下载每个 LFS 文件的“当前”版本。从技术上讲,LFS 文件是在“checkout”期间下载的,而不是在“clone”期间。
因此,Git LFS 与其说是关于高效地存储大型文件,不如说是避免下载选定文件的不必要版本。那个历史记录通常也不是很有趣,如果你需要一个旧版本,Git 可以连接到 LFS 服务器并获取它。这与普通的 Git 相反,后者可让你脱机检出任何提交。

5
请注意,使用现代的git(服务器和客户端都必须支持)时,第一句话不再正确。您可以通过使用无blob克隆来获得类似于使用LFS的效果:您将获得一个完全功能的仓库,其大小比完整的仓库小,并且会按需下载缺失的内容。 - Joachim Sauer
@JoachimSauer:好观点。对于希望选择按需下载哪些文件、哪些文件存储在专用 LFS 服务器上而不是 Git 服务器上的人来说,LFS 仍然可能具有优势。 - John Zwinck
哦,是的,在选择 LFS 方面可能仍然有理由,但这不再是唯一的选择。 - Joachim Sauer
我刚刚发现git lfs的免费版本有如此严格的限制,以至于它是无用的,我现在正在删除所有我的公共免费存储库中的它。请参见此答案(GitHub.com的存储库大小限制),并搜索“git lfs”部分。 - Gabriel Staples
John,看起来git lfs的唯一好处就是避免一次性下载大量数据,对于任何总内容大小(git repo + would-be git lfs repo)小于200 GB的repo来说,这似乎是一个相当微不足道甚至无用的好处。使用git lfs所做的只有1)使git checkout变得非常缓慢(字面上需要数小时)(不好),2)使我通常快速离线的git命令,如git checkout现在变成在线和缓慢的git命令(不好),以及3)作为另一个需要付费的GitHub服务(不好)。 - Gabriel Staples
4
我很高兴你在这里记录了那些限制,但我认为我们应该明确它们是 GitHub 上的 Git LFS 的限制,而不一定是 Git LFS 总体上的限制。我从未见过有人在 GitHub 的免费账户上使用 Git LFS,也许这就是原因。 - John Zwinck

3
如何使git LFS比git更高效地跟踪和存储二进制数据?
总结: 它并不是更高效地跟踪大型二进制文件。实际上,它只是在一个单独的服务器上远程执行此操作,以释放一些本地存储空间,并使初始的git clone过程下载更少的数据。以下是概要:
使用Git LFS时,文件数据不存储在存储库中,因此当您克隆存储库时,不需要下载存储在LFS中的文件的完整历史记录。只有每个LFS文件的“当前”版本从LFS服务器下载。从技术上讲,LFS文件在“checkout”期间下载,而不是在“clone”期间。
[链接1:@John Zwinck] [链接2:@Schwern]
它可以大幅减少对存储库的初始git克隆的大小。 它可以大幅减少本地存储库的大小。 @Mark Bramnik: 这个想法是,二进制文件是从“远程”存储库中懒加载下载的,即在检出过程中而不是在克隆或获取过程中。
细节
常规Git存储库
想象一下,你有一个庞大的单一存储库,其中包含约100 GB的文本文件(代码,包括所有git blob和更改),以及100 GB的二进制数据。请注意,这是一个我实际上处理了几年的现实、代表性的例子。如果100 GB的二进制数据已经提交过一次,它将占用100 GB,而您的总git存储库将是100 GB的代码blob + 100 GB的二进制数据提交一次 = 200 GB。
如果每个文件的100 GB二进制数据已经更改了10次,那么它占用了大约100 GB x (1 + 10) = 1.1 TB的空间,再加上100 GB的代码,总共是1.2 TB的仓库大小。现在,克隆这个仓库:
# this downloads 1.2 TB of data
git clone git@github.com:MyUsername/MyRepo.github.io.git

如果你想执行一个 `git checkout`,不过这个操作很快!所有的二进制数据都存储在本地仓库中,因为你拥有二进制数据的11个快照(初始文件 + 10次更改)!
# this downloads 0 bytes of data;
# takes **seconds**; you already have the binary data locally, so no new data is
# downloaded from the remote server
git checkout some_past_commit

# this takes seconds and downloads 0 bytes of new data as well
git checkout another_past_commit

与之相比,看看git lfs
一个使用Git LFS存储所有二进制文件的Git仓库
你拥有与上述相同的仓库,只是100GB的代码在git仓库中。Git LFS使得git只存储指向LFS服务器的指针文本文件,因此git仓库中只有100GB的代码+少量用于指针文件的存储空间。
另一方面,Git LFS服务器包含了全部1.1TB的二进制文件。所以,你会得到这样的效果:
# this downloads 0.1 TB (100 GB) of code/text data
git clone git@github.com:MyUsername/my_repo.github.io.git
# this downloads 0.1 TB (100 GB) of binary data--just the most-recent snapshot
# of all 100 GB of binary data on Git LFS
cd my_repo
git lfs pull

# this downloads potentially up to another 0.1 TB (100 GB) of data;
# takes **hours**; you do NOT have the binary data for all snapshots stored
# locally, so at **checkout** Git LFS causes your system to download all new
# LFS data!
git checkout some_past_commit

# this downloads up to another 0.1 TB (100 GB) of data, taking **more hours**
git checkout another_past_commit

实际上,普通的Git比Git LFS更有效地存储二进制文件

请参考这里Alexander Gogl在回答中的表格。添加一个28.8 MB的Vectorworks (.vwx)文件作为git blob和Git LFS blob都占用26.5 MB的空间。但是,如果将其作为git blob存储,然后运行git gc执行"垃圾回收"和blob压缩,普通的git会将其缩小到1.8 MB。而Git LFS对其不做任何处理。请在这个表格中查看其他示例。

如果你看一下这个表格,你会发现git整体上比Git LFS存储得更高效:

类型 | 变化 | 文件 | 作为git blob | git gc后 | 作为git-lfs blob ---|---|---|---|---|--- Vectorworks (.vwx) | 添加几何图形 | 28.8 MB | +26.5 MB | +1.8 MB | +26.5 MB GeoPackage (.gpkg) | 添加几何图形 | 16.9 MB | +3.7 MB | +3.5 MB | +16.9 MB Affinity Photo (.afphoto) | 切换图层 | 85.8 MB | +85.6 MB | +0.8 MB | +85.6 MB FormZ (.fmz) | 添加几何图形 | 66.3 MB | +66.3 MB | +66.3 MB | +66.3 MB Photoshop (.psd) | 切换图层 | 25.8 MB | +15.8 MB | +15.4 MB | +25.8 MB Movie (mp4) | 裁剪 | 13.1 MB | +13.2 MB | +0 MB | +13.1 MB 删除文件 | | -13.1 MB | +0 MB | +0 MB | +0 MB

Git LFS的优缺点

Git LFS的优点:

  1. 克隆仓库的初始过程更快,因为它只克隆轻量级指向二进制数据的指针。
  2. 本地仓库的大小更小。

Git LFS的缺点:

  1. git checkout现在必须下载二进制数据,可能需要27GB并花费3个小时以上来完成git checkout如果你提前停止,就会全部丢失
    1. 每次运行git checkout并且Git LFS需要下载更多数据时,这可能会连续发生多次。
  2. 执行git checkout需要一个活跃的高速互联网连接。(在正常的git中,git checkout可以在离线状态下进行,无需互联网连接)。
  3. 与常规Git相比,二进制文件存储实际上效率较低(请参见上表)。
注意:您可以定期清理未用于当前检出的Git LFS数据,方法是使用git lfs prune命令。请参阅我在这里的回答:如何缩小您的.git文件夹在您的git存储库中

普通的git什么时候从互联网下载文件?详细了解git fetchgit pull

这可能不为人所知,所以我认为我应该添加这一部分来说明普通的git工作原理。当我使用“下载”一词时,我指的是从互联网上下载。

正常的git只有在执行git clone、git fetch或git pull时才会从互联网上下载文件。如果你当前在主分支main上,那么git pull实际上是执行了git fetch origin main(一个在线命令,用于下载)后跟着一个git merge origin/main(一个离线命令,不进行下载)。克隆操作只是为了最初从互联网上下载存储库,所以让我们先来详细讨论一下git fetch。
但首先,让我们谈谈分支。对于每个分支,实际上你有3个分支。例如,对于你的主分支main,你有:
你本地存储的非隐藏的`main`分支, 你本地存储的远程跟踪的隐藏分支,名为`origin/main`,当你运行`git branch -r`时会显示, 你在名为`origin`的远程服务器上的远程分支,名为`main`。 要查看你的远程仓库及其URL,请运行`git remote -v`。 要查看真正的远程分支,你必须打开一个网页浏览器,并在线访问GitHub、Gitlab或Bitbucket等平台。例如,执行`git fetch && git checkout origin/main`只是显示了它的本地存储的远程跟踪副本。

git fetch 下载所有远程分支到它们隐藏的本地存储的 origin/branch_name 分支对应项,包括将你的远程 main 分支更改下载到你本地存储的、远程跟踪的隐藏分支 origin/main。当下载远程更改时使用 git fetch。如果你接着运行 git checkout main,然后是 git merge origin/main,在这两个命令中都不会下载新数据。相反,在执行 git merge origin/main 时,已经下载的数据会被合并到你本地存储的非隐藏 main 分支中。在常规的 git 中,git checkout 是一个离线命令,只是将你本地存储的已下载 git 数据库 blob 文件夹中的所有文件更新到你的本地文件系统中。

所以,让我们回顾一下并举一些更多的例子:

# Online command: download remote server data to all of your locally-stored
# remote-tracking hidden "origin/*" branches (including `origin/main`). 
git fetch

# Online command: download remote server data to only your locally-stored
# remote-tracking hidden "origin/main" branch.
git fetch origin main

# Online command: perform an online `git fetch origin main` to update
# `origin/main`, followed by an offline merge of `origin/main` into `main`. 
# So, this one command is the equivalent of these 3 commands:
#
#       git fetch origin main  # online command
#       git checkout main      # offline command
#       git merge origin/main  # offline command
#
git fetch origin main:main

# Offline command: update your local file-system to match a given
# already-downloaded state
git checkout main

# Offline command: merge your already-downloaded remote-tracking hidden branch,
# `origin/main`, into `main`.
git merge origin/main

# Online command: perform a `git fetch origin main`, which is an online command,
# followed by `git merge origin/main`, which is an offline command. This one
# command is the equivalent of these two commands:
#
#       git fetch origin main  # online command
#       git merge origin/main  # offline command
#
git pull main

与Git LFS相比,可以看出以下对比:使用git lfs时,git checkout现在变成了一个在线命令,从远程在线服务器下载存储在git lfs中的任何在线二进制文件,而不是从本地存储在main或origin/main中复制它们。这就是为什么在一个庞大的仓库中,几秒钟的git checkout现在变成了几个小时的git checkout。这也是我讨厌并且不推荐Git LFS的原因。我需要我的git checkout保持离线命令,只需几秒钟,而不是变成需要几个小时的在线命令,这样我就可以在8小时的工作日内完成8小时的工作,而不需要12到16个小时的工作日,其中一半时间被浪费掉。
附录:还要注意,Git LFS只有在本地缓存的数据不存在时才会从互联网上下载。因此,在一个全新的仓库中,当你在主分支上执行git checkout A时,它会去互联网上下载分支A的LFS数据,并将其缓存在.git/lfs目录下。然后,执行git checkout B时,它会再次去互联网上下载B分支的LFS数据。再次执行git checkout A时,它会直接从.git/lfs目录中检索已经缓存的数据,而不需要再次访问互联网,因为数据已经存在于本地。
关于.git/lfs目录的更多信息,请参考我在这里的回答:如何缩小你的git仓库中的.git文件夹
为了缓解`git checkout`变成一个"在线"命令的行为,您可以设置一个定时任务(cronjob)来定期运行`git lfs fetch --all`,比如每晚一次,如果您的硬盘有足够的空间,这样它就会在每晚预先获取一次 Git LFS 数据到您本地的`.git/lfs`目录中。详见我在这里的答案:《git lfs fetch》、《git lfs fetch --all》和《git lfs pull》之间有什么区别?。但是,如果您的硬盘足够大,那就更好了:首先根本不要使用 Git LFS。

其他参考:

  1. 关于我最初了解到三个Git分支的地方,包括本地存储的远程跟踪隐藏的 origin/* 分支,请参见此处的答案:如何在本地和远程删除Git分支?,以及我在其下方的几条评论,从这里开始

另请参阅

我的问题:在写这个问题后进行了额外学习的更新:不要使用git lfs。我现在建议不要使用git lfs
  1. 所有的“参见”链接在我的问题底部
  • 我的问答:git lfs fetchgit lfs fetch --allgit lfs pull之间有什么区别?

  • 1
    感谢您对底层过程的详细澄清。我之前并不知道!您是不是在说,如果我在启用了Git LFS的存储库中在分支A和B之间切换,然后再切回A,Git会首先从远程获取分支A的blob,然后是分支B的blob,再然后又是分支A的blob?为什么它不直接使用已经存在于本地存储库中的LFS blob呢? - Alexander Gogl
    1
    非常感谢您对底层过程命令的详细澄清。我之前并不知道这一点!您是在说,如果我在一个启用了Git LFS的仓库中从分支A切换到分支B,然后再切换回分支A,Git会先从远程获取分支A的blob,然后获取分支B的blob,最后再获取一次分支A的blob吗?为什么它不直接使用已经存在于本地仓库中的LFS blob呢? - undefined
    @AlexanderGogl,Git LFS只在本地的.git/lfs目录中没有缓存时才从互联网下载数据。因此,在一个全新的存储库中,你在main分支上,执行git checkout A会从互联网下载A的LFS数据,并将其缓存在.git/lfs/中。然后,执行git checkout B会再次从互联网下载B分支的LFS数据。再次执行git checkout A将从.git/lfs中检索到已经缓存的数据,而不需要再次访问互联网,因为它已经被缓存了。 - Gabriel Staples
    @AlexanderGogl,关于.git/lfs目录的更多信息,请参考我在这里的回答:如何缩小你的git存储库中的.git文件夹 - Gabriel Staples
    @AlexanderGogl,我又更新了我的回答,在末尾附上了一个“附录”。 - Gabriel Staples
    显示剩余4条评论

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接