Git与大文件

57

情况

我有两个服务器,生产和开发。在生产服务器上,有两个应用程序和多个(6)数据库(MySQL),需要将其分发给开发人员进行测试。所有源代码都存储在开发服务器上的GitLab中,开发人员仅使用此服务器工作,并且无法访问生产服务器。当我们发布一个应用程序时,管理员登录到生产环境,从Git拉取新版本。数据库很大(每个超过500M并不断增加),我需要尽可能简单地将它们分发给开发人员进行测试。

可能的解决方案

  • 执行备份脚本将每个数据库转储为单个文件后,执行将每个数据库推送到其自己的分支的脚本。如果开发人员想要更新本地副本,则拉取其中之一。

    此方法未能奏效。

  • 生产服务器上的Cron每天保存二进制日志,并将它们推送到该数据库的分支中。因此,在该分支中,存在具有每日更改的文件,开发人员拉取他们没有的文件即可。当前的SQL转储将以另一种方式发送给开发人员。当存储库大小变得太大时,我们将向开发人员发送完整的转储,并清除存储库中的所有数据并从头开始。

问题

  • 这个解决方案可行吗?
  • 如果Git在将文件推送/拉取到/自仓库时,会上传/下载整个文件,还是仅更改其中的内容(即添加新行或编辑当前行)?
  • Git能管理如此大的文件吗?不行。
  • 如何设置在库中保留多少修订版本?新解决方案无关紧要。
  • 是否有更好的解决方案?我不想强制开发人员通过FTP或类似的方式下载这么大的文件。

