基于 MonoRepo 的 Web 端 CI/CD 实践与优化
本文作者 Summer Gan,铃盛 SDET 团队 Team Leader,主要负责前端自动化框架开发、持续集成/持续交付等。文章为作者 7 月 8 日参加 QECon 全球软件质量 &效能大会的演讲分享整理。
分享目录:
01 背景介绍
02 MonoRepo CI/CD 基础建设
03 端到端自动化测试在 CI/CD 中的实践
04 稳定性、可靠性建设
05 总结
01 背景介绍
Monorepo 介绍
为何本文的主题词要加一个 MonoRepo 呢?MonoRepo 又是什么呢?MonoRepo 是来自 Facebook 的工程实践。它的诞生与 Facebook 在快速发展的过程中遇到的一个问题息息相关。随着 Facebook 的发展,逐渐出现了几十个项目工程(Repository,下文简称 Repo),并按多仓库(MultiRepo)的方式存储于代码管理系统中。这些 Repo 之间存在错综复杂的依赖关系。为了开发新功能,团队可能需要在几十个 Repo 之间同步代码,导致产品维护难度升高、开发效率降低;同时,当一个新人加入团队后,往往需要了解这几十个项目就需要花费 3-4 周的时间。更糟糕的是,随着时间的推移,Repo 数量会越来越多,开发成本维护成本越来越高。
2012 年 7 月的一次黑客马拉松中,一位工程师决定做一个新的系统来彻底解决上述棘手的问题。在他的方案中,在代码管理上用 MonoRepo 替换 MutiRepo,移除重复的文件,重新划分模块。后来,为了能够进一步提升研发效能,Facebook 还针对性的开发了新的 Build System - Buck。
所以,MonoRepo 是管理项目代码的的一个方式,在一个代码仓库中管理多个的模块/包,不同于常见的每个模块建一个代码仓库(MultiRepo)。在图中我们可以清晰的看到常见的 MultiRepo 是每个包一个仓库,而 MonoRepo 是把所有的项目代码按照一定规则进行组织,存放于同一个代码仓库中。FaceBook 为了做更好的支持 MonoRepo 开发了 Buck。那么,如何针对性的建设一个适合我们产品团队的高效的构建系统呢?让我们带着这个疑问往下看。
项目介绍
今天介绍的 MonoRepo CI 主要是服务于 T 这个团队。这个团队主要是把 RingCentral 的产品集成到第三方的平台比如 Google、Salesforce、Microsoft 等。团队在代码管理层面主要使用 MonoRepo,团队使用敏捷开发的模式,每个 Feature Team 都负责多个 APP 的开发。
业务背景
为了更好的理解 MonoRepo,接下来我们看两个简单的场景。
第一个场景是要在 3 个项目里面实现一个 A 用户向 B 用户发送短信的功能,当使用 MultiRepo 时,需要在三个 Repo 中都实现相应的功能,而 MonoRepo 只需要在一个通用组件的这个 package 中实现相应的功能,各个项目便可以直接引用。
第二个场景是要实现一个把企业的通讯记录保存到第三方 APP 的功能,需要在 B、C 两个项目中实现该功能,同时 A 项目不需要该功能。当使用 MultiRepo 时,需要在其中的两个 Repo 中实现相应的功能;而在 MonoRepo 中,需要在通用组件中实现公共的功能,而差异部分在项目 B 和项目 C 各自的 package 中实现。
以上是两个例子的介绍,在实际的项目开发过程中,团队有一套更清晰/标准的规则。
面对的挑战
在项目 T 团队中,采用 MonoRepo 的方式给我们带来了一些切切实实的好处。
首先,工作流的一致性。由于所有的项目放在一个仓库当中,复用起来非常方便,如果有依赖的代码变动,那么用到这个依赖的项目当中会立马感知到。并且所有的项目都是使用最新的代码,不会产生其它项目版本更新不及时的情况。
其次,项目基建成本的降低。所有项目复用一套标准的工具和规范,无需切换开发环境,如果有新的项目接入,也可以直接复用已有的基建流程。比如,构建脚本等。
再者,团队协作也更加容易。一方面,大家在同一个代码仓库开发,能够方便地共享和复用代码,方便检索项目源码;另一方面,Git Commit 的历史记录也支持以功能为单位进行提交,之前对于某个功能的提交,需要改好几个仓库,提交多个 Commit,现在只需要提交一次,简化了 Commit 记录,方便协作。
当然,在享受 MonoRepo 的实践所带来的好处的同时,我们也必须要面对它所带来的挑战。
基于 MonoRepo 的项目管理,绝不是仅仅只是把代码放到一起就可以。更多更复杂的代码存放在同一个代码工程中,对工程代码的工程实践和质量保障提出了更高的要求。因此在实际场景中落地 MonoRepo,需要一套完整的工程体系来进行支撑。我们需要处理好项目间依赖分析、依赖安装、构建流程、测试流程、持续集成(Continuous Integration, CI) 及发布流程等诸多工程环节。同时,还要应对当项目规模到达一定程度后可能出现的性能问题,比如通过增量构建/测试、按需执行 CI 等等方法来解决项目构建/测试时间过长的问题等。
作为能力团队,我们需要确保支撑团队工程实践的基础服务能够适应 RingCentral 研发团队对高 EQ(Efficiency & Quality,效率和质量)的追求。
02 MonoRepo CI/CD 基础建设
研发模式与流水线
持续集成/持续交付的实践同研发团队的分支模型密不可分。我们的产品团队采用的是类似主干开发分支发布的方法。开发人员在开发一个新的特性时,会从研发主干 master 分支 checkout 代码到本地,完成相应功能的开发,推送到特性分支,然后通过 MR(Merge Request)提交代码到 master。严格意义上,我们是分支开发的,但在实际项目中,由于 feature 分支粒度小,存在的时间短,所以, 认为接近于主干开发的模式。产品发布前的回归测试阶段,需要从 master 新建一个预发分支,在自动化/手工回归测试后从该分支创建一个 Release Tag 并发布到生产。在这样的模型下要求 CI 具备单向开发/快速集成的能力,为了保证主干分支随时都处于可以发布的状态,每一个合并到主干的 Commit 都需要通过质量门禁。同时在 MonoRepo 的产品形态中,需要具备多个产品同时进行回归/发布的能力。
MonoRepo CI/CD 建设思路
在我们进行 MonoRepo 的 Pipeline 的能力建设时,首要关注的方面是标准化的构建。这里所说的标准化,包括标准化的构建脚本、标准化的测试脚本、标准化的源代码结构。其次,通过对 CI/CD 运行环境的容器化,实现统一的资源管理,利用容器的特性实现动态的按需的精准的构建,提高软硬件资源的使用率。同时,通过对于支撑工具的持续改进,降低使用门槛,确保 CICD 具备快速接入,高效构建的能力。最后,由于研发团队的工作严重依赖一个稳定的流水线,我们为 Pipeline 添加了一系列的监控,进行数据驱动的持续改进。
接下来,我将围绕 Pipeline 的不同阶段进行介绍。
MR 阶段
MR(Merge Request)阶段指的是当开发完成一个功能的开发,提交一个 Merge Request 到目标分支。我们通常需要在 MR 阶段采用一系列的手段来保证主干代码的质量。
让我们通过一个小故事来了解下, Bob 完成了一个新功能的开发,新功能的 Commit 合并到了主干,此时触发了 A、B 项目的构建。与此同时,Jerry 完成了另一个新功能的开发,新功能的 Commit 也合并到了主干,触发了 C 项目的构建。完成开发任务的 Bob 与 Jerry 就去茶水间品尝咖啡了。项目部署成功后,负责测试的 Linda 在测试的过程中,发现 C 项目的一个主要功能无法正常工作了,提交了一个 issue 后就去找 Jerry 了解情况。Jerry 排查后,发现是 Bob 新合并的 Commit 影响到了项目 C。
在 MonoRepo 实践中,由于工程的复杂性,不可避免的会出现对代码影响范围的误判,从而对项目造成影响。因此对于 MR 阶段的质量门禁提出了更高的要求。
同时,在 MonoRepo 实践中,我们还需要面对来自不同子项目的特殊需求,一个新的子项目的接入往往需要专门的人来处理等一系列问题。
标准化
面对以上的一系列问题,将会从两个维度来解决。
一方面,MonoRepo 不仅仅只是把代码放到一起,在这里的每个项目需要考虑依赖分析,模块划分,每个子项目都需要遵循一定的项目规范。对于接入的项目,主要有以下两个的配置:
1. 每个项目都需要声明标准的构建命令,确保在 CI 接入的过程中可以使用统一的脚本;
2. 每个项目的`package.json`中需要声明 CI 的配置(下文会具体介绍),用于声明需要在 CI 上执行的项目。
另一方面,对于 Jenkins Files,在测试阶段通过并发执行的方式来确保每个 MR 可以在最快的时间内执行完毕;为了确保 Pipeline 在失败的时候可以快速的进行分析,这里需要定义好每个 Stage 执行的任务(比如,Test Stage 只会执行跟测试相关的任务)以及 Logger 分析;最后,还有精准构建分析,在这一步会分析出需要构建的项目而非所有的项目都进行构建。
解决上述问题,显然不是改进的终点。我们对工程实践的持续改进,进一步提升了团队的效能。首先,降低了开发 Pipeline 的成本;其次,实现了新项目的 0 维护成本,秒级接入,在效率提升的同时又可以减少资源的浪费,降低服务器的使用成本;最后,确保 Pipeline 整体的稳定性, 让代码中隐藏的问题可以在 MR 阶段被发现。
接下来我们看下 Pipeline 是如何工作的呢?
当开发人员向目标分支提交一个 Merge Request 时,会自动触发对应的 MR Job(如上图)。流水线在 Prepare Stage(准备阶段)需要做一些初始的动作,最重要的是根据代码改动进行精准的分析。那么,如何做到精准分析呢?首先,脚本会根据变更的代码去计算出影响到了哪些的项目,把项目列表存到一个列表;然后,通过执行脚本,把相应的信息存入系统变量;最后,在测试阶段通过项目列表判断需要并发执行多少的进程的方式在不同的节点上去执行测试。在执行的过程中通过项目名字以及脚本获取到的相应的项目路径,确保每个线程只执行对应项目的测试。举例说,假设你的代码影响到 10 个上层应用,这时候 Pipeline 会根据变更的代码判断出影响范围,自动创建 10 个进程并且分发到 10 台可执行的机器上执行相应的构建,通过这种并发的方式确保 MR 可以在规定的时间内执行完毕。
同时,在 MR 阶段会根据获取到的信息判断是否需要执行相应的端到端自动化测试,在接下来会更详细的介绍。
在此阶段,有新的项目加入,开发人员只需要按照规则添加 CI 配置就可以完成 CI 的接入工作,无需邀请专门的人员来处理。
PUSH 阶段
接下来介绍的是 Push Stage,也就是当 Commit 合并到主干或者创建 Release 分支/Tag 通过 Push Event(GitLab WebHooks)会触发的操作。为了效率/资源的平衡,在我们项目中并不对推送到 feature 分支的事件进行流水线的执行(除非提交信息中包含特殊声明)。
让我们通过一个场景来了解下。Bob 完成了当前 release 的最后一个功能的开发,提交了 Commit 到主干触发了项目的构建,便去喝咖啡了。另一边是在焦急等待测试的 Linda,临近下班,迟迟没有收到通知,无法进行测试,这时候 QA Linda 就去问 Bob,怎么还不能测试呢?Bob 回来一看 Job,由于接近当前 release 的最后时间点,开发人员在频繁的提交 Commit,每一个 Commit 都需要进行相应的构建,消耗资源,导致自己提交的 MR 还在排队等待...这个故事告诉我们每次的构建都需要消耗资源,高频的构建可能会占用所有的可用资源,导致出现排队等待、等待时间长的问题。
标准化
此阶段的设计和 MR 阶段的采用了相同的设计规则。同时,在还利用了声明式的方法将 Pipeline 中不一致的部分抽象出来,在 ConfigMap 中引入了项目配置、YML 文件中配置相应的部署信息、以及其他的规则配置来确保 Pipeline 在处理差异化上可以更加的灵活,所有的 Pipeline 遵循一套标准。当有新的项目需要接入只需要通过简单的 YML 配置便可以实现,而整体的 Pipeline 开发维护也可以更加的高效。在使用场景上,我们对 Push 阶段的 Pipeline 分为两个部分,Commit 合并到主干和通过创建 Branch/Tag 的方式发布到回归(Regression) /生产 (Production) 环境。
我们来看下具体的流程
首先,来说下 Commit 合并到主干,也就是当开发人员完成一个功能后合并相关的 Commit 到主干, 此时 GitLab 的 WebHooks 监听到了一个 Push 事件并且目标分支是主干分支(master), 会触发对应的 Jenkins Job(如上图)。在 Prepare Stage(准备阶段)会执行前期的准备工作。这里重点介绍下精准分析,在分析策略方面按照以下的流程进行。首先,获取变更的代码;然后,执行分析的脚本,接着,脚本会根据变更的代码(文件级别)计算出影响到了哪些的子项目;之后,在通过项目的包名在 ConfigMap 中获取到对应的 Job;最后,在通过并发执行的方式触发对应的 Jenkins Job。这里有两个相关的配置,在 Package.json 中声明 CI 的配置,用于计算出变更的代码影响的项目; 其次,在 ConfigMap 中声明项目与 Jenkins 的 job 的对应关系,用于后续触发项目对应的 Job。每个项目的 Job 都包含 3 个标准的 Stage( Prepare、Build、Deploy)。
其次,通过创建 branch/Tag 的方式发布到 regression/production 环境,也就是当开发人员创建完成一个 regression 分支或者 tag, 此时 GitLab 的 WebHooks 监听到了一个 Push 事件并且匹配到对应的分支名称。这里需要强调的是团队在实现这一套回归/发布流程的时候,事先定义好 branch/Tag 的命名规范,确保每个项目的命名都是唯一的(见上图中的命名规范)回目标分支是主干分支。当 WebHooks 匹配到对应的规则后则会触发 Jenkins Job,在 Pipeline 的执行过程中,首先,在 Prepare 阶段会做些前些准备的工作,重要的是会去获取触发此次构建对应的分支名/标签名;然后,通过命名解析规则获取相应需要构建的项目;最后,触发子 Job 的构建。
质量门禁
在 Pipeline 中我们也需要设定质量门禁确保主干代码的稳定性、可用性。当提交一个 Merge Request 后通过 WebHooks 的 Merge Request Event 会触发相应的构建,构建后会把结果推送到 GitLab。如果 Pipeline 失败,我们会通过 GitLab 的接口将 Accept Merge Request 的按钮变为不可点击(红色),并反馈结果给开发人员,开发人员需要进行问题分析,修复问题。当开发人员修复完问题后,提交新的 Commit,此时,Pipeline 会再次执行构建,当新提交的代码通过质量门禁(在我们的实践中采用的规则是,测试都通过并且代码覆盖率大于 80%)后,我们会将 Accept Merge Request 的按钮变为可点击(绿色)。当 Code Review 通过便由评审人点击 Merge Button 将代码合并到主干。
在我们的实践中,MR 的质量门禁包含静态代码检查通过,UT(Unit Test)/IT(Integration Test)通过,同时,变更行代码覆盖率达到预期。在 MR 阶段,端到端的自动化测试是可选择的。如果判断出需要端到端自动化测试的 MR,则需要确保对应的端对端测试通过;如果没有相关的自动化测试便不会触发这一规则。
Pipeline 建设
Pipeline 的基础设施分成了两部分:
一个是对于 MonoRepo。对于 MonoRepo,这里需要统一的构建规范,CI/CD 的配置;
另一个是针对 CI Pipeline。对于 Pipeline,第一,外部我们需要建立一些相应的工具,docker 的镜像管理,构建;第二,建立标准的 Pipeline、通用的组件、配置文件管理、工具集。第三,对于 Jenkins Job 建立 Job 模版,通过这种方式可以快速的新建一个 Jenkins Job 插件管理,用户管理。第四,需要对 Job 建立数据的监控、Pipeline 监控、服务器监控、统一工件的管理。
CI/CD 流水线
以上我们对 MonoRepo CI/CD 做了详细的介绍,上图描述了 Pipeline 从 MR 阶段到合并至主干的流程,通过上图,可以较为直观的看出,相比较于 MutiRepo 的 Pipeline MonoRepo 会复杂许多,Pipeline 的流程上由原来的 1-1 变成了 1-N 的关系,这意味着在构建的量级上出现几倍的增长。这对基础的架构的稳定性、可靠性是一个大的挑战。在之前的分析中,我们知道 MonoRepo CI/CD 面临着构建稳定性、高效性等的问题,在 Pipeline 的建设中通过并发构建精准的推荐项目以及结合日志分析、自动自愈的能力让我们的 Pipeline 达到高效精准稳定的构建。最终,成功的确保在可接受范围内完成相应构建的效果。
03 端到端自动化测试在 CI/CD 中的实践
端到端自动化测试(下文中会使用 E2E)是最贴近用户行为的一种测试,信任度也是最高的。但是整体的稳定性,执行时间长往往又困扰着团队。那么,在 CI 中又该如何让 E2E 测试发挥最大的作用呢?
MR 阶段 可配置化
E2E 测试需要依赖真实的服务,要在浏览器上去模拟用户交互的操作步骤。E2E 的测试相对于其他的测试会更耗资源,执行时间也会更久。这意味着 E2E 测试会消耗大量的测试资源,需要较长的等待时间。团队希望 Bug 可以尽快被识别出来。那么,在 MR 阶段对于 E2E 的执行策略要如何更好的设计呢?
在这个阶段我们通过一种灵活的方式,在利弊之间选取一个平衡点,让团队成员可以自由选择是否执行相应的测试,从而确保核心的改动合并到主干是更加的稳定、可靠。也就是当我们提交一个 MR 后可以通过 Filter 来指定执行特定的测试集合,也可以通过 Skip 让 CI 不执行。同时,还可以在没有指定的情况下,则会由流水线自动分析出需要执行的 Case,并自动在 CI 上执行测试。测试结果会发送给 Commit 提交的人并自动 Comment 在 Merge Request 中,端对端的测试失败不会将 Merge Button 置为 Disable,而是交由开发人员自行判断是否合并(我们推荐的做法是尽可能的确保 E2E 也测试通过,若有紧急的功能可以先合并后修复测试),在这里更多依靠的是团队之间的契约。
Day-to-Day
E2E daily 的执行分成两个场景,一个是在 commit 合并到 master 之后执行的自动化测试,另一个是每天晚上通过定时任务触发的测试。
我们针对核心的项目(线上用户数较多的项目)会启用由 commit 合并到 master 之后执行的自动化测试。每次 MR 合并后,都会部署一套 E2E 的测试环境,然后根据配置执行 P0/P1 的自动化测试用例。执行完成后,结果会通知给 Commit 的提交者。对于非核心项目在这个阶段不执行 E2E 自动化测试。
另一个是每天晚上通过定时任务触发的测试。在我们的团队中,将具体的子项目分为了 Active / In-Active 两种状态。这是因为团队开发并维护了十几个子项目,其中活跃且密集开发的项目大概是一半。我们将活跃且密集开发的项目称之为 Active 项目,反之称为 In-Active。这时候针对 Active 的项目,每天会执行对应环境的部署,部署成功后执行自动化测试,有专门的人进行维护(分析失败的原因,修复不稳定的测试用例等),确保 Pass Rate 在 90%以上。In-Active(当前不在开发中)的项目只会执行 P0 的 Case,一周进行一次分析,节约维护成本;同时,可以确保尽快的发现问题及时修复。
回归发布
针对回归测试,在部署完测试环境后会部署一套 E2E 回归测试的环境,部署成功后会触发对应的自动化测试 Job,执行后会将结果通知到群里,失败的用例需要分析是脚本问题还是 Bug。如果,发现是一个 Bug,开发人员需要修复后重新提交 Commit,执行一次测试。
对于生产环境的发布,线上部署完成后会执行 PDV 用例。这里会直接使用生产环境进行测试,在用例的选择上会选择核心重要的用例,要确保可以在规定时间内完成相应的测试,同时确保测试不会在线上留下脏数据等。
04 稳定性,可靠性建设
接下来了解下稳定性、可靠性建设。让我们继续来看一个发生在研发团队的日常故事。Bob 今天很开心解决了困扰一周的问题,提交了一个 Merge Request 之后便去品尝咖啡了,刚好遇见了老同事 Jerry,跟 Jerry 分享了他的开心时刻,开始期待一个美好的周末。回到位置的 Bob,看到一条消息提醒,告诉他 Pipeline Failed,打开 MR 相关信息,出现在眼前的是看不懂的错误信息,需要如何排查问题呢?
对于不清晰的信息,大胆的猜测可能存在不稳定问题、代码本身问题等等,作为 Pipeline 的设计者也需要考虑用户(开发团队成员)的体验,下面让我们来看看如何让开发人员拥有更好的体验?
对于流水线的稳定性可靠性建设,这里归纳了二类问题,由于网络不稳定、服务不稳定、依赖不稳定、测试不稳定导致的稳定性问题;还有一类是影响效率的问题,重复执行的 Job、执行不必要的 Job、依赖太大的问题。对于网络的问题需要分析跟网络工程师团队一起去解决。同时,在 Pipeline 层面加入自动分析,重试,取消,跳过等策略为 Pipeline 提供自动自愈的能力。
以下通过三个例子来了解下,例一:当有新的 Commit 提交后,需要去分析当前的 MR 对应的 Job 是否正在执行旧的 Commit 的构建,如果正在执行需要取消旧的 Job 重新执行新提交的 Commit 的构建。因为,当一个 MR 提交了新的 Commit 此时不论旧的 Commit 成功与否新的都必须执行。取消旧的 Commit 对应的构建可以节约一部分 CI 的资源。例二:当构建遇到已经识别出来的不稳定场景(比如,执行 install 的时候可能由于网络不稳定,服务器负载过高等),Pipeline 可以捕获到相应的异常,自动的重试相应 Stage,节约维护的成本。当然,在 Pipeline 之外需要专项的解决这类的疑难杂症。例三:在执行测试的时候加入 Retry 的方法,提升测试稳定性,在加入重试后,若还是失败需要及时的给代码提交者一个清晰的提示。当然,对于不稳定的测试,除了在 Pipeline 中加入重试,还需要推动开发人员修复不稳定的测试用例,这样才能更好解决由于测试不稳定带来的风险。
数据驱动 - 提升稳定性/整体效能
相信每一位从事研发效能度量的实践者都听过著名管理大师彼得.德鲁克的名言 -- 没有度量,就没有管理。度量是优化与改进的前提。CI/CD 作为研发流程中重要的一个环节,这部分的数据也是至关重要的。
在实践的过程中,通过 influxdb Plugin 收集 Jenkins Job 的过程数据,再通过 Grafana 将数据展示出来。这里我们会跟开发团队一起去探讨建立适用于团队的度量指标(每个团队都有自己的特点,所以,指标的建立需要参照团队的实践)。建立模型后,便可以进行有效分析,来帮助团队改进问题,提升效率。
05 总结
最后,经过一系列的改进跟优化,Pipeline 取得了怎样的成果呢?
1.目前 MonoRepo 中的十几个项目都接入标准化流水线, 整体构建效率得到了很大的提升。同时,在服务的部署以及上线到生产都是由流水线自动完成,降低了上线流程不规范带来的风险。
2.通过构建标准的流水线,MonoRepo 中的所有项目使用统一的研发流程,方便对项目进行统一管理与持续改进。
3.在 MonoRepo CI/CD 能力不完善阶段,线下发现缺陷主要依赖手工测试,随着 CI/CD 的完善,流水线的检测能力越发的健全。在线下发下的有效缺陷中,利用自动化测试(包括单元测试、集成测试、端到端自动化测试)发现的的问题占比不断提升。
总体而言,MonoRepo CI/CD 的标准化建设为我们带来实实在在的收益。
版权声明: 本文为 InfoQ 作者【RingCentral铃盛】的原创文章。
原文链接:【http://xie.infoq.cn/article/4f51fdcede0531155e2badb53】。文章转载请联系作者。
评论