Maven多模块构建加速方案      

      拥有大量 Maven 模块会减慢项目构建和测试运行速度。为了维护多模块项目结构并快速运行测试,我们开发了一个新工具--Maven 模块合并器,它帮助我们将某些构建时间从 50 分钟缩短到 12 分钟。在本文中,我将详细介绍 Maven 模块合并器帮助我们解决了哪些问题,并分享一些有关其创建的细节。

      通过 Autotests 项目中的 53,000 多个测试,我们确保了我们的产品是一流的。其中 16,000 个是 REST API 测试,其余 37,000 个是 Selenium 测试。大约 30 个 Scrum 团队每月新增 1000 个测试,并不断改进旧测试。在自动测试项目中,我们使用 Java 17JUnit 5 和 Maven 作为项目构建工具。我们使用 HtmlElements2 库编写 Selenium 测试,并使用 Retrofit 库测试内部 API。所有测试分布在 250 个 Maven 模块中,位于一个包含 160 多万行代码的项目中。Maven 模块是一个子项目,您可以独立于其他模块使用它(例如,运行测试或编译代码)。


为什么我们决定使用多模块项目结构?
      与单模块项目相比,多模块项目结构有很多优点。让我们来了解一下:

1. 多模块结构允许您从逻辑上划分产品不同组件的测试。

    将代码拆分成包可以达到同样的效果,但这种方法无法提供多模块项目的其他优势。

2. 将代码划分为模块有助于避免类的误用

     如果所有类都在同一个模块中,开发人员就有可能犯错,误用名称相似的类。让我们来看一个例子。在我们的例子中,类 TSDateInputSteps(Typescript 版本)、DateInputSteps(Dart 版本)和 DateTimeInputSteps 位于不同的模块中。这样可以确保不会意外使用一个类代替另一个类。如果开发人员想使用另一个类,他们必须在 pom 文件中明确指定依赖关系,这在代码审查时会很明显。

3. 多模块结构允许您在每个模块中分别运行测试

    要运行某些模块中的测试,您需要使用 -pl - Maven 密钥,它接受一个以逗号分隔的模块列表。当传递一个列表时,只有指定的模块及其依赖项会被编译(而不是编译整个项目),这样可以节省时间。以我的笔记本电脑为例,编译整个项目需要 19 分钟,而编译一个中型模块只需 1 分钟。我的笔记本电脑规格如下 MacBook Pro(16 英寸,2019 年),CPU 2.6 GHz 6 核英特尔酷睿 i7,内存 16 GB 2667 MHz DDR4。

4. 你可以从其他模块中抽象出来,进行修改并添加新的测试

    您可以单独打开一个模块,将其作为集成开发环境中的一个小型独立项目来处理。

5. 将多模块项目拆分成独立项目更方便

    在这种情况下,每个模块都可以成为一个独立的项目。那为什么不直接创建 250 个独立项目呢?我们最初也有过这样的想法,将项目划分为多个模块是一种保险措施,以防万一我们决定分割项目。事实证明,维护大量高度相关的项目是非常困难的--变化无时无刻不在发生,而且不同版本的项目之间经常可能发生冲突。

在模块数量较少的情况下,多模块结构运行良好,但随着时间的推移,模块数量开始达到数百个,我们面临着一些问题:

我们很难管理并行运行测试的线程数量。
每个模块结束后重试测试所花费的时间占整个运行时间的 50%。


线程数量管理问题

大量的测试使我们无法按顺序进行。如果我们全部运行这些测试,就必须等待两个多月才能得到测试结果。因此,我们通常在 TeamCity 中以 80-150 个线程运行测试,具体取决于构建类型。随着模块数量的增加,管理用于运行测试的线程数量会变得越来越困难。如果模块中的测试数少于线程数,那么并行运行的测试数将等于模块中的测试数。 例如,如果我们用 80 个线程运行测试,那么有 10 个测试的模块将只使用 10 个线程。同时,每个模块都将等待前一个模块的测试完成。

image

