在过去的几年里,优步经历了一段高速发展期,服务范围遍及全球550多个城市。为了跟上节奏,我们的移动团队也必须发展壮大。2014年,我们只有十几名移动工程师致力于iOS应用。今天,我们的移动团队有数百人。因此,我们的移动工装经历了重大的变化,以满足这个更大、更动态的团队的需求。
在去年九月的@Scale会议上,我们展示了优步工程公司是如何从早期发展起来的。在这里,我们将更深入地关注为什么我们最终不得不切换到单个移动单块存储库(monorepo)——乍一看,似乎与我们的单片迁移到一个MicroService基础设施建筑——以及这一变化优步的移动工程为了更好。
Monorepo之前
让我们回过头来看看在“独角兽”出现之前,优步的iOS开发世界的状况。在iOS上,我们只依赖于一个开源工具CocoaPods来构造我们的应用程序。CocoaPods集包管理器、依赖解析器和集成工具于一身。它允许开发人员快速地将其他库集成到他们的应用程序中,而不需要在其中配置复杂的项目设置Xcode。CocoApods还通过针对依赖图的循环来解析依赖性,这些图形可以在编译时导致问题。由于它是Cocoa最受欢迎的包装经理,因此我们依赖的许多第三方框架仅通过Cocoapod获得。此外,Cocoapods的人气意味着该工具的问题通常通过其贡献者团队迅速运行,或通过其贡献者团队迅速固定。
虽然我们的团队和项目都很小,但CocoaPods为我们提供了很好的服务。在优步的最初几年里,我们用一个共享库和一些开源库来构建我们的大多数应用程序。依赖关系图通常很小,所以CocoaPods可以非常快速地解决我们的依赖关系,并且为我们的开发人员带来最小的痛苦。这让我们能够专注于创造产品。
模块化CodeBase.
到2014年底,优步工程共享库已经成为缺乏任何具体组织的代码的垃圾场。因为所有的东西都可能只需要导入一个,所以我们的共享库允许看似独立的组件和类相互依赖。那时,我们已经积累了好几年的关键任务代码,没有进行单元测试,API也没有很好地老化。我们知道我们可以做得更好,所以我们开始努力模块化我们的代码库,同时重写我们的应用的核心部分(如Networking, Analytics等)。
为此,我们将应用程序的所有关键部分组织成构建任何Uber应用程序可以使用的构建块。我们称这些框架的模块'且每个模块都在其自己的存储库中。此模块化允许我们在需要时旋转快速原型应用程序,以及Bootstrap实际生产软件。例如,当我们决定旋转时UberEATS进入它自己的独立申请在2015年底,Ubereats团队严重利用我们建造的新模块。工程师能够花费大部分时间在产品上工作,而不是平台要求。例如,我们建立了一个UI工具包,实现了在我们的移动应用程序中使用的公司中设计师设计的排版,配色方案和常见UI元素。
在2015年初,我们有五个模块。截至2017年初,我们现在有超过40个应用程序之间共享的库,从框架级库(如网络和日志)到特定于产品的库(如映射和支付)。这三个应用程序(Rider、Driver和EATS)都依赖于这种共享基础设施。模块中的Bug修复和性能改进立即反映在使用它们的应用程序中——这是这种设置的巨大好处。总的来说,我们的模块化工作取得了重大成功。
但随着我们从五到四十个模块中缩放,我们遇到了一些问题。我们意识到,由于我们拥有的模块和相互依赖性,我们与Cocoapods进行了艰难的时间。与此同时,超过150名工程师加入了我们更广泛的IOS团队,这意味着我们的应用程序和模块处于不断发展的状态。
改变的时候了
随着公司继续增长,我们的工程需求开始发生变化。一旦我们开始将许多库集成到我们的应用程序中,我们就会迅速达到Cocoapods依赖性分辨率功能的限制。起初,我们的圆荚体安装时间不到10秒。然后,他们计算了分钟数。我们的依赖关系图是如此复杂,以至于工程师们每天都要花上几个小时等待CocoaPods解决这个图并集成我们的模块。这种时间浪费对我们来说更糟持续集成基础设施。
我们也感受到了多存储库方法的压力。每个模块都位于自己的存储库中,并且任何给定的模块都可能依赖于许多其他模块。因此,一个模块内的更改需要在更改出现之前更新应用程序的Podfile。大的破坏性更改需要更新所有依赖于发生更改的模块。
因为我们需要对所有这些模块进行版本化以集成它们,所以我们采用了语义版本化约定来标记我们的模块。虽然语义版本控制本身是一个简单的概念,但在现实中,不同的编译器设置可能会造成不同的破坏。
结果,在给定模块中的看似无害的代码更改可以在其他受依赖模块或应用程序中引入错误(或警告,我们将其视为CI的错误)。例如,考虑以下代码片段(一些用于简洁的样板):
公共枚举kittentype {
例常规
情况下的小鬼
}
公共协议KittenProtocol {
public var type: kittenttype {get}
public var name: String {get set}
}
public struct Kitten: KittenProtocol {}
公共协议小猫{
var小猫:Set<小猫> {get}
func包含(kitten ofType: KittenType) -> Bool
}
类中添加一个新属性KittenProtocol.不应该在模块外引发错误。但也有可能,这取决于该协议的访问控制级别。任何人都可以遵守协议,如果我们让它公开访问我们的模块(让我们称这个模块' ubercats ',因为这似乎易于)。然后,添加新属性将是一个断开的变化,因为协议中的属性必须在其符合类或结构中实现。
甚至为...添加新案例KittenType枚举构成该设置中的断开变化。同样,由于我们制作了这个枚举公众,任何使用它的现有交换机语句都会为未处理的新的遗失案例产生编译器错误。
上述问题是最小的,可以通过Uberkittens模块的任何消费者轻松解决。但是在语义版本化世界中使这些变化安全的唯一方法是使修补程序包含一个主要版本的凹凸。我们有数百名工程师以及在任何一天发生的数百个变化。不可能抓住每一次潜在的破坏变化。此外,更新您的图书馆取决于您可以在您的代码中引入几十个警告的库。
我们真的希望我们的工程师能够快速行动,做出他们需要的改变,而不必担心版本号。解决版本冲突让开发人员感到沮丧,正如前面提到的,CocoaPods在解决我们现在复杂的依赖关系方面变得非常缓慢。我们也不希望工程师花几天时间更新依赖关系图中的模块,以便在我们发布的应用程序中看到它们的变化。工程师应该能够在尽可能少的提交中做出他们需要的任何和所有更改。
解决方案?单一的存储库。
计划Monorepo
当然,单片存储库不是一个新想法;许多其他大型科技公司已经通过了巨大的成功。在将所有代码放入一个存储库时可以具有其缺点(VCS性能,影响所有目标等的断裂),因此Upsides可能是巨大的,具体取决于开发工作流程。使用MONOREPO,我们的工程师可以在一个提交的一个提交中突破模块中跨越模块跨越的变化。没有任何版本号来担心,解决我们的依赖图会更简单。由于我们是一家拥有数百名具有许多不同团队的公司,我们可以将所有iOS代码集中在一个地方,使得更容易发现。
我们不能错过这些好处,所以我们知道我们需要一个单orepo。然而,我们不确定需要什么工具来处理它。当我们第一次开始模块化工作时,我们考虑过围绕CocoaPods构建一个monorepo。但这就意味着必须为工程师可能提出的代码审查的每个代码更改构建每个应用程序和每个模块。我们想更聪明地只建造什么改变了但是,这将需要将数千小时投入到一个工具中,该工具只能仅智能地重建改变的部件。
幸运的是,有一种工具可以做到这一点(或更多),它叫做Buck。
巴克省了一天
buck是一个构建的工具,用于可以构建代码,运行单元测试和在机器上分发构建工件的单片存储库,以便其他开发人员可以花费更少的时间编译老的代码和更多的时间编写新代码。Buck是为带有小型、可重用模块的存储库而构建的,它利用了所有代码都在一个地方这一事实来智能地分析所做的更改,并只构建新的内容。由于它是为速度而设计的,它也利用了我们的工程师在他们的笔记本电脑中拥有的多核cpu的优势,因此多个模块可以同时构建。Buck甚至可以同时运行单元测试目标!
我们听到了很多关于巴克的好事,但在公开支持IOS和Objective-C项目之前无法利用它。什么是facebook宣布为iOS支持在2015年@Scale会议期间,我们很高兴能够在我们的应用程序中开始测试。
我们的初始测试表明,使用降压可以大大改善CI的构建和测试时间。通常,在使用时XcodeBuild.,最好的方法是在建造和/或测试之前始终清洁。任何给定的CI主机都可以是在提交历史记录中向后(和转发)的构建提交,这意味着缓存将不断处于通量。因为这个,XcodeBuild.缓存可以不稳定(我们的CI稳定性是最优先级)。但是如果您必须在构建之前清洁,则CI作业将不必要地慢,因为您无法逐步构建新的更改。因此,我们的建设时间随着我们的增长而飙升。随着数百名工程师,集体时间丢失等待CI在每天建立代码更改。
Buck用一个可靠的(可选的,分布式的)缓存解决了这个问题。它积极地缓存已构建的工件,并将构建尽可能多的可用内核。当构建目标时,在该目标(或它所依赖的目标之一)中的代码更改之前,不会重新构建目标。这意味着您可以设置一个存储库,工具将在其中智能地确定需要重新构建和重新测试哪些内容,同时缓存其他内容。
我们的CI机器从这种缓存体系结构中获益良多。今天,当一个工程师提出一个需要重新构建的代码变更时,那些构建工件将在该机器和其他机器上的未来构建中分布。工程师还可以通过使用在本地CI上已经构建的构件来为他们的构建节省更多时间。我们最近开放的Buck的HTTP缓存API的实现供团队使用。
Buck还提供了其他好处。我们可以通过使用Buck来生成Xcode项目文件来消除合并冲突和开发人员受挫的常见原因。这使得优步的每个iOS应用程序都可以共享一组公共的项目设置。工程师可以很容易地检查这些设置的任何更改,因为它们是在易于阅读的配置文件中,而不是深埋在Xcode项目文件中。
此外,由于Buck是一个功能齐全的工具,支持构建和测试,我们的工程师可以在不加载Xcode的情况下检查代码的有效性。可以使用一个命令运行测试,该命令在xctool。更好的是,如果我们的工程师想完全放弃Xcode,他们可以打开Xcode核素,将调试支持和自动完成功能添加到原子的文本编辑器。
大移动迁移
我们是如何迁移到Monorepo的?答案是:很多干跑。大部分工作是可重复和确定性的,所以我们写了脚本来为我们做繁重的举重。例如,我们所有的模块都包括一个CocoapodsPodspec.文件。我们发表这Podspec.到一个内部私有Podspecs存储库,Cocoapods在集成时使用。这些PODSPEC可以映射1:1到相应的降压文件(命名“巴克”),所以我们写了一个生成的脚本巴克文件并更换Podspec.。
我们还创建了一个虚拟的monorepo,仅用于测试目的,它使用符号链接模拟了提议的回购结构。这使得我们可以轻松地测试monorepo结构和设置,以及在模块发生变化时更新它们。
然而,我们注意到模块中的更改很快使测试存储库过时。我们降落巴克所有模块中的文件,但在此期间我们的工程师仍然必须使用Podspec.文件。因此,我们需要一种方法来保持Buck文件和podspecs同步。为了做到这一点,我们对CI中的所有模块更改启动测试存储库,并在他们的更改破坏测试存储库中的任何代码或模块时向工程师发出信号。
这种设置帮助工程师们熟悉了即将到来的新的“巴克世界”,同时也保持了巴克文件最新。在迁移前的最后一周,我们也为申请做了这一点,以便工程师知道他们的代码修补程序是否在降压宇宙中打破了应用程序。
构建实际的Monorepo是一个具有挑战性的努力。我们知道我们最终必须创造它,但问题是,何时?最初,我们计划在同一个周末建造它,我们将把所有工具和基础设施移动到新设置。但是,如果我们最早的几个星期,它会稍后节省更多的压力。迁移也是我们可以脚本的东西,因为这些步骤是对所有存储库的重现。我们遵循创建Monorepo的一般步骤是:
- 克隆要合并到临时目录中的存储库。
- 将该存储库中的所有文件移动到MONOREPO中的相应路径。
- 提交更改并将其推向具有特定名称的远程分支。
- 输入MONOREPO并将存储库添加为遥控器。
- 将远程分支合并到MONOREPO中。
- 从已合并的存储库中删除远程分支。
- 确定表示的提交头从被合并的存储库中。提交到一个隐藏文件。我们将在稍后更新monorepo时使用该文件。
- 重复下一个存储库的步骤1-7。
一旦我们创建了MONOREPO,我们必须弄清楚如何在未来几周内保持最新状态。我们通过利用我们在步骤7中创建的文件来完成此操作。由于它代表了头SHA当MONOREPO最后从原始存储库更新时,我们可以每小时运行脚本,从而创建一个上次更新沙来的GIT补丁头,然后在monorepo中的正确路径上应用补丁。
迁移本身非常简单,我们设法在一个周末完成。我们暂时阻止了Git图层的所有存储库,切换了所有CI作业以使用降压命令而不是XcodeBuild.,并删除了所有Xcode项目文件和podspecs。由于过去几个月我们一直在测试项目、CI工作和发布渠道,所以在2016年5月推出monorepo时,我们感到很有信心。
结果:集中所有iOS代码
使用monorepo,我们将所有iOS代码集中到一个地方。我们将存储库组织成这样的目录结构:
├──应用程序
│├──iPhone-Driver
│├──iPhone-eats
│├──iPhone-Rider
├──库
│├──分析
│├──……
│└──公用事业
└──供应商
├──fbsnapshottestcase.
├──……
└──ocmock.
现在,突破API变化分析模块需要在同一提交中更新该模块的所有消费者。巴克只会建立和测试分析以及依赖的任何模块分析。相反,我们不再有任何版本冲突,我们始终知道主是绿色的。
然而,最大的好处来自于切换到Buck缓存:
让建设者构建:我们在XCodeBuild下支配先前更长的建设时间的趋势。
比较xcodebuild构建和巴克构建步骤,我们看到了一个巨大的胜利,因为我们不再在每次CI工作之前清理我们的缓存。通过切换到具有可信任缓存的工具,我们也可以在CI主机之间共享构建的工件。Buck还可以缓存测试结果,所以如果我们知道某个模块已经测试过了,并且不受某个代码更改的影响,我们可以跳过这些测试。
总的来说,我们的迁移是成功的。然而,在转向monorepo的过程中有一些成本是我们最初无法避免的,比如git性能下降,以及当master崩溃时,所有代码都会受到影响。
例如,我们开始看到在CI上传递的完全有效的提交在基于主分支的顶部重新进行时将失败。当我们第一次启动monorepo时,我们必须时刻警惕地恢复失败的提交。在最糟糕的情况下,10%的提交必须被恢复,这将导致开发人员损失数小时的时间。为了解决这个问题,我们引入了一个名为Submit Queue的系统,在存储库合并和推送操作之间(在Uber我们称之为“土地”,因为我们使用巧匠)。当工程师尝试降落他们的提交时,它会在提交队列中排队。此系统一次拍摄一个提交,res resprate反对主设备,构建代码并运行单元测试。如果没有休息,那么它就被合并到了主人。随着提交队列到位,我们的总成功率跃升至99%。
结论
转向新的工具链为我们提供了试验新想法的基础,从而更快更好地构建我们的应用程序。最近,我们的团队正在与Facebook的Buck团队合作,为Buck添加完整的Swift支持,以及对macOS和iOS动态框架的支持。这使得我们可以利用Buck的优势,同时将我们的代码库转移到Swift时代。
将我们的代码移动到一个地方,我们的应用程序只是我们走向现代化发展方式的几个步骤。除了我们,我们的Android团队也采用了Buck他们也看到了快速、可重复构建的好处。但我们不会止步于此;我们的团队致力于在未来的一年里为Buck的iOS支持添加更多的功能,我们也计划继续增加我们的开源努力贡献返回更广泛的移动社区。
想加入我们的团队吗?我们正在招聘!
照片标题信用:“移动reedbuck.小牛跑独奏“由Conor Myhrvold,南琅世界公园,赞比亚。








