从25分钟到7分钟,我们用了这些方法提升Rails CI的效率

2020 年 6 月 18 日

从25分钟到7分钟,我们用了这些方法提升Rails CI的效率

最近,我们在 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 个步骤:


让它跑起来(Make it work)


让它走上正轨(Make it right)


让它跑得更快(Make it fast)


“让它跑起来”指的是做出不会随便崩溃的软件。在这一步代码可能晦涩难懂,但足以为客户提供价值,并且通过了测试,让我们能信任它。没有测试,就很难判断“它能行吗?”


“让它走上正轨”指的是要让代码可维护,且易于更改。代码不仅能在计算机上运行,更要让人容易理解。新来的工程师可以轻松向代码添加功能,代码中的缺陷也应该很容易隔离和纠正。


“让它跑得更快”指的是要提升软件性能。为什么它会是最后一步呢?对于像 Gusto 这样的金融科技公司来说,如果只关注速度却无视质量,那么我们的客户和我们自己就离破产不远了。并非每段代码都需要优异的性能,如果一段代码每天可能只执行一次,那么就算它有"高性能"水平,却难以阅读和理解,那也是一段失败的代码。


我们把这套原则应用在 CI 套件的提速优化过程中。


让它跑起来


消除不可靠测试


首先需要做的事情是消除测试套件中的不可靠测试(test flakes)。不可靠测试(flaky test)指的是结果不确定的测试,它有时会通过,有时会失败。速度飞快但不可靠的测试套件并不能让你确信代码可以正常运行,这只是在抛硬币赌运气而已。


为了让一个规模庞大的工程团队消除不可靠测试,我们采用并执行了以下政策:


在 master 分支上所有失败的测试都将视为不可靠的。这些测试将标记为已跳过(skipped)。负责不可靠测试的团队可以在空闲时修复它们并取消跳过标记。


这个做法不仅能让测试套件一直亮绿灯,同时也让各个团队决定何时编写更多确定性测试。他们可以立刻开始编写,也可以选择等到再次处理这个功能时再行动。这种方法减少了一个团队的不确定测试给其他团队带来的损害。


当然,这种方法也存在质疑,“如果我们跳过了一项重要的测试该怎么办?”是最常见的问题。没错,这个问题很重要,但我们需要搞清楚问题的背景。一个测试之所以会被标记为已跳过,是因为它会随机失败,首先要考虑的是我们对这个测试和功能到底有多大的信心。很多时候,测试会出现不可靠情况是因为生产环境中的确存在错误!


通过这种方式,我们在主分支上的构建绿灯率从约 75%增至 98%!


让它走上正轨


回到默认状态


随着时间的流逝,我们逐渐偏离了运行 RSpec 测试的默认路径。遵守默认值是很难的。下面是 RSpec 测试的一些默认值:


  • 在各个测试用例之间重置状态。这样可以确保测试是可重复的、确定性的,并且不会相互依赖。

  • 测试执行是随机的。这样可以确保测试之间不存在相互依赖,帮助避免测试污染。

  • 测试文件使用 Rails 自动加载器。这意味着我们仅加载应用程序所需的部分,而不是程序整体,可以帮助避免不完整的测试设置。


重新采用这些默认值的过程并不轻松。确保每个测试用例都重置其状态(数据库、Redis 值、缓存等),都会带来新的不可靠测试。根据其性质,我们可以修复更改或将之前正常的测试标记为不可靠。


我们慢慢重新引入了 RSpec 默认值,这为测试提速奠定了基础。


让它跑得更快


引入测试时间上限


我们的测试是不平衡的。有些测试文件只需几毫秒就能执行完毕,还有些则需要花费数十分钟时间。耗时几分钟的测试是集成测试,涉及我们应用程序中最重要的一些流程。我们希望这些测试的速度能更快,但并不想移除它们。


因为测试套件是分布在多个节点上并行执行的,所以很快就遇到了测试提速的瓶颈。


我们的测试套件速度取决于最慢的测试文件,因此实施了一项新政策:


任何测试文件的执行时间都不能超过 2 分钟。


这个门槛是凭空拉出来的,但似乎很实用。我们只有 40 多个耗时超过 2 分钟的文件。


确定界限之后,我们开始处理速度缓慢的测试,试图让它们通过新的门槛,之前 40 个文件的时间都降到了阈值以下。之后,每个团队都有责任确保其测试文件的执行时间不超过 2 分钟,而执行时间超过 2 分钟的测试文件会被标记为已跳过。


根据最坏情况来平衡测试


现在我们有了一个可靠的测试套件,只是速度很慢,它可以按任何顺序执行测试,但是将测试分配给节点的方法是随机的。有些节点只需几秒钟就完成了,而另一些节点则需要数十分钟。我们怎样才能让它们平衡呢?


我们面临的最后一个问题是测试平衡。我们在这一步评估了两种解决方案:


  1. 开发一个队列,以在节点准备就绪后为其输入测试用例。虽然这种方案原理上没问题,但 RSpec 需要对框架做大幅更新才能兼容这种方案。此外,它在所有各不相同的并行作业之间引入了共享状态。

  2. 在一次 CI 流程开始时在一个数据库中记录测试时间,将测试分为不同的桶,让所有分组都有相同的长度。