如果一个模块中有 81 个测试,测试在 80 个线程中运行,那么每个测试只有一个线程。要解决这个问题,可以尝试使用 -T %number% 并行运行模块。这里,-T 是 Maven 的一个参数,用于在可能的情况下并行运行 %number% 模块的构建。

image

不过,这种解决方案也有一些缺点:

在测试数量较少的模块中,我们仍然会有少量测试并行运行(第一和第二模块)。
在测试数量较多的模块中,我们会有太多的线程(第三和第四模块)。

Maven 可能无法始终并行运行模块(由于模块之间的相互依赖性)。
在单模块项目中,这不是问题--我们可以随时轻松调整所需的线程数:

image

耗时的测试重试问题 
      我们使用经过修改的万无一失测试运行器,它可以重试失败的测试。它会收集这些测试,并尝试在所有其他测试之后再次运行它们。如果每个模块中有一个测试失败,那么这些失败的测试将在一个线程中重新启动。与此同时,下一个模块的测试将等待一个测试完成。如果测试都在同一个模块中,那么重试所需的时间就会短得多。

image

事实证明,项目的多模块化会对测试运行时间产生负面影响。但我们并不想放弃多模块项目的优势,也没有将所有模块合二为一。我们有了另一个想法: 如果我们每次在 TeamCity 中运行测试时都将所有模块合并为一个模块,会怎么样呢?这样我们就能在测试开发阶段享受多模块项目的优势,在测试运行阶段享受单模块项目的优势。 新工具的想法很简单:将所有文件复制到新模块中,生成包含所有依赖关系的 pom.xml,然后在新模块中运行测试。我们将此工具命名为 Maven 模块合并器(或简称合并器)。

Maven 模块合并器

让我们来看看合并的实现细节。

合并器的输入是

以逗号分隔的要合并的模块列表
项目路径
要将结果写入的文件路径
操作模式:源代码的源代码或编译后项目的目标代码


既然可以直接合并所有模块,为什么还要传递模块列表呢?我们使用 -pl 参数将模块列表传递给 Maven,这样就可以只运行某些模块的测试。(在每次构建时,我们只希望合并必要的模块)。

合并的算法是这样的

定义要合并的模块列表
复制文件
为合并后的模块创建 pom 文件
将合并后的模块作为子模块添加到根 pom 文件中
将生成的模块写入文件

现在让我们详细分析一下算法的每个步骤。

第1步:确定要合并的模块列表

    起初,我们希望允许合并所有模块,但在实施过程中遇到了一些困难。目前,所有模块的资源文件夹都被合并到一个目录中,但我们的模块在这个目录中存储了不同的配置文件。我们使用 test/resources/allure.properties 文件将 Allure 中的 API 测试与 Selenium 测试分开。我们只合并带有 allure.properties 文件的模块用于 Selenium 测试,其余模块(后台测试和其他)保持原样。在我们项目的 250 多个模块中,只有 8 个模块运行非 Selenium 测试,它们不会被合并。在这一步中,我们将收集具有相同配置的模块,并从模块列表中删除重复的模块。

第 2 步:复制文件

     在这一步中,我们将把已收集模块中的文件复制到项目目录中名为 merged_modules 的新模块中。对于源模式,我们复制 src 文件夹中的所有文件。在目标模式下,我们复制 target/classes 和 target/test-classes 文件夹中的所有文件。如果你有一个已经编译过的项目,并且不想在运行合并后再次编译代码,那么后一种模式可能会很有用。我们保留主分支代码的编译版本,以节省在不同 TeamCity 版本中多次编译相同代码的时间。在我们的项目中,不同模块之间的文件名经常会有交叉,导致复制过程中出现冲突。为了解决这个问题,我们为不同模块中的包赋予了与模块名称一致的唯一名称。

第 3 步:为 merged_modules 模块创建 pom 文件

      要将 merged_modules 文件夹转化为模块,需要为其添加一个 pom 文件。在 merged_modules 模块的 pom 文件中,只有合并模块的所有依赖关系列表。有时,模块可能会依赖于同一库的不同版本。在我们的项目中,所有模块都使用 1.0-SNAPSHOT 版本,因此将它们作为依赖关系使用时不会出现版本冲突。使用第三方库时可能会出现冲突,因此我们要在根 pom 文件中配置它们的版本。

