Close

Git 中的多包存储库


什么是多包存储库?


定义各不相同,但我们对多包存储库的定义如下:

  • 存储库包含多个逻辑项目(例如 iOS 客户端和 Web 应用)
  • 这些项目的连接很可能是无关的、连接松散的,或者可以通过其他方式(例如通过依赖关系管理工具)进行连接
  • 存储库在很多方面都很大:
    • 提交次数
    • 分支和/或标记的数量
    • 跟踪的文件数
    • 跟踪的内容的大小(通过查看存储库的 .git 目录来衡量)

Facebook 有一个多包存储库的例子:

Facebook 的主源存储库每周有数千次提交,涉及数十万个文件,比 Linux 内核还要大很多倍。2013 年,Linux 内核签入了 1,700 万行代码和 4.4 万个文件。

数据库
相关资料

如何移动完整的 Git 存储库

Bitbucket 徽标
查看解决方案

了解 Bitbucket Cloud 的 Git

进行性能测试时,Facebook 使用的测试存储库如下:

  • 四百万次提交
  • 线性历史记录
  • 大约 130 万个文件
  • .git 目录的大小大约为 15GB
  • 索引文件的大小为 191MB

概念性挑战


在 Git 的多包存储库中管理不相关的项目时,会遇到许多概念上的挑战。

首先,Git 会在每次提交时跟踪整棵树的状态。这对于单个项目或相关项目来说没问题,但对于包含许多不相关项目的存储库来说,这会变得很麻烦。简而言之,树中不相关部分的提交会影响与开发人员相关的子树。随着大量提交推进树的历史记录,这个问题就会变得很明显。由于分支提示一直在变化,因此需要经常在本地进行合并或变基才能推送变更。

在 Git 中,标记是特定提交命名别名,指的是整个树。但是在多包存储库的背景下,标记的用处会降低。问问自己:如果您正在开发一个持续部署在多包存储库中的 Web 应用,那么版本控制的 iOS 客户端的发布标记有什么相关性?

性能问题


除了这些概念性挑战外,还有许多性能问题,这些问题可能会影响多包存储库设置。

提交次数

在提交级别上大规模管理单个存储库中的不相关项目可能会很麻烦。随着时间的推移,这可能会导致大量的提交,且增长率很高(Facebook 引用“每周数千次提交”)。这变得特别麻烦,因为 Git 使用有向非循环图 (DAG) 来表示项目的历史记录。在大量提交的情况下,随着历史记录的加深,任何遍历图表的命令都可能变慢。

Some examples of this include investigating a repository's history via git log or annotating changes on a file by using git blame. With git blame if your repository has a large number of commits, Git would have to walk a lot of unrelated commits in order to calculate the blame information. Other examples would be answering any kind of reachability question (e.g. is commit A reachable from commit B). Add together many unrelated modules found in a monorepo and the performance issues compound.

引用数量

您的多包存储库中的大量引用(即分支或标记)会以多种方式影响性能。

Ref advertisements contain every ref in your monorepo. As ref advertisements are the first phase in any remote git operation, this affects operations like git clone, git fetch or git push. With a large number of refs, performance takes a hit when performing these operations. You can see the ref advertisement by using git ls-remote with a repository URL. For example, git ls-remote git://git.kernel.org/ pub/scm/linux/kernel/git/torvalds/linux.git will list all the references in the Linux Kernel repository.

If refs are loosely stored listing branches would be slow. After a git gc refs are packed in a single file and even listing over 20,000 refs is fast (~0.06 seconds).

Any operation that needs to traverse a repository's commit history and consider each ref (e.g. git branch --contains SHA1) will be slow in a monorepo. In a repository with 21708 refs, listing the refs that contain an old commit (that is reachable from almost all refs) took:

用户时间(秒):146.44*

*这将因页面缓存和底层存储层而异。

跟踪的文件数

索引或目录缓存 (.git/index) 跟踪存储库中的每个文件。Git 使用此索引通过对每个文件执行 stat (1),并将文件修改信息与索引中包含的信息进行比较,以确定文件是否已变更。

因此,跟踪的文件数量会影响许多操作的性能*:

  • git status 可能很慢(统计每个文件,索引文件会很大)
  • git commit 也可能很慢(还会统计每个文件)

*这将因页面缓存和底层存储层而异,并且只有在有大量文件(成千上万)时才会明显。

大文件

单个子树/项目中的大文件会影响整个存储库的性能。例如,尽管开发人员(或内部版本支持人员)正在处理不相关的项目,但在多包存储库中添加到 iOS 客户端项目的大型媒体资产仍会被克隆。

组合效果

