如今,大多数软件产品都是以团队形式开发的。 团队发展的成功条件可以以简单的方案的形式提出。

编写代码后,必须确保它:
- 可以用
- 它不会破坏任何东西,包括您同事编写的代码。
如果同时满足这两个条件,那么您将走上成功之路。 为了轻松检查这些条件并且不改变获利路径,他们提出了持续集成。
CI是一种工作流,您可以在其中将您的代码尽可能频繁地集成到通用产品代码中。 并且不仅要集成,还要不断检查一切是否正常。 由于您需要经常检查,因此应该考虑自动化。 您可以检查有关手动牵引力的所有信息,但不值得,这就是原因。
- 人很贵 。 任何程序员一个小时的工作比任何服务器一个小时的工作都要昂贵。
- 人们错了 。 因此,当他们在错误的分支上运行测试或为测试人员收集了错误的提交时,可能会出现这种情况。
- 人们很懒 。 定期完成任务时,我会想到:“但是要检查什么? 我写了两行-stopudovo一切正常!” 我认为对于某些人来说,有时会想到这种想法。 但是您始终需要检查。
参与CI / CD Android应用程序所有演变的参与者Nikolay Nesterov(
nnesterov )表示,Avito移动开发团队如何引入和开发持续集成,它们如何达到每天0到450个装配体,并且建造机器每天要收集200个小时。 。
这个故事是建立在Android团队的例子之上的,但是大多数方法也适用于iOS。
从前,一个人在Avito Android团队工作。 根据定义,他不需要持续集成的任何东西:没有人可以集成。
但是应用程序不断增长,出现了越来越多的新任务,团队也不断增长。 在某个时候,是时候更正式地建立集成代码的过程了。 决定使用Git flow。

Git流的概念是众所周知的:项目中有一个通用的developer分支,对于每个新功能,开发人员都会剪切一个单独的分支,将其提交,推送,然后在他们想要将其代码注入到dev分支中时,打开拉取请求。 为了共享知识并讨论方法,我们引入了代码审查,也就是说,同事必须检查并确认彼此的代码。
支票
用眼睛看代码很酷,但还不够。 因此,引入了自动检查。
- 首先,我们检查ARC的组装 。
- 许多Junit测试 。
- 由于我们运行测试,因此我们考虑代码覆盖率 。
要了解应如何运行这些检查,让我们看一下Avito中的开发过程。
可以用以下方式表示:
- 开发人员在他的笔记本电脑上编写代码。 您可以在此处运行集成检查-使用提交钩子,或仅在后台运行检查。
- 开发人员运行代码后,他打开拉取请求。 为了使他的代码进入developer分支,您必须进行代码审查并收集所需的确认数量。 您可以在此处启用检查和构建:在所有构建成功之前,拉取请求无法合并。
- 合并拉取请求并开发代码后,您可以选择一个方便的时间:例如,晚上,当所有服务器都空闲时,并根据需要进行检查。
没有人喜欢在笔记本电脑上运行测试。 开发人员完成功能后,他想快速启动它并打开拉取请求。 如果那时候启动了一些长时间的检查,这不仅非常令人愉悦,而且会减慢开发速度:笔记本电脑正在检查某些东西时,就不可能在上面正常工作。
我们真的很喜欢在晚上进行检查,因为有很多时间和服务器,您可以散步。 但是,不幸的是,当开发功能代码时,开发人员已经没有太多动力去修复CI发现的错误。 当我在早上的报告中查看所有错误后,我会不时地思考,发现我会在以后的某个时间修复它们,因为现在在Jira摆着一个很酷的新任务,我只是想开始做。
如果检查阻止了请求请求,那么就有足够的动力,因为在构建变为绿色之前,代码不会进入开发阶段,这意味着该任务将无法完成。
结果,我们选择了这种策略:在晚上,我们会驱动最大数量的检查,其中最关键的检查是最重要的,最重要的是快速执行拉动请求。 但是我们不止于此-我们正在同时优化通过检查的速度,以使它们从夜间模式切换到对拉动请求的检查。
当时,我们的所有程序集运行起来都足够快,因此我们仅将ARC程序集,Junit测试和代码覆盖率计算与拉取请求阻止程序一起包括在内。 他们打开了它,考虑了一下,并放弃了代码覆盖范围,因为他们认为我们不需要它。
我们花了两天时间完成了基本的CI设置(以下是临时估计,是估计规模所需的)。之后,他们开始进一步思考-我们是否正确检查? 我们是否根据请求请求正确启动构建?
我们在打开了拉取请求的分支的最后一次提交上开始构建。 但是检查此提交只能表明开发人员编写的代码有效。 但是他们没有证明他没有破坏任何东西。 实际上,在将功能注入到develop分支之后,您需要检查其状态。

