最近,我们在Gusto中??创造了一个新记录:6分29秒。这是我们为Gusto最大的应用程序之一——Rails单体运行测试套件所花费的时间。6分29秒的时间是公司持续集成(CI)流水线上线以来最快的。上一次CI套件运行这么远时,公司还很小,现在我们在世界各地有数百名工程师使用这个Rails单体,为美国1%的小型企业提供支持。对于Gusto而言,高速CI管道不仅仅是为了展示,我们将其视为一种竞争优势,代码部署得越快,客户的业务就会越快。随着CI速度的提高,工程师的工作效率也会提高,并且CI时间每损失一分钟,Gusto发现每位工程师每周的拉取请求增加2%。我们的目标只是让我们的测试套件的速度成为一个参数的函数:我们愿意花多少钱?将基础架构简化到这个级别可以更容易地进行成本效益分析,例如如果您想将构建速度从7分钟提高到5分钟,则需要1美元。这篇文章描述了我们如何加速我们的测试套件,涉及一个Rails单体和一个主要用React编写的JavaScript单页应用程序(SPA),这些课程适用于所有较慢的测试套件。我的同事Kent说构建软件有3个步骤:让它工作让它正确让它快速让它快速我们的目标是制作不会崩溃的软件。代码此时可能晦涩难懂,但足以为客户提供价值,并且它通过了测试,因此我们可以信任它。没有测试,很难判断“它会起作用吗?”“正确”意味着使代码易于维护和更改。代码不仅要能在计算机上运行,??而且要便于人们理解。新工程师可以轻松地向代码中添加功能,并且代码中的错误应该易于隔离和更正。“Makeitrunfaster”指的是提高软件性能。为什么是最后一步?对于像Gusto这样专注于速度而忽视质量的金融科技公司,我们的客户和我们自己离破产不远了。并不是每段代码都需要很大的性能,如果一段代码很可能每天只执行一次,即使它具有“高性能”级别,但难以阅读和理解,那么它就是一段失败的代码代码。我们在优化CI套件速度的过程中应用了这套原则。1.开始运行要消除碎片,您需要做的第一件事就是从您的测试套件中消除测试碎片。片状测试是一种结果不确定的测试,有时会通过,有时会失败。一个快速但不可靠的测试套件不会让你相信你的代码会工作,它只是一个掷硬币。为了让大型工程团队消除不可靠的测试,我们采用并执行了以下政策:所有在master分支上失败的测试都被认为是不可靠的。这些测试将被标记为已跳过。负责不可靠测试的团队可以在闲暇时修复它们并删除跳过标记。这不仅使测试套件保持绿色亮起,而且还让团队决定何时编写更具确定性的测试。他们可以立即开始写作,也可以选择等到他们再次处理该功能。这种方法减少了一个团队的非确定性测试对其他团队造成的损害。当然,也有人对这种做法存有疑虑,“万一我们跳过了一个重要的测试怎么办?”是最常见的问题。是的,这个问题很重要,但我们需要了解问题的上下文。一个测试之所以会被标记为跳过,是因为它会随机失败,首先要考虑的是我们对这个测试和功能的信心有多大。很多时候,测试失败是因为生产中确实存在错误!这样,我们在master分支上的构建批准率从大约75%上升到了98%!2.让它走上正轨回到默认值随着时间的推移,我们已经偏离了运行RSpec测试的默认路径。坚持默认设置很难。以下是RSpec测试的一些默认值:重置测试用例之间的状态。这确保了测试是可重复的、确定性的并且不相互依赖。测试执行是随机的。这确保了测试之间没有相互依赖性,并有助于避免测试污染。测试文件使用Rails自动加载器。这意味着我们只加载需要的应用程序部分,而不是整个程序,这有助于避免不完整的测试设置。恢复到这些默认值的过程并不容易。确保每个测试用例都重置其状态(数据库、Redis值、缓存等)会引入新的不可靠测试。根据其性质,我们可以修复更改或将以前工作的测试标记为不可靠。我们慢慢地重新引入了RSpec默认值,这为测试加速奠定了基础。3.让它运行得更快引入测试时间上限我们的测试是不平衡的。一些测试文件在几毫秒内执行,而另一些则需要数十分钟。需要几分钟的测试是集成测试,涉及我们应用程序中一些最重要的进程。我们希望这些测试更快,但不想删除它们。由于测试套件分布在多个节点并并行执行,很快就会遇到测试提速的瓶颈。我们的测试套件仅与最慢的测试文件一样慢,因此我们实施了一项新政策:任何测试文件的执行时间不得超过2分钟。门槛是凭空拉出来的,但看起来还可以用。我们只有40多个文件需要超过2分钟。界限确定后,我们开始进行慢速测试,试图让它们通过新的阈值,而之前的40个文件都低于阈值。然后每个团队有责任确保他们的测试文件执行时间不超过2分钟,执行时间超过2分钟的测试文件将被标记为已跳过。最坏情况平衡测试现在我们有了一个可靠但缓慢的测试套件,它可以按任何顺序执行测试,但是将测试分配给节点的方法是随机的。有些节点在几秒钟内完成,而其他节点则需要数十分钟。我们怎样才能使它们平衡呢?我们面临的最后一个问题是测试天平。我们在这一步评估了两种解决方案:开发一个队列,以便在节点准备就绪时为其提供测试用例。虽然这个方案原则上没问题,但RSpec需要对框架进行实质性更新才能兼容这个方案。此外,它在所有不同的并行作业之间引入了共享状态。在CI过程开始时将测试时间记录在数据库中,将测试划分到不同的桶中,并让所有组具有相同的长度。我们使用logging和bucketing的方式将测试分发到各个节点,因为它非常适合knapsack(https://docs.knapsackpro.com/ruby/knapsack)。这种方法在测试运行期间也不在许多不同的并行作业之间共享状态。这很重要,因为共享队列可能有数百个节点,每个节点每秒可以请求工作数千次来构建。我们搭建了一个MySQL实例来记录所有文件的测试时间。在每个CI过程开始时,它会根据每个测试文件的第99个百分位时间生成一个背包文件。在每个CI过程结束时,它将上传新的结果。为什么是第99个百分位数?由于我们在共享硬件(AWS)上运行CI,我们无法控制基础设施,并且测试时间因测试文件而异。我们无法将这些波动与所用EC2实例的类型或任何其他可测量参数相关联。我们没有进一步完善构建基础设施,而是让系统具有弹性。我们使用第99个百分位来组织我们的测试,这确保了测试性能的下限,而不是实现更好平均性能的个别情况。即使底层硬件发生变化或基础设施层出现故障,CI管道仍能保持可预测的性能水平。这个策略实施之后,我们就有了一个自我平衡的系统。测试越多,系统就越平衡。如果一些测试随着时间的推移变慢,测试桶会相应地重新平衡。改进并行性现在是有趣的部分:使测试实际上更快。这里主要的做法是提高并行度。自项目启动以来,我们已经从40个并行作业增长到130个。这会略微增加成本,但会使CI运行得更快。在Gusto,我们使用Buildkite作为我们的CI基础设施,但这种并行化理念适用于所有主要的CI产品。虽然我们将并行度提高到3倍以上,但CI成本并没有随之线性增长。为什么?因为我们更好地利用了我们拥有的CPU时间,通过在节点之间平衡作业,总CPU时间没有改变,但实际运行时间却大大减少了。4.总结在过去的几个月里,我们一直在使主要Gusto应用程序的CI管道更加稳固、可靠和更快。这种改进仍然是一项日常工作。我们仍然会在不可靠的测试出现时跳过它们,或者找到新的优化策略来加速构建。无论您今天使用什么技术,我们都希望本文能为您的团队提供路线图参考,帮助改进您的CI管道和软件发布架构。
