Facebook的主要源代码库每周提交数千次,涉及数十万个文件,其规模甚至是Linux内核的数倍,后者在2013年签入了1700万行代码和44000个文件。考虑到我们的规模和复杂性-以及Facebook每天两次发布代码的做法-改进我们的源代码控制是帮助我们的工程师快速行动的一种方式。
两年前,当我们看到我们的存储库继续以惊人的速度增长时,我们坐下来将我们的增长向前推了几年。根据这些预测,我们当时的技术,一种带有Git镜像的Subversion服务器,很可能很快就会成为生产力的瓶颈。我们查看了可用的选项,发现没有一种既快速又易于大规模使用的选项。
我们的代码库是有机增长的,其内部依赖关系非常复杂。我们本可以花费大量时间,以一种对源代码控制工具友好的方式使其更加模块化,但是使用单一存储库有很多好处。即使在我们目前的规模下,我们也经常在整个代码库中进行较大的更改,拥有单一的存储库对于持续的现代化很有用。将其拆分会使大型的原子重构变得更加困难。最重要的是,认为我们的源代码控制系统的缩放约束应该决定我们的代码结构的想法并不适合我们。
我们意识到我们必须自己解决这个问题。但是,我们没有从头开始构建一个新的系统,而是决定采用现有的系统,并使其规模化。我们的工程师对Git很满意,而我们更喜欢使用熟悉的工具,所以我们花了很长时间,努力改进它,使其能够规模化工作。经过深思熟虑后,我们得出结论,对于一个雄心勃勃的扩展项目来说,Git的内部结构将很难使用。
相反,我们选择改进Mercurial。Mercurial是一个类似于Git的分布式源代码控制系统,具有许多相同的功能。重要的是,它主要是用干净、模块化的Python编写的(带有一些用于热路径的本机代码),这使得它具有很强的可扩展性。同样重要的是,Mercurial开发者社区正在积极帮助我们解决可伸缩性问题,他们检查我们的补丁,并在设计新功能时牢记我们的规模。
当我们第一次开始研究Mercurial时,我们发现它在几个值得注意的领域比Git慢。为了缩小这一性能差距,在过去一年半的时间里,我们为Mercurial贡献了500多个补丁。范围从新的图形算法到在本机代码中重写紧密循环。这些都有帮助,但我们也想做出更根本的改变来解决规模问题。
对于像我们这样大的存储库,一个主要的瓶颈就是找出哪些文件发生了更改。Git会检查每个文件,随着文件数量的增加,Git自然会变得越来越慢,而Perforce则通过强迫用户告诉它他们要编辑哪些文件来“作弊”。Git方法不能扩展,Perforce方法也不友好。
我们通过监视文件系统的更改解决了这个问题。这之前已经尝试过了,即使是多变的,但要让它可靠地工作是令人惊讶的挑战。我们决定查询构建系统的文件监视器Watchman,以查看哪些文件发生了更改。Mercurial的设计使得与Watchman的集成变得简单,但我们预计Watchman会有错误,所以我们开发了一种策略来安全地解决这些问题。
通过繁重的压力测试和内部疏导,我们确定并修复了文件系统监视中常见的许多问题和争用情况。特别是,我们在所有工程师的机器上运行了一个beta测试,将Watchman对真实用户查询的回答与实际文件系统结果进行比较,并记录任何差异。经过几个月的监控和修复使用上的差异,我们得到了足够低的使用率,我们可以放心地为我们的工程师默认启用Watchman。
对于我们的存储库,启用Watchman集成使Mercurial的status命令比Git的status命令快5倍以上。查找更改的文件的其他命令(如diff、update和Commit)也变得更快。
犯罪率和我们历史的巨大规模也带来了挑战。我们每天都有数以千计的提交,随着存储库变得越来越大,克隆和提取所有的存储库变得越来越痛苦。像Subversion这样的集中式源代码控制系统只签出一次提交,将所有历史记录保留在服务器上,从而避免了这种情况。这节省了客户端的空间,但如果服务器宕机,您将无法工作。更新的分布式源代码控制系统,如Git和Mercurial,将所有历史复制到客户端,这需要更多的时间和空间,但允许您完全在本地浏览和提交。我们希望在集中式系统的速度和空间与分布式系统的健壮性和灵活性之间找到一个令人满意的中间地带。
通常,当您运行Pull时,Mercurial会计算出自上次Pull以来服务器上发生了什么变化,并下载任何新的提交元数据和文件内容。由于每天都有数以万计的文件在更改,因此每天将所有这些历史记录下载到客户端的速度很慢。为了解决这个问题,我们为Mercurial创建了remotefilelog扩展。此扩展将克隆和拉入命令更改为仅下载提交元数据,而忽略占下载大容量的所有文件更改。当用户执行需要文件内容的操作(如签出)时,我们使用Facebook现有的memcache基础设施按需下载文件内容。这样,无论历史发生了多少更改,克隆和拉入都可以很快,而只会给签出增加很小的开销。
但是如果中央Mercurial服务器出现故障怎么办?分布式源代码控制的一大好处是能够在不与服务器交互的情况下工作。Remotefilelog扩展可以智能地缓存本地提交所需的文件修订,这样您就可以签出、更改基址和提交到任何现有书签,而无需访问服务器。因为我们仍然下载所有提交元数据,所以不需要文件内容(如日志)的操作也完全是本地的。最后,我们使用Facebook的memcache基础设施作为中央Mercurial服务器前面的缓存层,这样即使中央存储库出现故障,memcache也会继续为许多文件内容请求提供服务。
当然,这种类型的设置并不适合每个人-它针对拥有可靠的Mercurial服务器并且始终连接到快速、低延迟网络的工作环境进行了优化。对于没有快速、可靠的Internet连接的工作环境,此扩展可能会导致Mercurial命令运行缓慢,并且在服务器拥塞或无法访问时会意外失败。
在Facebook为员工启用RemoteFileLog扩展使得Mercurial克隆和拉取速度提高了10倍,使克隆速度从几分钟降到了几秒。此外,由于remotefilelog将其本地数据存储在磁盘上的方式,大型rebase的速度要快2倍。与我们以前的Git基础设施相比,这些数字仍然令人印象深刻。通过扩展实现这些类型的性能提升是我们选择Mercurial的主要原因之一。
最后,remotefilelog扩展允许我们将大部分请求流量转移到memcache,这将Mercurial服务器的网络负载减少了10倍以上。这将使我们多变的基础设施更容易不断扩展以满足不断增长的需求。
Mercurial有几个很好的抽象,使得这个扩展成为可能。最值得注意的是FileLog类。文件日志是用于表示特定文件的每个修订的数据结构。文件的每个版本由唯一的散列标识。给定散列后,文件日志可以重建所请求的文件版本。Remotefilelog扩展用具有相同接口的替代实现替换文件日志。它接受哈希,但不是从本地数据重建文件的版本,而是从本地缓存或远程服务器获取该版本。当我们需要向服务器请求大量文件时,我们会进行大批量操作,以避免大量请求的开销。
Hgwatchman和remotefilelog扩展一起提高了我们开发人员的源代码控制性能,让他们可以花更多的时间来完成任务,而不是等待他们的工具。如果您部署了大量的分布式修订控制系统,我们建议您查看一下。它们为我们的开发人员带来了改变,我们希望它们对您的开发人员也有价值。