为此,我们编写了一个简单的bash脚本
premerge.sh:
在这里,所有来自development的最新更改都被简单地提取并合并到当前分支中。 我们将premerge.sh脚本添加为所有构建的第一步,并开始准确检查我们想要的内容,即
Integration 。
花了三天时间来定位问题,找到解决方案并编写此脚本。开发应用程序,出现越来越多的任务,团队不断壮大,并且premerge.sh有时开始让我们失望。 在开发过程中,渗透的冲突改变了程序集。
如何发生这种情况的一个示例:

两名开发人员同时开始锯切功能A和B。功能A的开发人员发现了项目中未使用的
answer()
函数,并且像一个不错的侦查员一样将其删除。 同时,功能B的开发人员在其分支中为此功能添加了新调用。
开发人员完成工作并同时打开请求请求。 构建开始,premerge.sh检查两个拉动请求是否具有全新的开发状态-所有检查均为绿色。 在合并了拉取请求特征A之后,合并了拉取请求特征B ... oom! 开发中断是因为在开发代码中调用了一个不存在的函数。

如果不发展,这是
局部灾难 。 整个团队无法收集任何东西进行测试。
碰巧我最常参与基础架构任务:分析,网络,数据库。 也就是说,我编写了其他开发人员使用的函数和类。 因此,我经常遇到这种情况。 我什至一次有这样一张照片。

由于这不适合我们,因此我们开始研究如何防止这种情况的选择。
如何不打破发展
第一种选择:
在升级开发时重建所有拉取请求。 如果在我们的示例中首先开发了具有功能A的请求,则将重新构建功能B的请求,因此,由于编译错误,检查将失败。
要了解将花费多长时间,请考虑一个具有两个PR的示例。 我们打开两个PR:两个版本,两个测试启动。 在第一个PR投入开发后,必须重新构建第二个PR。 总共两次PR支票检查需要三个PR:2 +1 = 3。
原则上,这是正常的。 但是我们查看了统计数据,我们团队中的典型情况是10个未完成的PR,然后检查的次数就是进度的总和:10 + 9 + ... + 1 =55。也就是说,要接受10个PR,您需要重建55次。 这是一种理想的情况,当所有检查都第一次通过时,在处理这十个请求时,没有人打开另一个请求请求。
想象一下您自己是一个需要先有时间按下“合并”按钮的开发人员,因为如果这是由邻居完成的,则您将不得不等到所有装配再次通过后才能进行...不,那将行不通,这将严重降低开发速度。
第二种可能的方式:
在代码审查后收集拉取请求。 也就是说,打开拉取请求,从同事那里收集必要数量的更新,修复所需的内容,然后运行构建。 如果成功,则拉取请求与developer合并。 在这种情况下,没有其他重新启动,但是反馈会大大降低。 作为开发人员,当我打开请求请求时,我立即想看看他是否打算这样做。 例如,如果测试失败,则需要快速修复。 在构建延迟的情况下,反馈会变慢,这意味着整个开发。 这也不适合我们。
结果,仅剩下第三种选择-
循环 。 我们所有的代码,所有源代码都存储在Bitbucket服务器的存储库中。 因此,我们不得不为Bitbucket开发一个插件。

该插件覆盖了拉取请求合并机制。 开头是标准的:PR打开,所有程序集开始,代码审查通过。 但是在代码审查通过之后,开发人员决定单击“合并”,插件会检查以查看开发检查针对的状态。 如果在构建后开发成功进行更新,则该插件将不允许您将这样的请求合并到主分支中。 它将只是相对于新开发重新启动构建。