无论是文件数量、变更频率还是文件大小,这些问题加在一起都会对性能产生更大的影响:

  • 在分支/标记之间切换,这在子树上下文(例如我正在研究的子树)中最有用,它仍然会更新整个树。由于受影响的文件数量众多,此流程可能会很慢,或者需要解决方法。使用 git checkout ref-28642-31335 -- templates 会更新./templates 目录至匹配给定分支,但不更新 HEAD,其副作用是在索引中将更新后的文件标记为已修改。
  • 克隆和提取速度很慢,并且占用服务器上的大量资源,因为所有信息在传输前都会压缩在打包文件中。
  • 垃圾回收速度很慢,默认是在推送时触发的(如果需要垃圾回收的话)。
  • 每个涉及(重新)创建打包文件的操作的资源使用率都很高,例如git upload-pack, git gc.

缓解策略


虽然如果 Git 能够支持单体式存储库这样的特殊用例,那就太好了,但是 Git 的设计目标使它取得了巨大的成功并且广受欢迎,有时与希望以非其设计的方式使用它的愿望不一致。对于绝大多数团队来说,好消息是,实际上,真正的大型单体式存储库往往是例外而不是常规,因此,尽管这篇文章很有趣,但它很可能不适用于您所面临的情况。

也就是说,在处理大型存储库时,有一系列缓解策略可以提供帮助。对于历史记录较长或二进制资产较大的存储库,我的同事 Nicola Paolucci 介绍了一些解决方法

删除引用

如果您的存储库有很多引用,您应该考虑删除不再需要的引用。DAG 保留了变更演变的历史记录,而合并提交指向其父项,因此即使分支已不存在,也可以追踪在分支上进行的工作。

基于分支的工作流程中,要保留的长期运行分支的数量应该很少。不要害怕在合并后删除一个短期运行的功能分支。

考虑删除所有已合并到主分支(如生产)的分支。只要可以从主分支访问提交,并且您已经将分支与合并提交合并,那么追踪变更演变的历史记录仍然是可能的。默认的合并提交消息通常包含分支名称,允许您在必要时保留此信息。

处理大量文件

如果您的存储库有大量文件(数万到数十万),则使用快速本地存储和大量可用作缓冲区缓存的内存会有所帮助。这是一个需要对客户进行更重大变更的领域,例如,类似于 Facebook 为 Mercurial 实施的变更

他们的方法使用文件系统通知来记录文件变更,而不是遍历所有文件来检查其中是否有任何变更。已经为 git 讨论了类似的方法(也使用了 watchman),但尚未最终实现。

使用 Git LFS(大型文件存储)

本部分已于 2016 年 1 月 20 日更新

对于包含视频或图形等大型文件的项目,Git LFS 是限制它们对存储库大小和整体性能的影响的一种选择。Git LFS 不是将大型对象直接存储在存储库中,而是存储一个同名的小型占位符文件,其中包含对该对象的引用,该文件本身存储在专门的大型对象存储库中。Git LFS 挂接到 Git 的原生推送、拉取、签出和提取操作,以透明地处理工作树中这些对象的传输和替换。这意味着您可以像往常一样处理存储库中的大型文件,而不会受到存储库过大的影响。

Bitbucket Server 4.3(及更高版本)嵌入了完全兼容的 Git LFS v1.0+ 实现,允许在 Bitbucket 用户界面中直接预览和比对 LFS 跟踪的大型图像资产。

Image tracked by LFS

我的同伴 Atlassian Steve Streeting 是 LFS 项目的积极贡献者,最近写了关于该项目的文章

确定界限并拆分存储库

最激进的解决方法是将您的多包存储库拆分为更小、更有针对性的 git 存储库。尝试不再跟踪单个存储库中的每一次变更,而是通过识别具有相似发布周期的模块或组件来确定组件边界。对于透明子组件,一个很好的试金石是在存储库中使用标记,以及它们对源树的其他部分是否有意义。

虽然如果 Git 能够支持多包存储库会很棒,但是多包存储库的概念与最初让 Git 取得巨大成功和受欢迎的原因略有冲突。但是,这并不意味着您应该因为自己只有多包存储库而放弃 Git 的功能——在大多数情况下,对于出现的任何问题,都有可行的解决方案。


分享此文章
下一主题

推荐阅读

将这些资源加入书签,以了解 DevOps 团队的类型,或获取 Atlassian 关于 DevOps 的持续更新。

人们通过满是工具的墙进行协作

Bitbucket 博客

Devops 示意图

DevOps 学习路径

与 Atlassian 专家一起进行 Den 功能演示

Bitbucket Cloud 与 Atlassian Open DevOps 如何协同工作

注册以获取我们的 DevOps 新闻资讯

Thank you for signing up