通过以下功能加速Dropbox中的Git Monorepo

2020-06-11 05:20:48

我们在2014年从Mercurial迁移到Git以提高本地性能,并开始整合托管后端代码的存储库。但是随着这种单一备份的增长,我们遇到了Git性能问题,这些问题随着我们添加的文件数量呈线性增长。令人不便的是,这个问题在OSX上是最严重的-我们的大多数工程师都在OSX平台上工作。幸运的是,在Git本身的上游改进和自定义代码的小包装之间,我们已经能够加快Git操作的速度,而不会使我们统一且不断增长的存储库支离破碎。

最初,我们的代码分布在几十个Mercurial存储库中。但在2014年左右,我们进行了测试,发现使用Git会有更好的本地表现。更重要的是,Git已经成为大多数新工程师已经使用过的行业标准工具。因此,在几周的Hack过程中,一小群工程师将我们的许多存储库从Mercurial迁移到Git,并计划迁移其余的。

当时Dropbox的后端业务逻辑主要存在于一个单一的Python Web应用程序中,基础设施组件独立构建。随着时间的推移,我们将目标组件从整体中提取到单独的服务中,但我们的大量工程师仍在为整体做出贡献。例如,我们的自定义块存储系统Magic Pocket从第一天起就是单独构建的,而我们的元数据存储Edsterore最初是一个简单的客户端Python库,最终发展成为一项复杂的服务。

在实践中,这意味着整体是最大的,而且通常是单独存储库中较小的服务或某些代码的唯一使用者。因此,开发人员将在整体存储库中为这些服务编写集成测试。当服务的代码更改时,这些测试经常失败,而且由于我们的持续集成(CI)流程只是在每个存储库的头部运行所有测试(没有固定的概念),因此很难诊断问题的根本原因。

更改的频率很高,这意味着一周会有多个故障。在每日发布之前调试测试失败、跟踪更改和修复构建是一个痛苦的过程。这给发布工程师带来了大量的工作,使发布过程变得缓慢且不一致。由于这些测试失败的不确定性,工程师不信任CI测试结果并检查失败,这导致了更多的问题。为了解决这个问题,我们需要一个提交标识符来重复确定我们正在测试的代码的状态。我们要么需要一个多语言工具(或每种语言的一组工具)来直接为每个依赖项固定版本,一个像git子树这样的基于存储库的固定机制,要么需要将我们的存储库合并为一个存储库。

有一段时间,我们有一个“超级存储库”,每当与服务器相关的存储库之一发生更改时(通过Git预接收挂钩),它都会接收签入。这提供了更改和测试结果的全局排序,并有助于缩小导致损坏的存储库范围。最终,我们意识到合并所有相关存储库将是最简单的。合并后的存储库大小并不是很大(大约50,000个文件),我们估计Git的性能至少在几年内是可以接受的。这种合并,再加上其他各种改进测试基础设施和质量的计划,帮助我们以更少的工作保持Master的稳定性,并使我们的发布过程更加顺畅。最终,我们看到了更多的好处,比如简单的代码共享、简单的大规模重构、与Bazel等以monorepo为中心的构建工具的良好可操作性,以及简单的自动二等分和还原。

这在我们工作了几年,但不出所料,Git的性能开始下降。具体地说,它似乎随着添加到存储库的文件数量线性下降。像git status这样的常见操作随着时间的推移变得越来越慢。因此,在2017年末,我们开始研究为我们的用户加快Git速度的各种选择。

首先,签入大文件会严重影响Git中许多操作的性能。幸运的是,我们没有依赖于此的工作流,我们设置了一个预接收钩子来限制推送到存储库的新文件的大小,并防止倒退。

OSX是Dropbox工程师支持的开发平台,但他们可以自由使用他们认为合适的其他平台。例如,在Dropbox桌面客户端上工作的工程师可能在Windows上工作,而一些服务器工程师更喜欢Linux。实际上,我们的大多数服务器工程师都使用OSX,这也是我们集中精力的地方。

为了解决本地Git性能问题,我们需要控制OSX上的开发人员使用的Git版本。此外,我们还必须衡量绩效。我们创建了一个小的Git分支,用于测量Git状态和Git拉入等操作的时间,自动配置并安装在开发人员机器上,并设置$PATH,以便开发人员使用我们的Git而不是系统或Homebrew安装的Git。