在我们的示例中,发生冲突的更改时,由于编译错误,此类构建将失败。 因此,功能B的开发人员将必须更正代码,重新启动检查,然后插件将自动应用提取请求。
在实施此插件之前,每个请求平均要进行2.7次测试。 使用该插件可以启动3.6次。 它适合我们。
值得注意的是,此插件有一个缺点:它仅重新启动构建一次。 也就是说,无论如何,仍然存在一个小窗口,可以通过该窗口进行冲突的更改。 但是这种可能性并不高,我们在启动次数和失败概率之间做出了折衷。 两年来,它只开过一次枪,因此可能没有白费。
我们花了两个星期的时间为Bitbucket编写了该插件的第一个版本。新支票
同时,我们的团队不断壮大。 添加了新的检查。
我们认为:如果可以预防错误,为什么要修复? 因此,他们介绍了
静态代码分析 。 我们从Android SDK中包含的lint开始。 但是那时他根本不知道如何使用Kotlin代码,而且我们已经有75%的应用程序是用Kotlin编写的。 因此,内置的
Android Studio检查已添加到lint中
。为此,我必须非常变态:拿Android Studio,将其打包在Docker中,然后使用虚拟监视器在CI上运行,以便它认为它在真正的笔记本电脑上运行。 但这行得通。
同样在这个时候,我们开始编写大量的
仪器测试并实施了
屏幕截图测试 。 这是为单独的小视图生成参考屏幕截图时的测试,测试是从视图中获取屏幕截图,并将其与像素逐像素直接比较。 如果存在差异,则表示布局已移至某处或样式有误。
但是,仪器测试和屏幕截图测试需要在设备上运行:在模拟器或真实设备上。 鉴于有很多测试并且经常进行,因此您需要一个完整的农场。 要启动自己的服务器场很费力,因此我们找到了现成的选项-Firebase测试实验室。
Firebase测试实验室
之所以选择Firebase是因为Firebase是Google的产品,也就是说,它必须可靠并且不可能死亡。 价格可承受:真实设备每小时5美元,仿真器每小时1美元。
在我们的CI中实施Firebase测试实验室大约花了三周的时间。但是团队不断壮大,不幸的是,Firebase开始让我们失望。 当时,他没有SLA。 有时,Firebase让我们等到所需数量的测试设备可用后才开始免费使用,并没有立即执行它们,这正是我们想要的。 排队等候长达半小时,这是一个很长的时间。 每个PR都要进行仪器测试,延迟会极大地减慢开发速度,然后每月还需支付一定的费用。 总的来说,由于团队已经足够壮大,因此决定放弃Firebase并在内部进行查看。
Docker + Python + Bash
我们将docker塞入模拟器中,编写了一个简单的Python程序,该Python程序在适当的时间以正确的版本引发了适当数量的模拟器,并在必要时停止了它们。 当然,还有几个bash脚本-没有它们的地方呢?
创建我们自己的测试环境花了五个星期。结果,每个拉取请求都有一个广泛的阻止检查合并列表:
- ARC的大会;
- Junit测试
- 皮棉;
- Android Studio检查;
- 仪器测试;
- 屏幕截图测试。
这避免了许多可能的故障。 从技术上讲,一切正常,但是开发人员抱怨等待结果的时间太长。
太久了多少钱? 我们将来自Bitbucket和TeamCity的数据上传到分析系统,并意识到
平均等待时间为45分钟 。 也就是说,开发人员打开一个拉取请求,平均预期构建时间为45分钟。 我认为这很多,您不能那样做。
当然,我们决定加快所有构建速度。
加快速度
看到通常的建筑是一致的,我们
买的第一件事
就是铁 -广泛的开发是最简单的。 建筑物停止排队,但等待时间仅减少了一点,因为一些检查本身已经追逐了很长时间。
我们删除了过长的支票
我们的持续集成可以捕获这些类型的错误和问题。
- 不去 。 当由于冲突更改而无法执行某些操作时,CI可能会捕获到编译错误。 就像我说的那样,没人能收集任何东西,事态发展,每个人都会感到紧张。
- 行为上的错误 。 例如,在构建应用程序时,但是在单击按钮时,它崩溃了,或者根本没有按下该按钮。 这很糟糕,因为这样的错误可以到达用户。
- 布局错误 。 例如,按下一个按钮,但向左移动了10个像素。
- 技术债务增加 。
通过查看此列表,我们意识到只有前两点很关键。 我们首先要抓住这样的问题。 在设计审查阶段检测布局中的错误,然后轻松修复。 处理技术债务需要单独的流程和计划,因此我们决定不对其进行拉动请求检查。
基于此分类,我们整理了整个检查清单。
删掉Lint并推迟了晚上的发布时间:以便它给出项目中有多少问题的报告。 我们同意单独承担技术债务,但
完全拒绝了Android Studio检查 。 Docker用于启动检查的Android Studio听起来很有趣,但是在支持方面却带来了很多麻烦。 对Android Studio版本的任何更新都是为了与模糊的错误作斗争。 维护屏幕截图测试也很困难,因为该库运行不稳定,存在误报。
屏幕截图测试已从检查列表中删除 。
结果,我们离开了:
Gradle远程缓存
没有严格的检查,情况就会好起来。 但是,完美无止境!
我们的应用程序已被划分为大约150个gradle模块。 通常,在这种情况下,Gradle远程缓存运行良好,因此我们决定尝试一下。
Gradle远程缓存是一项服务,可以在单独的模块中缓存单个任务的构建工件。 Gradle并没有实际编译代码,而是通过HTTP取消了远程缓存,并询问是否有人已经执行了此任务。 如果是这样,只需下载结果。
启动Gradle远程缓存很容易,因为Gradle提供了Docker映像。 我们在三个小时内做到了这一点。所需要做的就是启动Docker并在项目中注册一行。 但是,尽管您可以快速启动它,以便一切正常,但这将需要很多时间。
以下是缓存未命中的图表。