感兴趣的内容:Git中的文件限制(数量和大小)是什么? - user456814
你的意思是说git-annex不能作为解决方案使用吗? - VonC
这些问题似乎很相关:https://dev59.com/2mYr5IYBdhLWcg3whKit#13689613 和 https://dev59.com/iXRA5IYBdhLWcg3wvQhh - onionjake
请查看我的回答中关于git限制和可能的基于git的备份工具的更多信息(链接:https://dev59.com/qmMm5IYBdhLWcg3wFL04#19494211)。 - VonC
1
我已经使用Git LTS(GitHub,2015年4月)编辑了我的答案 - VonC
7个回答

70

2017年更新:

微软正在为Microsoft/GVFS做出贡献:这是一个Git虚拟文件系统,允许Git处理"地球上最大的代码库"(即Windows代码库,约有350万个文件,当检入到Git存储库时,会产生大约300GB的存储库,并在440个分支上每天生成1,760个"实验室构建"以及数千个拉取请求验证构建)。

GVFS将你的git存储库下面的文件系统虚拟化,以便git和所有工具看到的是一个正常的存储库,但GVFS只在需要时下载对象。

GVFS的一些部分可能会被贡献到上游(即Git本身)。
但与此同时,所有新的Windows开发现在(2017年8月)都在Git上进行


Update April 2015: GitHub提出:宣布Git大文件存储(LFS) 使用git-lfs(参见git-lfs.github.com)和支持它的服务器:lfs-test-server,您可以仅在git repo中存储元数据,并将大文件存储在其他地方。每次提交最大为2 Gb。

https://cloud.githubusercontent.com/assets/1319791/7051226/c4570828-ddf4-11e4-87eb-8fc165e5ece4.gif

请参阅git-lfs/wiki/Tutorial

git lfs track '*.bin'
git add .gitattributes "*.bin"
git commit -m "Track .bin files"

原始回答:

关于 git对大文件的限制,您可以考虑使用bup(在GitMinutes#24中详细介绍)

bup的设计突出了限制git repo的三个问题:

  • 巨大的文件(packfile的xdelta仅存在于内存中,这对于大文件来说并不好)
  • 大量的文件,这意味着每个blob一个文件,并且git gc缓慢地生成一个packfile。
  • 巨大的packfiles,具有从(巨大的)packfile检索数据效率低下的packfile索引。

处理大文件和xdelta

Git不能处理大文件的主要原因是它会通过xdelta处理这些文件,这通常意味着它会尝试一次性将整个文件的内容加载到内存中
如果不这样做,它将不得不存储每个文件的每个版本的全部内容,即使您只更改了该文件的几个字节。
那将是磁盘空间非常低效的使用方式,而且Git以其惊人的高效存储库格式而闻名。

不幸的是,xdelta对于小文件效果很好,但对于大文件则变得非常缓慢且占用内存
对于Git的主要目的,即管理源代码,这不是一个问题。

翻译: bup所做的不是使用xdelta,而是我们称之为"哈希拆分"。我们想要一种通用的方法来高效地备份任何可能会发生小变化的大文件,而不需要每次都存储整个文件。我们逐字节读取文件,计算最后128字节的滚动校验和。 你可以在bupsplit.c中找到rollsum函数。它在其工作中表现良好。基本上,它将最后读取的128个字节转换为32位整数。然后我们取滚动校验和的最低13位,如果它们全部为1,则认为这是一个块的结尾。这平均每2^13 = 8192个字节发生一次,因此平均块大小为8192个字节。我们根据滚动校验和将这些文件分成块,然后将每个块单独存储(由其sha1sum索引)作为git blob。
使用哈希分割,无论您在文件中添加、修改或删除多少数据,在受影响块之前和之后的所有块都是完全相同的。
哈希分割算法所关心的只是32字节的“分隔符”序列,单个更改最多只能影响一个分隔符序列或两个分隔符序列之间的字节。
就像魔术一样,哈希分割块算法每次都会以相同的方式对文件进行分块,即使不知道它以前如何进行分块。
接下来的问题不太明显:在将一系列块存储为git blob之后,如何存储它们的顺序?每个blob都有一个20字节的sha1标识符,这意味着仅仅是块的简单列表就将占据文件长度的0.25%。
对于一个200GB的文件,那就是488兆的序列数据。
我们使用所谓的“扇出”进一步扩展了hashsplit算法。我们不仅检查校验和的最后13位,而是使用额外的校验和位来产生更多的分割。你最终得到的是一个实际的blob树 - git 'tree'对象是表示这种结构的理想选择。
处理大量文件和git gc
Git旨在处理相对较小且相对不经常更改的存储库。您可能认为您会“频繁”地更改源代码,并且git可以处理比如svn更频繁的更改。但这不是我们讨论的那种“频繁”。
它添加新对象到存储库的方式是#1杀手:它为每个blob创建一个文件。然后您稍后运行'git gc'并将这些文件合并为单个文件(使用高效的xdelta压缩,并忽略不再相关的任何文件)。
'git gc'很慢,但对于源代码存储库来说,结果是超级高效的存储(以及与存储的文件关联的非常快速的访问),这是值得的。

bup不会这样做。它只是直接写入packfiles。
幸运的是,这些packfiles仍然符合git格式,因此一旦它们被写入,git就可以愉快地访问它们。

处理庞大的存储库(意味着巨量的packfiles)

Git实际上并不是为了处理超级大的存储库而设计的
大多数git存储库都足够小,可以将它们全部合并到单个packfile中,通常'git gc'最终会这样做。

大packfile的问题不在于packfile本身 - git旨在期望所有pack的总大小大于可用内存,一旦它可以处理它,它就可以同样有效地处理任何数量的数据。
问题在于packfile索引(.idx)文件

每个git中的pack文件(*.pack)都有一个关联的idx文件(*.idx),其中包含了git对象哈希和文件偏移量的排序列表。如果你要查找特定sha1的对象,可以打开idx文件,进行二进制搜索以找到正确的哈希,然后获取相应的文件偏移量,在pack文件中寻找并读取该对象的内容。
二分搜索的性能大约为O(log n),随着pack中哈希数量的增加而增加,但有优化的第一步(可以在其他地方了解详细信息),可以将其略微提高到O(log(n)-7)。但遗憾的是,当你有很多pack时,这种方法会有点失效。
为了改善这种操作的性能,bup引入了midx(读作“midix”和“multi-idx”的缩写)文件。正如名称所示,它们可同时索引多个pack。

3
你使用的模糊概念“frequent”、“huge”和“lots”的定义是什么?是“每天两次”、“1 GB”和“1E6”吗? - Cees Timmerman
@CeesTimmerman 我没有复制 https://github.com/bup/bup/blob/master/DESIGN 的全部部分。例如,关于第一个术语,它包括以下内容(我没有包括在内):“想象一下,你正在备份磁盘上的所有文件,其中一个文件是一个拥有数百个日常用户的 100 GB 数据库文件。您的磁盘变化如此频繁,即使您全天候备份文件,也无法备份所有修订版。这就是“频繁”。” - VonC
所以,“frequently”意味着“周期比推送所需时间更短”。 - Cees Timmerman
@CeesTimmerman 是的,但最近“模块化”方法因其复杂性而受到批评,因此Facebook在Mercurial周围启动了一项倡议来管理他们的巨大repo(http://www.infoq.com/news/2014/01/facebook-scaling-hg)。最近对DVCS模型进行了一些(轻微的)批评:http://bitquabit.com/post/unorthodocs-abandon-your-dvcs-and-return-to-sanity/,http://gregoryszorc.com/blog/2014/09/09/on-monolithic-repositories/。 - VonC
嗨,如果您能用终端命令完善您的答案,那就太完美了:使用git-lfs将大文件(来自任何地方)发送到大型存储库而无需克隆存储库。 - Peter Krauss
显示剩余5条评论

32

你绝对不想向 Git 仓库中提交大型二进制文件。

每次更新都会累积增加存储库的整体大小,这意味着在以后的时间里,你的 Git 仓库将需要更长的克隆时间,并占用越来越多的磁盘空间,因为 Git 在本地存储分支的整个历史记录,这意味着当有人检出该分支时,他们不只是要下载数据库的最新版本; 他们还必须下载每个先前版本。

如果需要提供大型二进制文件,请将它们上传到其他服务器,然后检入一个文本文件,其中包含开发人员可以下载大型二进制文件的 URL。FTP 实际上是较好的选项之一,因为它专门设计用于传输二进制文件,尽管 HTTP 可能更加简单明了。


2
我同意。在git中的历史记录实际上并没有什么意义,因此添加二进制文件没有太多意义。相反,应该想出一个良好的命名约定,将它们存储在某个地方,并使用脚本来处理提取。 - onionjake
@JakubRiedl,你最好找到一种非Git的方式来分发你的SQL补丁。 - Amber

31

Rsync可以高效地更新开发者拷贝的数据库。它使用增量算法来逐步更新文件,这样只传输已更改或新增的文件块。当然,他们仍需要先下载完整的文件,但以后的更新会更快。

基本上,您可以获得类似于git fetch的增量更新,而没有git clone会提供的不断扩大的初始副本。缺失的是历史记录,但听起来您不需要那个。

如果您需要在Windows上使用rsync,它是大多数Linux发行版的标准组成部分,可以使用打包的端口

要将数据库推送到开发人员,您可以使用类似于以下命令:

rsync -avz path/to/database(s) HOST:/folder

或者开发人员可以使用以下命令获取他们需要的数据库:

rsync -avz DATABASE_HOST:/path/to/database(s) path/where/developer/wants/it

这对我们来说是最好的解决方案,因为开发人员只需要一个脚本就可以更新他的数据库,并移动他没有的文件部分。非常感谢。 - Jakub Riedl
如果您使用rsync,您如何知道哪个数据库版本对应哪个提交? - JuanPablo
1
你可以通过校验和来检查数据库的“完整性”,但是,你如何控制数据库的更改?你如何知道需要对数据库进行哪些更改才能与特定提交一起使用? - JuanPablo

26
您可以查看类似于git-annex的解决方案,它涉及使用git管理(大)文件,而不将文件内容检入到git中!
(2015年2月:像GitLab这样的服务主机可以原生地集成它
请参阅“GitLab是否支持通过git-annex或其他方式处理大型文件?”)

Amber她的回答中所解释的那样,git不能管理大文件。

那并不意味着git将来不能做得更好。来自GitMinutes episode 9(2013年5月,下面还有相关内容),来自Peff(Jeff King),在36'10''处:
有一个完全不同的大型存储库领域,人们有兴趣存储20、30或40 GB,甚至是TB级别的存储库,这一部分来自于拥有大量文件,但更多的是来自于具有非常大的文件和二进制文件,它们彼此之间处理得不好。这是一个公开的问题。有几种解决方案:git-annex可能是其中最成熟的解决方案,他们基本上不会将资产放入git中,而是将大型资产放在资产服务器上,并在git中放置一个指针。我想做类似的事情,其中资产在概念上在git中,也就是说该对象的SHA1是树中的一部分,是提交ID中的一部分以及所有这些内容。因此从git的角度来看,它是仓库的一部分,但在下面的级别,即对象存储级别,在概念历史图形的下面,我们已经有了多种存储对象的方法:我们有松散对象,我们有打包对象,我想要一个新的存储对象的方式,也就是说“我们没有它,但可以通过资产服务器获得”,或者类似的东西。像git-annex这样的问题是:一旦你使用它们,你就会永远被锁定在当时做出的决定中。你知道,如果你决定200 MB很大,我们将存储在资产服务器上,然后,稍后你决定,啊应该是300 MB,好吧,那就太糟糕了:这将永远编码在你的历史中。因此,通过在git级别上说概念上,这个对象在git仓库中,并不是指向它的指针,不是指向资产服务器的指针,实际的对象在那里,然后在低级别、存储级别上处理这些细节,这样就可以使你自由地做出许多不同的决策,甚至可以在以后改变你关于如何实际存储硬盘上的东西的决定。

现在不是一个高优先级的项目...


三年后的2016年4月,Git Minutes 40中包括了一次GitHub的Michael Haggerty的采访,大约在31分钟处(感谢Christian Couder进行采访)。

他已经专注于后端相关工作有一段时间了。 他在引用David Turner的后端工作作为目前最有趣的内容。(请参见David当前的“可插拔后端”分支的git/git fork)
CD:目标是将 git refs 存储在数据库中,是吗? MH:是的,我认为有两个有趣的方面:第一个就是具有插入不同源条目引用的能力。 条目引用存储在文件系统中,作为松散引用和打包引用的组合。
松散引用是每个引用一个文件,打包引用是一个包含许多引用列表的大文件。所以这是一个不错的系统,特别是对于本地使用;对于普通人来说,它没有任何真正的性能问题,但它确实有一些问题,例如您无法在引用被删除后存储引用 reflogs,因为可能会与使用类似名称创建的较新引用发生冲突。还有一个问题是引用名称存储在文件系统上,因此您可以拥有命名相似但具有不同大写字母的引用。
因此,通过一般具有不同引用后端系统可以修复这些问题。而 David Turner 的补丁系列的另一个方面是更改以将引用存储在称为lmdb 的数据库中,这是一个非常快速的基于内存的数据库,比文件后端有一些性能优势。
[跟随其他考虑,关于更快的打包和参考补丁广告]

有趣的话题:使用Git管理大型二进制文件 - user456814

2
拥有一个从git堆栈代码中引用的文件辅助存储是大多数人选择的方案。尽管git-annex看起来非常全面,但许多商店只使用FTP或HTTP(或S3)存储库来存储大文件,如SQL转储文件。我的建议是通过将某些元数据 - 特别是校验和(可能是SHA) - 填充到哈希中,并添加日期,将git仓库中的代码与辅助存储中的文件名绑定在一起。
  • 因此,每个辅助文件都会获得一个基本名称、日期和SHA(对于某个版本n)总和。
  • 如果你拥有不断变化的文件,仅使用SHA会导致哈希冲突的微小但真实风险,因此需要包含日期(epoch time或ISO日期)。
  • 将生成的文件名放入代码中,以便可以非常明确地引用辅助块。
  • 以这样的方式组织文件名,以便可以轻松编写一些脚本来查找所有辅助文件名,以便获取任何提交的列表。这也允许在某些点上退役一些旧的文件,并且可以与部署系统集成,以便在激活来自git仓库的代码之前将新的辅助文件从生产环境中拉出而不覆盖旧文件。
将大文件塞入git(或大多数仓库)会对git的性能产生不良影响-例如,git clone真的不应该花费20分钟。而使用引用文件的方式意味着有些开发人员甚至根本不需要下载大块内容(这与git clone形成了鲜明对比),因为很可能它们只与在生产中部署的代码相关。当然,你的情况可能会有所不同。

0

上传大文件有时会出现问题和错误。这通常发生。主要是git支持少于50MB的文件上传。如果要在git存储库中上传超过50MB的文件,则用户应该需要安装另一个助手来协助上传大文件(.mp4,.mp3,.psd)等。

在上传大文件到git之前,您需要了解一些基本的git命令。这是上传到github的配置。需要安装gitlfs.exe

lfsinstall.exe安装它



然后,您应该使用git的基本命令以及一些不同的命令

git lfs install
git init
git lfs track ".mp4"
git lfs track ".mp3"
git lfs track ".psd"
git add .
git add .gitattributes
git config lfs.https://github.com/something/repo.git/info/lfs.locksverify false 
git commit -m "Add design file"
git push origin master` ones

如果你在推送时没有使用 lfs.https://github.com/something/repo.git/info/lfs.locksverify false,可能会在 push 命令期间发现类似指示的内容。


0

正如其他答案中所述,将大文件存储在git中是极不推荐的。我不会再重复这个问题。

你的问题似乎更像是关于数据库持久性而不是git。如果数据库信息不是很多,那么

  1. 对于Java,你可以使用flywaydb(java)来存储每个发布之间数据库的差异。
  2. 对于Django,它可以将数据库信息存储为json转储(python manage.py dumpdata your_app > datadump.json),然后在其他地方重新加载它(python manage.py loaddata datadump.json)。

然而,由于你的数据库很大,那么你应该考虑使用流行的二进制存储库,比如nexusartifactory,它们可以存储二进制文件用作gitlfs的存储库。然后为了减轻开发人员的负担,因为你不希望他们明确下载文件,你需要构建自己的CI/CD流水线,使开发人员能够通过一键发布。


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