当时,git状态平均需要两秒以上。在我们的示例中,许多Git操作都很慢,并且呈线性增长,因为它们对存储库中的每个文件运行lstat syscall以检查它是否是最新的。由于大多数开发人员只修改了一小部分文件,因此这在大多数情况下都是浪费周期。有趣的是,与OSX相比,Linux上的GIT状态要快5-10倍。

在过去的几年里,开源社区在为大型存储库加速Git方面做了大量工作。例如,Git现在有一个文件系统监视器(Fsmonitor)来检测更改的文件,它与Watchman集成在一起,Watchman是一个监视和缓冲文件系统更改的守护进程。Fsmonitor是一个Git钩子,充当Watchman的薄薄包装器。对于内部Git测试来说,这是一个有用的抽象,如果需要使用备用文件监视器或从Watchman迁移,则会很有帮助。

Git使用一个索引文件,该文件包含存储库中每个文件的条目,并确定暂存哪些文件以供提交。该索引还支持fsmonitor等各种功能的扩展,并且他的文件能够缓存来自fsmonitor的结果。需要本地文件系统状态(如add、status或diff)的Git操作使用Fsmonitor检查更改的文件,如果有任何更改,则更新索引。

默认情况下,索引按文件路径排序。因此,像将文件添加到索引(通过git add)这样的常见操作需要完整的索引重写才能将新路径插入到正确的位置,这对于索引较大的存储库来说是很慢的。

Git还引入了一种新的--拆分索引模式来转换索引格式,因此增量可以快速地附加到拆分索引文件中,并最终合并到几个共享索引文件中,从而大大加快了索引写入速度。最后,还有一个未跟踪的缓存模式,可以缓存目录mtime,这样Git就可以跳过未修改目录的遍历。

我们在开发者MacBook上部署了fsmonitor和Watchman,并发布了一些关于如何打开它的说明。不幸的是,其中既有性能错误(Git在某些情况下似乎忽略了fsmonitor数据),也有正确性错误(在启用fsmonitor的情况下,git status等操作有时会返回错误结果)。我们深入研究并修复了一些错误,但当我们失去了拥有Git专业知识的团队成员并有其他优先事项时,我们最终转移了重点。

2019年下半年,我们的回购已经达到了>;25万个文件,我们决定重新关注这个问题。上游修复看起来很有希望,我们修复了一个很大的性能问题。这一次,我们有信心在没有用户干预的情况下为所有用户实现这些改进。

我们的核心原则之一是提供默认情况下为每个人正确配置的开发人员工具。我们决定在Git之上发布一个包装器,它将自动调整配置并为开发人员启用fsmonitor(如果它还没有打开的话),只为一组列入白名单的存储库启用fsmonitor。这在原则上与微软的Scalar非常相似,但没有大部分功能,也不需要开发人员学习额外的工具和运行额外的命令。

我们看到普通手术的p50和p90持续时间显著缩短。值得注意的是,这些操作远不及在较小的存储库中运行Git快,但与现状相比,它们是一个很大的改进,并且对于大多数目的来说都是可以接受的。我们还确信,这些时间不会随着存储库中的文件数量线性增长。最后,所有这些都是通过<;200行围绕Git的自定义管道代码实现的,不需要维护额外的服务(或大型虚拟文件系统)。

开源领域正在进行一些激动人心的工作,以提高大型存储库(Git和Mercurial)的版本控制性能。通过一些准备工作,比如不允许大文件和为客户端设置正确的标志,Git可以相当好地执行,几乎没有持续的维护负担。在单一存储库和多个存储库之间做出决定时有很多权衡,但是版本控制可伸缩性在很长一段时间内不应该成为交易的破坏者。

从Mercurial到Git的迁移,到存储库合并,最后到加速Git的工作,数十名工程师多年来为上述工作做出了贡献。各种现任和前任Dropboxers包括Tim Abbott,Jon Goldberg,Nipunn Koorapati,Jason Michalski,Greg Price,Mike Solomon和Alex Vandiver。此外,非常感谢Git的维护人员,以及帮助检查我们的补丁并为Git贡献了几个主要功能(如文件系统监视器)的微软工程师。并赞扬Facebook建立和开源Watchman,以及编写了我们使用的Go Watchman库的查尔斯·斯特拉汉(Charles Strahan)。