在开始时,通过缓存的未命中百分比约为65。三周后,我们设法将该值提高到20%。 事实证明,由于Gradle错过了缓存,Android应用程序收集的任务具有奇怪的传递依赖关系。
通过连接缓存,我们极大地加快了装配速度。 但是除了组装之外,仪器测试仍在追逐,并且追逐了很长时间。 也许并非每个拉取请求都需要进行所有测试。 为了找出答案,我们使用了影响分析。
影响分析
根据请求,我们构建git diff并找到修改后的Gradle模块。

仅运行那些测试修改后的模块以及所有依赖它们的模块的仪器测试是有意义的。 对相邻模块运行测试没有任何意义:代码在那里没有更改,没有任何破坏。
仪器测试不是那么简单,因为它们必须位于顶级“应用程序”模块中。 我们应用了字节码分析启发法来了解每个测试属于哪个模块。
升级仪器测试只花了八个星期的时间,以仅测试所涉及的模块。验证加速措施已成功运行。 从45分钟开始,我们达到了大约15分钟。等待构建的四分之一小时已经很正常了。
但是现在,开发人员开始抱怨他们不清楚启动哪个版本,日志将在哪里查看,为什么该版本为红色,哪个测试失败等等。

反馈问题减慢了开发速度,因此我们尝试提供有关每个PR和构建的最易理解和详细的信息。 我们从对Bitbucket for PR的评论开始,指出哪个版本下降以及为什么,在Slack中写了针对性的消息。 最后,他们为PR页面创建了一个仪表板,其中包含当前正在运行的所有构建及其状态的列表:内联,开始,崩溃或结束。 您可以单击内部版本并进入其日志。
六个星期用于详细反馈。计划
我们传递到最新的历史。 解决了反馈问题后,我们进入了一个新的高度-我们决定建立自己的仿真器农场。 当有许多测试和仿真器时,它们很难管理。 结果,我们所有的仿真器都迁移到了具有灵活资源管理功能的k8s集群。
此外,还有其他计划。
- 返回Lint (以及其他静态分析)。 我们已经朝着这个方向努力。
- 在所有版本的SDK的PR阻止程序上运行所有的端到端测试 。
因此,我们追溯了Avito中持续集成的发展历史。 现在,我想从经验丰富的角度给出一些建议。
小费
如果我只能给出一个建议,那就是:
请谨慎使用shell脚本!
Bash是一个非常灵活且功能强大的工具,在其上编写脚本非常方便快捷。 但是和他一起你可以掉入陷阱,不幸的是,我们掉入了陷阱。
这一切都始于在构建机器上运行的简单脚本:
但是,正如您所知,随着时间的推移,一切都会发展并变得复杂-让我们在另一个脚本中运行一个脚本,在其中传递一些参数-最后,我不得不编写一个函数,该函数确定现在我们要使用的嵌套bash级别以替换必要的引号,这样一切就开始了。