第 4 步:将 merged_modules 模块作为子模块添加到根 pom 文件中

      下一步是将生成的 merged_modules 模块作为子模块添加到根 pom 文件中,这样 Maven 模块结构就正确了,Maven 也能在新模块中运行测试。

步骤 5:将生成的模块写入文件

      最后一步,我们将合并的模块(merged_modules)和所有未合并的模块(modules)写入文件,中间用逗号隔开。这个模块列表稍后将用于传递给 -pl 参数。执行算法后,我们会得到一个包含新模块的项目,新模块合并了大部分模块,另外还有一个包含新模块列表的文件。在远程 TeamCity 代理上对一个拥有 12000 多个文件和 240 多个模块的项目执行这样的算法大约需要一到两秒。为确保不违反协议,我们添加了新的单元测试和 PMD 规则。您可以在我们的另一篇文章中了解如何设置 Checkstyle 和 PMD。

我们如何使用合并

       我们在 TeamCity 构建中使用合并功能。为此,我们创建了一个单独的模板,在其中尝试合并模块。如果出现问题,作为备用选项,我们可以不使用合并器运行测试。

下面是这样一个构建流程图:

image

合并实施结果
因此,我们发现在大量模块上启动的构建速度明显加快。例如,为 138 个模块中的前端组件运行 11,000 个测试的组件测试构建所需的时间仅为之前的一半。

image

有些构建过程会在所有 250 多个模块中运行一组预定义的测试。引入合并功能后,这种构建速度提高了两到四倍。例如,运行所有屏幕截图测试的构建只需 12 分钟,而以前需要 50 分钟。

image

     每天都要部署新的产品功能。在部署之前,我们会运行项目中的所有测试,其中一些测试需要在不同的浏览器中运行。实施合并后,所有测试的总运行时间减少了三分之一以上,从 12.5 小时减少到 8 小时!现在,61000 多个测试Tests的总运行时间为 50 分钟。之所以能做到这一点,是因为我们并行运行了一些构建,每个构建的线程数为 150 个。

image

Maven合并源代码可在我们的 GitHub 上找到。我们希望我们的工具在您的项目中也能派上用场。欢迎您提出意见和想法。祝您使用愉快.


今天先到这儿,希望对云原生,技术领导力, 企业管理,系统架构设计与评估,团队管理, 项目管理, 产品管管,团队建设 有参考作用 , 您可能感兴趣的文章:
领导人怎样带领好团队
构建创业公司突击小团队
国际化环境下系统架构演化
微服务架构设计
视频直播平台的系统架构演化
微服务与Docker介绍
Docker与CI持续集成/CD
互联网电商购物车架构演变案例
互联网业务场景下消息队列架构
互联网高效研发团队管理演进之一
消息系统架构设计演进
互联网电商搜索架构演化之一
企业信息化与软件工程的迷思
企业项目化管理介绍
软件项目成功之要素
人际沟通风格介绍一
精益IT组织与分享式领导
学习型组织与企业
企业创新文化与等级观念
组织目标与个人目标
初创公司人才招聘与管理
人才公司环境与企业文化
企业文化、团队文化与知识共享
高效能的团队建设
项目管理沟通计划
构建高效的研发与自动化运维
某大型电商云平台实践
互联网数据库架构设计思路
IT基础架构规划方案一(网络系统规划)
餐饮行业解决方案之客户分析流程
餐饮行业解决方案之采购战略制定与实施流程
餐饮行业解决方案之业务设计流程
供应链需求调研CheckList
企业应用之性能实时度量系统演变

如有想了解更多软件设计与架构, 系统IT,企业信息化, 团队管理 资讯,请关注我的微信订阅号:

MegadotnetMicroMsg_thumb1_thumb1_thu[2]

作者:Petter Liu
出处:http://www.cnblogs.com/wintersun/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。 该文章也同时发布在我的独立博客中-Petter Liu Blog。

posted on 2023-08-12 12:46  PetterLiu  阅读(452)  评论(0编辑  收藏  举报