在汽车或航空航天等传统工业中,工程师首先设计产品,制造设施根据设计生产汽车或飞机。在软件开发中,构建系统类似于使用源代码的制造工具,并将其转换为服务,工具和应用程序。除了促进软件编译和链接外,构建系统通常需要生成代码,下载外部包,或构建不同的安装包。一些构建系统还可以管理工具,例如编译器,链接器和代码生成器,使构建工件较少依赖于其本地环境。当优步开始杠杆化以发展我们的后端服务时,我们使用了流行的开源构建系统制作与Go的默认构建系统相结合去建设。
在我们的移动安卓和iOS.在优步项目更有效的monorepo模式,Go开发者经验团队对Go项目做出了类似的举动,发现Make和Go构建不再满足我们的需求。我们最终决定使用Bazel,它为Go语言提供了很好的支持,并因其活跃的开源社区的不断贡献而得到加强。
有很大的部分我们的技术堆栈在Go中开发的Uber的Go monorepo可能是在Bazel上运行的最大的Go知识库之一。我们注意到我们可以改善和促进巴泽尔生态系统的领域,加强巴泽尔规则的生成和整合巴泽尔sendraudue.优步的系统,以确保monorepo的主分支总是能够成功构建和测试。
我们希望我们用Bazel构建大型存储库的经验以及我们对开源Bazel生态系统的贡献能够帮助其他工程团队使用Bazel构建他们的源代码存储库。
超级monorepo去吧
优步的大部分后端服务和库都是用Go编写的。在我们决定建立一个Go monorepo之前,Uber的工程师在许多小而孤立的存储库中开发了这些Go项目(我们开放的一些人)。我们在2018年初推出了Go monorepo,并在早期采纳者项目中看到了构建效率的立即上升。随着Go monorepo的成熟,我们将越来越多的项目转移到它上面,并且使用迅速扩展,如下图1和图2所示:
在写这篇文章的时候,我们的Go monorepo有超过7万个文件。由于我们一般不提交生成的代码,这些Go文件主要是手工编写的。Go monorepo的大规模增长鼓励我们评估新的构建解决方案,如Bazel,以满足我们的开发需求。
Bazel的设计是为了大规模工作,并支持跨分布式基础设施的增量密封构建,这对Uber庞大的代码库是必要的。用官方的Bazel去规则集,我们能够管理Go Toolchain和外部库,而无需根据本地安装的Go Toolchain和外部库。还有一个官方挥之克尔项目,瞪羚,我们使用它来生成Go和协议缓冲区规则。有了Gazelle,我们能够在我们的Go monorepo中以最少的人力投入为大多数围棋包生成Bazel规则。Gazelle也可以导入的版本去模块这样我们就可以方便有效地构建外部库。
有了Bazel的远程缓存,我们的构建服务器也可以共享它们的构建工件。只有当包或其依赖项发生了变化时,才会构建和测试包。
改善Bazel.
开箱即用的软件解决方案很少适用于像Uber的Go monorepo这样庞大而复杂的代码库。我们已经添加并改进了Bazel以更好地满足我们的需求,改进了规则生成器,开发了几个新的Bazel规则和特性,以及用于大规模构建大型代码库的工具。在这个过程中,我们修复了Go和Bazel开源项目中的许多bug。
巴泽尔规则生成
Bazel要求使用构建规则明确定义所有构建目标。每个Go Package至少有两个构建目标,一个要将其构建为库,因此其他软件包可以导入它,另一个软件包将另一个包运行该软件包的单元测试。在存储库中创建和维护大量构建规则,优步的大小是繁琐而易于出错的任务。幸运的是,可以从其源代码推断出Go和协议缓冲区的大多数构建配置,这会使机会自动生成这些凸巨琴规则。这是瞪羚发挥的地方。
如前所述,我们的Go Monorepo可能是最大的Go储存库,以至于到目前为止使用Bazel和Gazelle,导致复杂的情景,Bazel和Gazele的设计者在使用规模使用软件时不一定预见。我们与开源社区密切合作,删除这些障碍,修复错误,并添加一些新功能。
在Bazel,外部Go模块下载使用go_repository规则。瞪羚在Go.mod和go.sum文件中为每个模块生成一个这样的规则,由此管理去工具链。在Go monorepo中,我们有超过一千个外部模块。
作为我们的Go Monorepo的开发的一部分,优步贡献了巨大的一些功能,以改善它如何生成和管理所产生的go_repository规则。例如,Gazelle只能生成allgo_repository巴泽尔的规则工作区文件,也包含手工编写和维护工作空间的规则和宏。必须在生成之前放置其中一些手动规则和宏go_repository规则,其他的在生成的规则之后。然而,Gazelle只能附加newgo_repository规则到最后工作区文件。我们添加了一个功能(# 480那# 493,这样它就可以将go_repository规则写入一个单独的宏文件,并将其加载到工作区文件。结果,所有生成的规则都保留在外部工作区文件,使这个文件更小,更容易维护。
我们的Go monorepo还允许工程师添加或删除外部模块。当一个模块被移除时,Go工具链会将其从Go中移除。国防部和走。和文件。然而,瞪羚却无法清理不必要的东西go_repository规则。我们添加了瞪羚的选项,不需要修剪go_repository规则(#514)。之后go_repositoryrule下载一个Go模块,它调用Gazelle来生成Bazel规则来构建该模块。我们还添加了参数(# 603那#649)go_repository规则,以便在外部模块中配置Gazelle的行为。这些改进,以及我们的团队多年来贡献的许多较小的功能和bug修复,将两名Uber工程师推到了Gazelle网站的第二名和第三名列表的贡献者截止到2020年4月。
瞪羚旨在支持许多不同语言的Bazel规则。瞪羚的官方扩展可以为Go和Protocol Buffers生成Bazel规则。但是,优步利用我们的Go Monorepo中的许多其他规则,包括规则Apache节俭那thriftrw那Apache Avro,GoMock。开源社区开发了一些这些规则,而其他人则在Uber内部开发。我们还开发了几个瞪羚扩展,以涵盖这些额外的规则,并计划在不久的将来开源我们的新规则和瞪羚扩展。
寻找和建造改变了目标
让我们的Go monorepo的总分支保持在绿色状态,这意味着主分支上的所有代码都可以在任何时候成功编译和测试,我们在提交到主分支之前执行一系列检查。这些检查包括构建和测试在提交中更改的所有包,以及传递的所有依赖包。根据提交所影响的包的数量,检查的时间从几分钟到几小时不等。
如果我们按顺序提交,每个提交都必须等到所有以前的提交都提交后才被检查,这可能会导致较长的着陆时间。更复杂的是,我们的monorepo越大,提交的次数就越多,它们会堆积起来并创建更长的队列。我们知道,最终提交的提交速度会太快,以致于我们无法按顺序进行检查。
为了跟上我们的高承诺率,Uber工程师使用sendraudue.在防止代码冲突时并行检查和登陆。为此,Submitueue需要知道受给定提交影响的构建目标列表。Uber使用的其他存储库buck因为他们的构建系统能够运行“巴克目标- 瞄准目标 - 哈希命令,找出哪些目标的哈希值由于提交而发生了变化,并将目标列表传递给SubmitQueue。不幸的是,即使Bazel知道它需要在内部重新构建哪些目标和操作,它也不会从命令行接口公开操作或目标的散列键。
在和谷歌的巴泽尔团队讨论了这个问题后,双方都在Github线下,我们决定在巴泽尔之外开发我们的解决方案。生成的工具遍历Bazel的构建图,通过组合规则定义、属性、输入文件和它所依赖的其他目标的散列,计算图中每个构建目标的散列。使用每个构建目标的散列,我们可以识别受提交影响的构建目标列表,并将该列表传递给SubmitQueue。我们打算在未来开放这个工具的源代码。
一旦Subsuiteue知道哪些构建目标受到影响,它需要调用Bazel来构建并测试这些目标,以确保将提交到主分支机构的安全性安全。当提交升级核心库时,目标列表可能非常大,例如,升级GO RULEAT.版本。随着MONOREPO的增长,构建目标列表增加到它变得太长的点,无法通过BAZEL的命令行界面。讨论问题后Github,我们最初能够通过使用传递构建目标列表来绕过它特定构建配置而不是命令行。随着Monorepo继续增长,这是解决方法失败的一次。
最后,我们的贡献一个特性到Bazel,以便它可以从文件中读取构建目标列表。该特性已被接受并合并到Bazel的主存储库中,从Bazel 3.1开始就可用了。
优步更好的Bazel集成
虽然我们成功地采用Bazel作为Uber的Go monorepo的构建系统,但仍然有很多工作要做。例如,目前的Go ide并不像我们希望的那样支持Bazel。我们发现ide无法定位在构建时生成和下载的Go包和模块,我们正计划与开源社区合作来缩小这一差距。
我们的Go monorepo也是最早使用Bazel的优步存储库之一。我们计划与优步的其他团队分享我们的经验和技术,帮助他们迁移到Bazel作为他们的构建系统。我们构建的许多工具和功能都有这样的愿景,它们设计得足够通用和可扩展,可以在我们的Go monorepo甚至Uber之外重用。