您可以想象开发此类脚本所涉及的工作。 我建议您不要陷入这个陷阱。
有什么可以替代的?
- 任何脚本语言。 使用Python或Kotlin脚本编写更加方便,因为它是编程而非脚本。
- 或者以您的项目的自定义gradle任务的形式描述所有构建逻辑。
我们决定选择第二个选项,现在我们正在系统地删除所有bash脚本并编写许多自定义gradle shuffle。
提示#2:让基础架构保持在代码中。如果Continuous Integration配置未存储在Jenkins或TeamCity UI界面等中,而是直接作为文本文件存储在项目存储库中,则非常方便。 这提供了可版本性。 在另一个分支上回滚或收集代码并不困难。
脚本可以存储在项目中。 与环境有关?
提示#3:Docker可以为环境提供帮助。不幸的是,它一定会帮助Android开发人员和iOS。
这是包含jdk和android-sdk的简单docker文件的示例:
FROM openjdk:8 ENV SDK_URL="https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip" \ ANDROID_HOME="/usr/local/android-sdk" \ ANDROID_VERSION=26 \ ANDROID_BUILD_TOOLS_VERSION=26.0.2 # Download Android SDK RUN mkdir "$ANDROID_HOME" .android \ && cd "$ANDROID_HOME" \ && curl -o sdk.zip $SDK_URL \ && unzip sdk.zip \ && rm sdk.zip \ && yes | $ANDROID_HOME/tools/bin/sdkmanager --licenses # Install Android Build Tool and Libraries RUN $ANDROID_HOME/tools/bin/sdkmanager --update RUN $ANDROID_HOME/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS_VERSION}" \ "platforms;android-${ANDROID_VERSION}" \ "platform-tools" RUN mkdir /application WORKDIR /application
编写了这个docker-file(我要告诉你一个秘密,您不能编写它,但是可以从GitHub将其拉出)并收集映像,您将获得一个虚拟机,可以在其上构建应用程序并运行Junit测试。
为什么这样做有意义的两个主要论点是可伸缩性和可重复性。 使用泊坞窗,您可以快速建立十二个构建代理,它们的环境将与旧的完全相同。 这使CI工程师的工作更加轻松。 将android-sdk推入docker非常简单,而仿真器则稍微复杂一些:您必须花点时间(好吧,或者再次从GitHub下载完成的)。
提示4:不要忘记检查不是为了检查,而是为了人。快速且最重要的是,清晰的反馈对于开发人员而言非常重要:他们发生了什么损坏,什么测试失败,构建日志在哪里。
提示5:持续集成请务实。清楚地了解要防止的错误类型,愿意花费多少资源,时间和计算机时间。 例行检查时间过长,例如可以整夜改期。 那些犯错不是很重要的人应该被完全抛弃。
提示6:使用现成的工具。现在有许多公司提供云CI。

对于小型团队,这是一个不错的出路。 您无需维护任何东西,只需支付一些钱,收集您的应用程序甚至进行仪器测试。
提示7:在大型团队中,内部解决方案更有利可图。但是迟早,随着团队的成长,内部解决方案将变得更加有利可图。 这些决定有一点。 在经济学中,有一条收益递减法则:在任何项目中,后续的改进都更加困难,需要越来越多的投资。
经济描述了我们的一生,包括持续集成。 我为持续集成开发的每个阶段制定了工作时间表。

可以看出,任何改进都变得越来越困难。 从这张图可以看出,持续集成的发展必须与团队规模的增长保持一致。 对于两个人的团队,花50天时间开发内部仿真器场是一个不错的主意。 但是同时,对于大团队来说,由于集成问题,解决通讯问题等原因,根本不进行持续集成也是一个坏主意。 这将花费更多时间。
我们从这样一个事实开始,那就是需要自动化,因为人们很昂贵,他们被误认为是懒惰的。 但是人们也可以自动化。 因此,所有这些相同的问题都适用于自动化。
- 自动化很昂贵。 记住工作时间表。
- 在自动化中,人们会犯错误。
- 自动化有时会很懒惰,因为所有事情都是这样。 为什么还要改进,为什么要进行所有这些持续集成?
: 20% . , . , , , - , develop, . , , - .
Continuous Integration. ., , AppsConf . . 22-23 .