我们采用了记录与分桶的方法将测试分配到各个节点上,因为它非常适合 knapsack。测试运行期间,这种方法也不会在许多不同的并行作业之间共享状态。这是很重要的,因为一个共享队列可能有数百个节点,每个节点每秒为一个构建可以请求数千次工作。


我们建立了一个 MySQL 实例来记录所有文件的测试时间。在每次 CI 流程开始时,它会根据每个测试文件的第 99 个百分位时间生成一个 knapsack 文件。在每次 CI 流程结束时,它将上传新的结果。


为什么是第 99 个百分位?由于我们在共享硬件(AWS)上运行 CI,因此无法控制基础架构,各个测试文件的测试时间会大相径庭。我们无法将这些波动与使用的 EC2 实例类型,或者其他任何可以衡量的参数关联起来。


我们没有进一步完善构建基础架构,而是让系统具备了弹性。我们使用第 99 个百分位来组织测试,从而保证了测试的性能表现有一个下限,而不是在获得较好的平均性能时却存在明显偏低的个例。即便底层硬件发生变化或基础架构层出现故障,CI 管道依旧能保障可预期的性能水平。


这套策略实施之后,我们就有了一个自平衡的系统。测试越多,系统也就越平衡。如果某些测试随着时间的推移变慢,则测试桶也会随之调整平衡状态。


提升并行度



现在到了有意思的地方:让测试速度真的变快。


这里的主要做法是增加并行度。项目开始以来,我们已经从 40 个并行作业增加到了 130 个。这稍稍增加了成本,但大幅提升了 CI 的运行速度。在 Gusto,我们使用 Buildkite 作为 CI 基础架构,但这种并行化的理念适用于所有主流 CI 产品。


虽然我们将并行度提高到了 3 倍以上,但 CI 费用却没有随之线性增长。为什么?因为我们更好地利用了已有的 CPU 时间,通过在各个节点之间平衡作业,总 CPU 时间并没有变化,但是实际运行时间大幅缩短了。


总结


在过去几个月中,我们在一点点让 Gusto 主要应用程序的 CI 管道变得更坚实可靠,而且速度更快。


这种改进依旧是一项日常工作。在出现不可靠测试时我们还是会跳过它们,或者寻找新的优化策略来加快构建速度。无论你们现在使用的是什么技术,我们都希望这篇文章可以为你们的团队提供一个路线图参考,帮助改进你们的 CI 管道和软件发布架构。


原文链接:


From 25 Minutes to 7 Minutes: Improving the Performance of a Rails CI Pipeline


2020 年 6 月 18 日 09:142191

评论

发布
暂无评论
发现更多内容

Java 25周年:MovedByJava之观点

范学雷

Java 架构 编程语言

JavaScript 基础拾遗(一)

吴昊泉

Java 学习 文章收集

SQLite是什么

这小胖猫

sqlite 数据库 RDBMS 存储

SpringBoot瘦身

JFound

Spring Boot sprnig

程序员需要了解的硬核知识大全

cxuan

Java c 计算机基础

Enhanced Github:一个 GitHub 专用的好插件

非著名程序员

GitHub 程序员 效率工具

职场“潜”规则

俊毅

个人成长 职场 新人 人才培养 能力模型

如何做好 To B 的 SAAS 服务

路边水果摊

SASS 企业 服务

ARTS_20200520

凌轩

Java ARTS 打卡计划

Android | Tangram动态页面之路(五)Tangram原理

哈利迪

android

往日之歌

彭宏豪95

竟然有人想看我的「日记」,满足一下大家

非著名程序员

学习 程序人生 提升认知

Redis 命令执行过程(下)

程序员历小冰

redis 源码分析

kotlin 200行代码开发一个简化版Guice

陈吉米

Java kotlin guice ioc mynlp

企业数字化转型:用 SpreadJS 打造互通互链的电力系统物联网

Geek_Willie

数字化转型 SpreadJS 电力

nginx 概念及上手

HelloZyjS

Elastic Stack 系列专辑

Yezhiwei

elasticsearch Logstash Kibana ELK Elastic Stack

2020年全球经济萎缩,火花国际PLUS逆袭而来闪耀数字经济

极客编

Spring Security 如何将用户数据存入数据库?

江南一点雨

Java spring Spring Cloud Spring Boot spring security

JVM源码分析之synchronized实现

猿灯塔

推动敏捷,就是推动软件业变革

盛安德软件

敏捷 推动软件业变革

敏捷为什么会失败之「PA-SA-WAKA-DA」理论

Worktile

Scrum 敏捷开发 Agile

Redis6.0 多线程源码分析

代码诗人

redis 源码 技术 线程模型

万字长文带你看懂Mybatis缓存机制

程序员小岑

Java 源码 技术 mybatis

回“疫”录(22):我以为结束了,其实才开始

小天同学

疫情 回忆录 现实纪录 纪实

Django的ListView超详细用法(含分页paginate功能)

Young先生

Python django ListView 分页

深入剖析ThreadLocal原理

JFound

Java

我的编程之路-4(进阶)

顿晓

进阶 看书 编程之路

当我们持续感觉很糟糕要怎么办

七镜花园-董一凡

写作 生活质量 情感

关于架构的几件小事:System context

北风

系统架构 系统性思考 架构师 系统上下文 极客大学架构师训练营

为提升网点业务员效率,我们做的事情。

黄大路

商业

从25分钟到7分钟,我们用了这些方法提升Rails CI的效率-InfoQ